Polish video canvas surface consistency
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-05 03:00:29 +08:00
parent a787a97221
commit e60177c177
5 changed files with 98 additions and 97 deletions

View File

@ -419,19 +419,19 @@ const AGENT_RESULT_MODE_LABELS: Record<AgentGenerationResultModeType, string> =
sku_replacement: "SKU 替换",
};
const VIDEO_CANVAS_PANEL_CLASS =
"pointer-events-auto overflow-hidden rounded-[28px] border border-border/60 bg-background shadow-2xl";
"pointer-events-auto overflow-hidden rounded-[28px] border border-slate-200 bg-white text-foreground shadow-[0_20px_50px_-28px_rgba(15,23,42,0.24)] dark:border-zinc-800 dark:bg-zinc-950";
const VIDEO_CANVAS_PANEL_BADGE_CLASS =
"border-border/60 bg-background text-foreground/75";
"border-slate-200 bg-white text-foreground/75 dark:border-zinc-800 dark:bg-zinc-950";
const VIDEO_CANVAS_SUBSECTION_CLASS =
"rounded-2xl border border-border/60 bg-background p-3";
"rounded-2xl border border-slate-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-950";
const VIDEO_CANVAS_TOOLBAR_CLASS =
"flex items-center gap-1.5 rounded-[28px] border border-border/60 bg-background px-2.5 py-1.5 shadow-2xl";
"flex items-center gap-1.5 rounded-[28px] border border-slate-200 bg-white px-2.5 py-1.5 shadow-[0_18px_40px_-24px_rgba(15,23,42,0.24)] dark:border-zinc-800 dark:bg-zinc-950";
const VIDEO_CANVAS_TOOLBAR_BUTTON_CLASS =
"h-9 w-9 rounded-xl border border-transparent text-foreground/75 hover:border-border/60 hover:bg-background hover:text-foreground";
"h-9 w-9 rounded-xl border border-transparent text-foreground/75 hover:border-slate-200 hover:bg-slate-50 hover:text-foreground dark:hover:border-zinc-800 dark:hover:bg-zinc-900";
const VIDEO_CANVAS_TOOLBAR_ACTIVE_BUTTON_CLASS =
"border-border/60 bg-background text-foreground shadow-sm hover:bg-background";
"border-slate-200 bg-white text-foreground shadow-sm hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900";
const VIDEO_CANVAS_COMPACT_CONTROLS_CLASS =
"rounded-full border border-border/60 bg-background px-2 py-1 shadow-sm";
"rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm dark:border-zinc-800 dark:bg-zinc-950";
function buildScopedCanvasChatId(projectId: string, scope: "agent-mode") {
const normalized = String(projectId || "").trim().toLowerCase().replace(/-/g, "");
@ -7095,8 +7095,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle className="text-base">{selectedVideoWorkflowDefinition.label}</CardTitle>
<CardDescription className="mt-1">
参考 APP 里这个节点后面通常会接这些分支,点击后会自动放到右侧并连线。
<CardDescription className="mt-1 line-clamp-2">
常用下游分支会放在这里,点击后自动放到右侧并连线。
</CardDescription>
</div>
<Badge variant="outline" className={VIDEO_CANVAS_PANEL_BADGE_CLASS}>
@ -7110,7 +7110,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
key={`${selectedVideoWorkflowDefinition.type}-${item.type}`}
size="sm"
variant="secondary"
className="rounded-full border border-border/60 bg-background hover:bg-background"
className="rounded-full border border-slate-200 bg-white hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
onClick={() => handleAddWorkflowNode(item.type)}
>
{item.label}
@ -7126,7 +7126,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle className="text-base">工作流执行</CardTitle>
<CardDescription className="mt-1">
<CardDescription className="mt-1 line-clamp-2">
{selectedWorkflowActionHint}
</CardDescription>
</div>
@ -7200,7 +7200,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
<div className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
上游提示
</div>
<div className="mt-2 text-sm leading-6 text-foreground">
<div className="mt-2 line-clamp-4 whitespace-pre-line text-sm leading-6 text-foreground">
{selectedWorkflowPrompt.trim()
? selectedWorkflowPrompt
: "当前还没有抽取到可执行提示词,先在上游接入脚本、分镜或参考素材。"}
@ -7217,7 +7217,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
{selectedWorkflowLinkedAgentProject.title || selectedWorkflowLinkedAgentProject.id}
</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">
再次点击下方按钮会把当前视频画布上下文同步进去,并直接打开该项目。
再点下方按钮会同步当前视频画布上下文,并直接打开该项目。
</div>
</div>
) : null}
@ -7245,7 +7245,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
variant="secondary"
onClick={() => void handleOpenSelectedWorkflowInAgent()}
disabled={isAnyWorkflowExecutionRunning}
className="w-full rounded-2xl border border-border/60 bg-background hover:bg-background"
className="w-full rounded-2xl border border-slate-200 bg-white hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
>
{isSelectedWorkflowAgentRunning ? (
<>
@ -7267,7 +7267,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
variant="secondary"
onClick={() => void handleCreateSelectedAgentResultBranches()}
disabled={isAnyWorkflowExecutionRunning}
className="w-full rounded-2xl border border-border/60 bg-background hover:bg-background"
className="w-full rounded-2xl border border-slate-200 bg-white hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
>
{isSelectedWorkflowBranchRunning ? (
<>
@ -7286,7 +7286,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
variant="secondary"
onClick={() => void handleRunStoryboardBatch()}
disabled={isAnyWorkflowExecutionRunning}
className="w-full rounded-2xl border border-border/60 bg-background hover:bg-background"
className="w-full rounded-2xl border border-slate-200 bg-white hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-900"
>
{isSelectedWorkflowBatchRunning ? (
<>

View File

@ -66,10 +66,10 @@ export function CanvasWorkspace({
const WORKFLOW_HANDLE_SIZE = 16;
const isWorkflowCanvas = workspaceVariant === "video";
const overlayPanelClassName = isWorkflowCanvas
? "rounded-full border border-border/60 bg-background px-3 py-1.5 text-xs text-muted-foreground shadow-sm"
? "rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-muted-foreground shadow-sm dark:border-zinc-800 dark:bg-zinc-950"
: "bg-background/80 backdrop-blur-sm rounded-md px-2 py-1 text-xs text-muted-foreground";
const zoomOverlayClassName = isWorkflowCanvas
? "absolute bottom-4 right-4 rounded-full border border-border/60 bg-background shadow-sm"
? "absolute bottom-4 right-4 rounded-full border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-950"
: "absolute bottom-4 right-4 bg-background/80 backdrop-blur-sm rounded-lg flex items-center gap-1 px-1 py-1 shadow-sm border border-border/50";
type SnapGuideLines = {
@ -1325,7 +1325,7 @@ export function CanvasWorkspace({
className={cn(
"px-4 py-2 text-sm text-primary shadow-sm",
isWorkflowCanvas
? "rounded-full border border-primary/20 bg-background"
? "rounded-full border border-primary/20 bg-white dark:bg-zinc-950"
: "rounded-md bg-background/90"
)}
>
@ -1525,8 +1525,8 @@ export function CanvasWorkspace({
{showEmptyState && nodes.length === 0 && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
{isWorkflowCanvas ? (
<div className="rounded-[30px] border border-border/60 bg-background/90 px-8 py-6 text-center shadow-[0_24px_60px_-34px_rgba(15,23,42,0.35)] backdrop-blur-xl">
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-2xl border border-border/60 bg-background text-foreground shadow-sm">
<div className="rounded-[30px] border border-slate-200 bg-white px-8 py-6 text-center shadow-[0_24px_60px_-34px_rgba(15,23,42,0.18)] dark:border-zinc-800 dark:bg-zinc-950">
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-2xl border border-slate-200 bg-white text-foreground shadow-sm dark:border-zinc-800 dark:bg-zinc-950">
<GitBranch className="h-5 w-5" />
</div>
<p className="text-lg font-semibold text-foreground">{workspaceLabel}还是空的</p>

View File

@ -2630,7 +2630,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<div
ref={containerRef}
className={cn(
"relative w-full h-full bg-card rounded-[4px] overflow-hidden border shadow-sm transition-colors",
"relative h-full w-full overflow-hidden rounded-[4px] border bg-white shadow-sm transition-colors dark:bg-zinc-950",
isSelected
? "rounded-none border-[2px] border-[#27a8ff] shadow-none"
: "border-transparent"
@ -2644,7 +2644,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
{isCompareMode && hasCompareSource ? (
<div
ref={compareFrameRef}
className="relative h-full w-full overflow-hidden bg-muted/30 select-none cursor-col-resize"
className="relative h-full w-full cursor-col-resize select-none overflow-hidden bg-slate-50 dark:bg-zinc-900"
style={{ touchAction: "none" }}
data-canvas-no-drag="true"
onPointerDown={(event) => {
@ -2661,7 +2661,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
clipPath: `inset(0 ${100 - compareSplitPercent}% 0 0)`,
}}
>
<div className="absolute inset-0 bg-muted/30" />
<div className="absolute inset-0 bg-slate-50 dark:bg-zinc-900" />
<img
src={displayImageSrc}
alt={displayTitle}
@ -2682,7 +2682,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
clipPath: `inset(0 0 0 ${compareSplitPercent}%)`,
}}
>
<div className="absolute inset-0 bg-muted/30" />
<div className="absolute inset-0 bg-slate-50 dark:bg-zinc-900" />
<img
src={displayCompareSrc}
alt={`${displayTitle}-原图`}
@ -2698,20 +2698,20 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
/>
</div>
<div
className="pointer-events-none absolute inset-y-0 z-30 w-[2px] bg-white/90 shadow-[0_0_0_1px_rgba(0,0,0,0.45),0_0_14px_rgba(255,255,255,0.55)]"
className="pointer-events-none absolute inset-y-0 z-30 w-[2px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.45),0_0_10px_rgba(255,255,255,0.45)]"
style={{ left: `${compareSplitPercent}%`, transform: "translateX(-50%)" }}
/>
<div
className="pointer-events-none absolute top-1/2 z-40 h-7 w-7 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/85 bg-black/70 text-white flex items-center justify-center"
className="pointer-events-none absolute top-1/2 z-40 flex h-7 w-7 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white bg-slate-950 text-white"
style={{ left: `${compareSplitPercent}%` }}
aria-hidden="true"
>
&lt;&gt;
</div>
<div className="pointer-events-none absolute left-2 top-2 rounded bg-black/55 px-1.5 py-0.5 text-[10px] text-white">
<div className="pointer-events-none absolute left-2 top-2 rounded bg-slate-950 px-1.5 py-0.5 text-[10px] text-white">
新图
</div>
<div className="pointer-events-none absolute right-2 top-2 rounded bg-black/55 px-1.5 py-0.5 text-[10px] text-white">
<div className="pointer-events-none absolute right-2 top-2 rounded bg-slate-950 px-1.5 py-0.5 text-[10px] text-white">
原图
</div>
</div>
@ -2722,7 +2722,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
src={displayImageSrc}
alt={displayTitle}
className={cn(
"w-full h-full object-contain bg-muted/30",
"h-full w-full object-contain bg-slate-50 dark:bg-zinc-900",
boxCutoutMode ? "cursor-crosshair" : isThisNodePicking ? "cursor-crosshair" : "cursor-pointer"
)}
draggable={false}
@ -2870,19 +2870,19 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
)}
</div>
) : data.status === 'generating' ? (
<div className="w-full h-full flex flex-col items-center justify-center bg-muted select-none">
<Loader2 className="h-8 w-8 animate-spin text-primary mb-2" />
<span className="text-sm text-muted-foreground text-center px-3 select-none">
{detailProgressLabel || "生成中..."}
</span>
</div>
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50 select-none dark:bg-zinc-900">
<Loader2 className="h-8 w-8 animate-spin text-primary mb-2" />
<span className="text-sm text-muted-foreground text-center px-3 select-none">
{detailProgressLabel || "生成中..."}
</span>
</div>
) : data.status === 'failed' ? (
<div className="w-full h-full flex flex-col items-center justify-center bg-destructive/10 select-none">
<div className="flex h-full w-full flex-col items-center justify-center bg-red-50 select-none dark:bg-red-950/40">
<AlertCircle className="h-8 w-8 text-destructive mb-2" />
<span className="text-sm text-destructive select-none">生成失败</span>
</div>
) : (
<div className="w-full h-full flex flex-col items-center justify-center bg-muted select-none">
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50 select-none dark:bg-zinc-900">
<ImagePlus className="h-8 w-8 text-muted-foreground mb-2" />
<span className="text-sm text-muted-foreground mb-3 select-none">点击上传图片</span>
{/* 隐藏的文件输入 */}
@ -2906,7 +2906,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
{/* 处理中遮罩 */}
{isProcessing && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="absolute inset-0 flex items-center justify-center bg-slate-950">
<div className="flex flex-col items-center">
<Loader2 className="h-8 w-8 animate-spin text-white mb-2" />
<span className="text-sm text-white">处理中...</span>
@ -3000,14 +3000,14 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
{/* 提示词显示 */}
{data.prompt && isHovered && !isProcessing && (
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-3">
<div className="absolute bottom-0 left-0 right-0 border-t border-slate-800 bg-slate-950 p-3">
<p className="text-xs text-white line-clamp-2">{data.prompt}</p>
</div>
)}
{data.note && !isSelected && (
<div
className="pointer-events-none absolute top-2 left-2 inline-flex items-center gap-1 rounded-full bg-amber-100/95 px-2 py-1 text-[11px] text-amber-700 shadow-sm"
className="pointer-events-none absolute top-2 left-2 inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-1 text-[11px] text-amber-700 shadow-sm"
title="该图片含有备注"
>
<StickyNote className="h-3 w-3" />
@ -3019,7 +3019,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
type="button"
onClick={() => setSnapshotDialogOpen(true)}
className={cn(
"absolute left-2 inline-flex items-center gap-1 rounded-full bg-black/70 px-2 py-1 text-[11px] text-white shadow-sm transition-colors hover:bg-black/80",
"absolute left-2 inline-flex items-center gap-1 rounded-full bg-slate-950 px-2 py-1 text-[11px] text-white shadow-sm transition-colors hover:bg-black",
data.note && !isSelected ? "top-10" : "top-2"
)}
title={`执行快照:${agentExecutionSnapshotId}`}
@ -3102,7 +3102,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<TooltipProvider delayDuration={200}>
<div
ref={toolbarRef}
className="fixed z-[9999] flex items-center rounded-[24px] border border-slate-200 bg-white/95 px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] backdrop-blur-md dark:border-slate-700 dark:bg-slate-900/95 dark:text-gray-100"
className="fixed z-[9999] flex items-center rounded-[24px] border border-slate-200 bg-white px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-slate-700 dark:bg-slate-900 dark:text-gray-100"
style={{
top: toolbarPosition.top,
left: toolbarPosition.left,
@ -3136,7 +3136,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('enhance')}
>
<span className="mr-1.5 rounded-md border border-slate-300 px-1.5 text-[11px] leading-5 text-slate-600 dark:border-slate-500 dark:text-slate-100">HD</span>
@ -3147,7 +3147,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('enhance-pro')}
>
<Sparkles className="mr-1.5 h-4 w-4" />
@ -3158,7 +3158,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('rmbg')}
>
<Scissors className="mr-1.5 h-4 w-4" />
@ -3168,7 +3168,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('watermark')}
>
<Eraser className="mr-1.5 h-4 w-4" />
@ -3178,7 +3178,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('resize')}
>
<Maximize2 className="mr-1.5 h-4 w-4" />
@ -3188,7 +3188,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing || isDetectingText || isApplyingTextEdit}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('text-edit')}
>
<Type className="mr-1.5 h-4 w-4" />
@ -3198,7 +3198,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('angle')}
>
<Box className="mr-1.5 h-4 w-4" />
@ -3208,7 +3208,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
disabled={isProcessing}
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 rounded-xl px-3 text-[13px] font-medium text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => handleAction('layer-split')}
>
<Layers className="mr-1.5 h-4 w-4" />
@ -3223,7 +3223,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-10 w-10 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 w-10 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
@ -3260,7 +3260,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
variant="ghost"
size="icon"
disabled={isProcessing}
className="h-10 w-10 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 w-10 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={handleDownload}
>
<Download className="h-4 w-4" />
@ -3273,7 +3273,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
{isSelected && !isMultiSelected && data.src && boxCutoutMode && typeof document !== "undefined" && createPortal(
<div
ref={boxCutoutToolbarRef}
className="fixed z-[9999] flex items-center rounded-[24px] border border-slate-200 bg-white/95 px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] backdrop-blur-md dark:border-slate-700 dark:bg-slate-900/95 dark:text-gray-100"
className="fixed z-[9999] flex items-center rounded-[24px] border border-slate-200 bg-white px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-slate-700 dark:bg-slate-900 dark:text-gray-100"
style={{
top: boxCutoutToolbarPosition.top,
left: boxCutoutToolbarPosition.left,
@ -3288,7 +3288,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
<div className="mr-1 inline-flex h-10 shrink-0 items-center gap-1.5 rounded-xl bg-slate-100 px-3 text-[13px] font-medium text-slate-700 dark:bg-white/10 dark:text-slate-100">
<div className="mr-1 inline-flex h-10 shrink-0 items-center gap-1.5 rounded-xl bg-slate-100 px-3 text-[13px] font-medium text-slate-700 dark:bg-zinc-800 dark:text-slate-100">
<Scan className="h-4 w-4 text-[#27a8ff]" />
<span className="whitespace-nowrap">{boxCutoutHintText}</span>
</div>
@ -3312,7 +3312,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
<Button
variant="ghost"
size="icon"
className="h-10 w-10 shrink-0 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-white/10"
className="h-10 w-10 shrink-0 rounded-xl text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
onClick={() => resetBoxCutoutState()}
>
<X className="h-4 w-4" />
@ -3359,7 +3359,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
{/* 文字编辑面板(显示在图片节点旁边) */}
{isSelected && textEditPanelOpen && data.src && typeof document !== "undefined" && createPortal(
<div
className="fixed z-[9999] w-[360px] rounded-xl border border-border bg-background/90 backdrop-blur shadow-2xl"
className="fixed z-[9999] w-[360px] rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950"
style={{
top: getTextPanelPosition().top,
left: getTextPanelPosition().left,
@ -3402,7 +3402,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
)}
{!isDetectingText && textEditItems.length === 0 && !textEditError && (
<div className="rounded-md bg-muted/50 text-muted-foreground text-xs px-3 py-3">
<div className="rounded-md bg-slate-50 px-3 py-3 text-xs text-muted-foreground dark:bg-zinc-900">
未识别到可编辑文字,可点击“重新识别”再试一次。
</div>
)}
@ -3436,7 +3436,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
))}
{deletedTextItems.length > 0 && (
<div className="rounded-md border border-dashed border-orange-300 bg-orange-50/60 px-3 py-2 text-xs text-orange-700">
<div className="rounded-md border border-dashed border-orange-300 bg-orange-50 px-3 py-2 text-xs text-orange-700 dark:border-orange-900 dark:bg-orange-950/40 dark:text-orange-200">
已标记删除 {deletedTextItems.length} 条文字,应用时会明确告诉生图模型移除这些文字并修复背景。
</div>
)}
@ -3501,7 +3501,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
>
{/* 关闭按钮 */}
<button
className="absolute top-4 right-4 p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
className="absolute top-4 right-4 rounded-full bg-slate-950 p-2 transition-colors hover:bg-black"
onClick={() => setFullscreenPreviewOpen(false)}
>
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@ -138,8 +138,8 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
return (
<div
className={cn(
"relative w-full h-full bg-card rounded-lg overflow-hidden border shadow-sm",
isSelected && "border-primary"
"relative h-full w-full overflow-hidden rounded-lg border bg-white shadow-sm dark:bg-zinc-950",
isSelected ? "border-primary" : "border-slate-200 dark:border-zinc-800"
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@ -150,7 +150,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
<video
ref={videoRef}
src={data.src}
className="w-full h-full object-cover"
className="h-full w-full bg-slate-950 object-cover"
muted={isMuted}
loop
onTimeUpdate={handleTimeUpdate}
@ -159,12 +159,12 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
{/* 播放控制层 */}
{isHovered && (
<div className="absolute inset-0 bg-black/30 flex flex-col justify-end">
<div className="absolute inset-0 flex flex-col justify-end">
{/* 中央播放按钮 */}
<Button
variant="ghost"
size="icon"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-14 w-14 rounded-full bg-black/50 hover:bg-black/70 text-white"
className="absolute left-1/2 top-1/2 h-14 w-14 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/20 bg-slate-950 text-white hover:bg-black"
onClick={togglePlay}
>
{isPlaying ? (
@ -175,7 +175,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
</Button>
{/* 底部控制栏 */}
<div className="p-3 space-y-2">
<div className="m-3 space-y-2 rounded-2xl border border-slate-800 bg-slate-950 p-3">
{/* 进度条 */}
<Slider
value={[progress]}
@ -191,7 +191,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-white hover:bg-white/20"
className="h-7 w-7 text-white hover:bg-slate-800"
onClick={togglePlay}
>
{isPlaying ? (
@ -203,7 +203,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-white hover:bg-white/20"
className="h-7 w-7 text-white hover:bg-slate-800"
onClick={toggleMute}
>
{isMuted ? (
@ -219,7 +219,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
)}
</div>
) : data.status === 'generating' ? (
<div className="w-full h-full flex flex-col items-center justify-center bg-muted select-none">
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50 select-none dark:bg-zinc-900">
<Loader2 className="h-8 w-8 animate-spin text-primary mb-2" />
<span className="text-sm text-muted-foreground select-none">生成中...</span>
{data.progress && (
@ -227,12 +227,12 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
)}
</div>
) : data.status === 'failed' ? (
<div className="w-full h-full flex flex-col items-center justify-center bg-destructive/10 select-none">
<div className="flex h-full w-full flex-col items-center justify-center bg-red-50 select-none dark:bg-red-950/40">
<AlertCircle className="h-8 w-8 text-destructive mb-2" />
<span className="text-sm text-destructive select-none">生成失败</span>
</div>
) : (
<label className="w-full h-full flex flex-col items-center justify-center bg-muted cursor-pointer hover:bg-muted/80 transition-colors select-none">
<label className="flex h-full w-full cursor-pointer flex-col items-center justify-center bg-slate-50 transition-colors select-none hover:bg-slate-100 dark:bg-zinc-900 dark:hover:bg-zinc-800">
<Video className="h-8 w-8 text-muted-foreground mb-2" />
<span className="text-sm text-muted-foreground select-none">点击上传视频</span>
<input
@ -275,7 +275,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
<button
type="button"
onClick={() => setSnapshotDialogOpen(true)}
className="absolute top-2 left-2 inline-flex items-center gap-1 rounded-full bg-black/70 px-2 py-1 text-[11px] text-white shadow-sm transition-colors hover:bg-black/80"
className="absolute top-2 left-2 inline-flex items-center gap-1 rounded-full bg-slate-950 px-2 py-1 text-[11px] text-white shadow-sm transition-colors hover:bg-black"
title={`执行快照:${agentExecutionSnapshotId}`}
>
快照 {shortAgentExecutionSnapshotId}
@ -289,7 +289,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
{/* 提示词显示 */}
{data.prompt && isHovered && (
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-3">
<div className="absolute bottom-0 left-0 right-0 border-t border-slate-800 bg-slate-950 p-3">
<p className="text-xs text-white line-clamp-2">{data.prompt}</p>
</div>
)}

View File

@ -143,43 +143,44 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
const batchFailedCount = Number(metadata.batchFailedCount || 0);
const batchMode =
String(metadata.batchMode || "").trim() === "gen-video" ? "视频批量" : "图片批量";
const accentGlow = toAlphaColor(accentColor, 0.18) || "rgba(59, 130, 246, 0.12)";
const accentLine = toAlphaColor(accentColor, 0.3) || "rgba(59, 130, 246, 0.2)";
const sectionClassName =
"rounded-2xl border border-border/60 bg-background p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.3)]";
"rounded-2xl border border-slate-200 bg-white p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.78)] dark:border-zinc-800 dark:bg-zinc-950";
const mutedKickerClassName =
"text-[11px] uppercase tracking-[0.18em] text-muted-foreground/80";
const itemClassName = "rounded-xl border border-border/50 bg-background px-3 py-2";
const badgeClassName = "border-border/60 bg-background text-foreground/80";
const itemClassName =
"rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900";
const badgeClassName =
"border-slate-200 bg-white text-foreground/80 dark:border-zinc-800 dark:bg-zinc-950";
const accentBarColor = toAlphaColor(accentColor, 0.9) || accentColor;
return (
<div
className={cn(
"relative h-full w-full overflow-hidden rounded-[28px] border border-border/60 bg-background shadow-[0_22px_55px_-30px_rgba(15,23,42,0.38)] transition-all",
"relative h-full w-full overflow-hidden rounded-[28px] border border-slate-200 bg-white shadow-[0_22px_55px_-30px_rgba(15,23,42,0.18)] transition-all dark:border-zinc-800 dark:bg-zinc-950",
isSelected &&
"border-primary/55 ring-2 ring-primary/15 shadow-[0_26px_60px_-28px_rgba(59,130,246,0.28)]"
"border-primary/55 ring-2 ring-primary/15 shadow-[0_24px_56px_-30px_rgba(59,130,246,0.24)]"
)}
>
<div
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: `radial-gradient(circle at top left, ${accentGlow} 0%, transparent 42%), linear-gradient(180deg, ${accentLine} 0%, transparent 18%)`,
}}
className="pointer-events-none absolute inset-x-0 top-0 h-1"
style={{ backgroundColor: accentBarColor }}
/>
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-border/70" />
<div className="pointer-events-none absolute inset-x-0 top-1 h-px bg-slate-200 dark:bg-zinc-800" />
<div className="relative flex h-full flex-col gap-4 p-4">
<div className="flex items-start justify-between gap-3">
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-border/60 bg-background shadow-sm">
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-950">
<GroupIcon className="h-4 w-4" />
</span>
<span>{groupMeta.label}</span>
</div>
<div>
<h3 className="text-base font-semibold leading-tight text-foreground">{title}</h3>
<p className="mt-1 text-sm leading-6 text-muted-foreground">{description}</p>
<p className="mt-1 line-clamp-2 text-sm leading-6 text-muted-foreground">
{description}
</p>
</div>
</div>
<Badge variant="outline" className={badgeClassName}>
@ -190,8 +191,8 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
<div className={sectionClassName}>
<div className={mutedKickerClassName}>节点标识</div>
<div className="mt-1 font-mono text-xs text-foreground/80">{data.templateType}</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{data.summary || "把这个节点接在上游素材之后,用来承接处理或输出。"}
<p className="mt-2 line-clamp-3 text-sm leading-6 text-muted-foreground">
{data.summary || "接在上游素材之后,用来处理或输出结果。"}
</p>
</div>
@ -227,7 +228,7 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
) : null}
{previewUrl ? (
<div className="overflow-hidden rounded-2xl border border-border/60 bg-slate-950 shadow-[0_16px_40px_-28px_rgba(15,23,42,0.65)]">
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-slate-950 shadow-[0_16px_40px_-28px_rgba(15,23,42,0.65)] dark:border-zinc-800">
{previewKind === "video" ? (
<video
src={previewUrl}
@ -261,9 +262,9 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
return (
<div
key={`${data.templateType}-compare-${index}`}
className="overflow-hidden rounded-2xl border border-border/60 bg-background"
className="overflow-hidden rounded-2xl border border-slate-200 bg-white dark:border-zinc-800 dark:bg-zinc-950"
>
<div className="border-b border-border/60 px-3 py-2">
<div className="border-b border-slate-200 px-3 py-2 dark:border-zinc-800">
<div className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
{String(compareItem.label || `对比 ${index + 1}`)}
</div>
@ -304,7 +305,7 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
<div className="text-xs font-semibold text-foreground/90">
{String(shot.title || `镜头 ${index + 1}`)}
</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">
<div className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
{String(shot.description || "")}
</div>
</div>
@ -352,7 +353,7 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
<div className="text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
{label}
</div>
<div className="mt-1 text-xs leading-5 text-foreground/85">
<div className="mt-1 line-clamp-3 text-xs leading-5 text-foreground/85">
{value}
</div>
</div>
@ -372,7 +373,7 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
{analysisHighlights.map((item, index) => (
<div
key={`${data.templateType}-analysis-${index}`}
className={cn(itemClassName, "text-xs leading-5 text-muted-foreground")}
className={cn(itemClassName, "line-clamp-3 text-xs leading-5 text-muted-foreground")}
>
{String(item || "")}
</div>
@ -392,7 +393,7 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
<div className="text-xs font-semibold text-foreground/90">
{String(character.name || character.title || `角色 ${index + 1}`)}
</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">
<div className="mt-1 line-clamp-3 text-xs leading-5 text-muted-foreground">
{String(character.description || character.prompt || "")}
</div>
</div>
@ -413,7 +414,7 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
<div className="text-xs font-semibold text-foreground/90">
{String(scene.name || scene.title || `场景 ${index + 1}`)}
</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">
<div className="mt-1 line-clamp-3 text-xs leading-5 text-muted-foreground">
{String(scene.description || scene.prompt || "")}
</div>
</div>