feat: make mode stage a full workspace
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-03-17 21:02:59 +08:00
parent b977242dc7
commit 7ce4d4cdc2
2 changed files with 207 additions and 169 deletions

View File

@ -957,78 +957,99 @@ export default function CanvasEditorPage({ params }: PageProps) {
</div>
) : (
<div className="absolute inset-0 flex pt-[72px]">
<div className="min-w-0 flex-1 border-r bg-white dark:bg-zinc-950">
<div className="flex h-full flex-col">
<div className="border-b p-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2 text-sm font-semibold">
<Wand2 className="h-4 w-4 text-amber-500" />
生图模式
</div>
<div className="mt-1 text-xs text-muted-foreground">
先在本页选择生图模式,再使用上方 SKU / SPU 取图,右侧会自动进入对应图片槽位。
</div>
<div className="mt-2 inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-[11px] text-muted-foreground">
第 2 步 / 2 步
<div className="flex h-full w-full bg-[#f5f7fb] dark:bg-zinc-950">
<div className="w-[520px] shrink-0 border-r bg-white dark:bg-zinc-950">
<div className="flex h-full flex-col">
<div className="border-b p-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2 text-sm font-semibold">
<Wand2 className="h-4 w-4 text-amber-500" />
生图模式
</div>
<div className="mt-1 text-xs text-muted-foreground">
先在本页选择生图模式,再使用上方 SKU / SPU 取图,右侧全屏工作区会自动进入对应图片槽位并展示结果。
</div>
<div className="mt-2 inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-[11px] text-muted-foreground">
第 2 步 / 2 步
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setCurrentAgentStage("workflow")}
>
<ArrowLeft className="mr-1 h-4 w-4" />
返回上一步
</Button>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setCurrentAgentStage("workflow")}
>
<ArrowLeft className="mr-1 h-4 w-4" />
返回上一步
</Button>
{!workflowProgress.isComplete ? (
<div className="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
第一步尚未完成,你也可以在当前直接继续生图模式选择。
</div>
) : null}
</div>
{!workflowProgress.isComplete ? (
<div className="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
第一步尚未完成,你也可以在当前直接继续生图模式选择。
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="space-y-4 p-4">
<AgentWorkflowMaterialSelectionPanel
inputs={workflowInputs}
onInputsChange={setWorkflowInputs}
materials={workflowMaterials}
onMaterialsChange={setWorkflowMaterials}
title="SKU / SPU 获取图片"
description="第二步直接在这里搜索主 SKU / SPU 与搭配 SKU 图片,选中的图片会自动进入右侧对应产品槽位。"
disabled={!selectedAgentPresetId}
disabledHint="请先选择一个生图模式,系统才能把你选中的图片自动送入对应生图槽位。"
/>
<AgentPresetPanel
embedded
layout="inline"
selectedPresetId={selectedAgentPresetId}
onSelectPreset={setSelectedAgentPresetId}
title="生图模式"
description="先选择生图模式,再在上方搜索 SKU / SPU 图片并自动进入对应槽位"
searchPlaceholder="搜索生图模式"
publicTitle="公共生图模式"
privateTitle="我的生图模式"
/>
</div>
) : null}
</div>
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="space-y-4 p-4">
<AgentWorkflowMaterialSelectionPanel
inputs={workflowInputs}
onInputsChange={setWorkflowInputs}
materials={workflowMaterials}
onMaterialsChange={setWorkflowMaterials}
title="SKU / SPU 获取图片"
description="第二步直接在这里搜索主 SKU / SPU 与搭配 SKU 图片,选中的图片会自动进入右侧对应产品槽位。"
disabled={!selectedAgentPresetId}
disabledHint="请先选择一个生图模式,系统才能把你选中的图片自动送入对应生图槽位。"
/>
<AgentPresetPanel
embedded
layout="inline"
selectedPresetId={selectedAgentPresetId}
onSelectPreset={setSelectedAgentPresetId}
title="生图模式"
description="先选择生图模式,再在上方搜索 SKU / SPU 图片并自动进入对应槽位"
searchPlaceholder="搜索生图模式"
publicTitle="公共生图模式"
privateTitle="我的生图模式"
/>
</div>
</div>
</div>
</div>
<div
className="shrink-0 border-l bg-white dark:bg-zinc-950"
style={{ width: FLOATING_PANEL_WIDTH }}
>
<div className="h-full overflow-hidden">
<CanvasChatPanel
chatId={`canvas-${id}`}
onClose={() => {}}
selectedAgentPresetId={isAgentFeatureEnabled ? selectedAgentPresetId : null}
chatScope="agent"
workflowContext={workflowContext}
/>
<div className="min-w-0 flex-1">
<div className="flex h-full flex-col">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-semibold">执行工作区</div>
<div className="mt-1 text-xs text-muted-foreground">
这里不再是侧边对话框,而是全屏生图工作台。生成结果、槽位操作和补充指令都在这里完成。
</div>
</div>
<div className="flex items-center gap-2">
{selectedAgentPresetId ? (
<div className="inline-flex items-center rounded-full bg-white px-3 py-1 text-xs font-medium text-foreground shadow-sm dark:bg-zinc-900">
当前模式已选
</div>
) : (
<div className="inline-flex items-center rounded-full bg-white px-3 py-1 text-xs text-muted-foreground shadow-sm dark:bg-zinc-900">
请选择左侧生图模式
</div>
)}
</div>
</div>
<div className="min-h-0 flex-1 overflow-hidden rounded-tl-[24px] border-t border-l bg-white shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] dark:border-white/10 dark:bg-zinc-950">
<CanvasChatPanel
chatId={`canvas-${id}`}
onClose={undefined}
selectedAgentPresetId={isAgentFeatureEnabled ? selectedAgentPresetId : null}
chatScope="agent"
workflowContext={workflowContext}
presentation="workspace"
/>
</div>
</div>
</div>
</div>
</div>

View File

@ -114,6 +114,7 @@ interface CanvasChatPanelProps {
selectedAgentPresetId?: string | null;
chatScope?: "canvas" | "agent";
workflowContext?: AgentWorkflowComposerContext | null;
presentation?: "panel" | "workspace";
}
type VideoStoryboardStreamData =
@ -1686,6 +1687,7 @@ export function CanvasChatPanel({
selectedAgentPresetId,
chatScope = "canvas",
workflowContext = null,
presentation = "panel",
}: CanvasChatPanelProps) {
const { data: session } = useSession();
const [input, setInput] = useState("");
@ -7882,9 +7884,15 @@ export function CanvasChatPanel({
const fallbackTitle = String(projectTitle || "").trim();
return fallbackTitle || "未命名画布";
}, [activeSessionTitle, projectTitle]);
const isWorkspacePresentation = presentation === "workspace";
return (
<div className="relative flex h-full min-h-0 flex-col bg-white dark:bg-zinc-950">
<div
className={cn(
"relative flex h-full min-h-0 flex-col",
isWorkspacePresentation ? "bg-transparent" : "bg-white dark:bg-zinc-950"
)}
>
{/* 全局遮罩提示 - 当处于画布选图模式时显示 */}
{detailSlotPickerIndex !== null && (
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white dark:bg-zinc-950 animate-in fade-in duration-200">
@ -7912,111 +7920,112 @@ export function CanvasChatPanel({
</div>
</div>
)}
{/* Header - Minimal with Icons */}
<div className="z-20 flex items-center justify-between gap-3 p-4 pb-2">
<div className="min-w-0 flex flex-1 items-center gap-2 pr-2">
<span className="inline-flex h-5 w-5 items-center justify-center rounded-md bg-[#eef2f7] text-[#6b7280]">
<MessageCircle className="h-3.5 w-3.5" />
</span>
<h2
className="min-w-0 flex-1 truncate text-[14px] font-semibold leading-tight tracking-tight text-[#111827]"
title={displayCanvasName}
>
{displayCanvasName}
</h2>
</div>
<div className="flex shrink-0 items-center">
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full hover:bg-muted"
onClick={createNewChatSession}
title="新建聊天窗口"
>
<Plus className="h-4 w-4" />
</Button>
<div className="relative" ref={sessionMenuRef}>
<Button
variant="ghost"
size="icon"
className={cn(
"h-8 w-8 rounded-full hover:bg-muted",
isSessionMenuOpen && "bg-muted"
)}
onClick={() => setIsSessionMenuOpen((open) => !open)}
title="聊天窗口记录"
{!isWorkspacePresentation ? (
<div className="z-20 flex items-center justify-between gap-3 p-4 pb-2">
<div className="min-w-0 flex flex-1 items-center gap-2 pr-2">
<span className="inline-flex h-5 w-5 items-center justify-center rounded-md bg-[#eef2f7] text-[#6b7280]">
<MessageCircle className="h-3.5 w-3.5" />
</span>
<h2
className="min-w-0 flex-1 truncate text-[14px] font-semibold leading-tight tracking-tight text-[#111827]"
title={displayCanvasName}
>
<History className="h-4 w-4" />
</Button>
{isSessionMenuOpen && (
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-white dark:bg-zinc-950 shadow-xl p-2">
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
<div className="max-h-56 overflow-y-auto space-y-1">
{chatSessions.length === 0 ? (
<div className="px-2 py-2 text-xs text-muted-foreground">暂无记录</div>
) : (
chatSessions.map((session, index) => {
const isActive = session.id === activeChatId;
const timeLabel = new Date(session.updatedAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
return (
<button
key={session.id}
type="button"
className={cn(
"w-full text-left rounded-lg px-2.5 py-2 transition-colors",
isActive
? "bg-primary/10 text-primary"
: "hover:bg-muted"
)}
onClick={() => switchToSession(session.id)}
>
<div className="text-sm font-medium truncate">
{session.title || `对话 ${index + 1}`}
</div>
<div className="text-[11px] text-muted-foreground mt-0.5">
最近更新 {timeLabel}
</div>
</button>
);
})
)}
</div>
</div>
)}
{displayCanvasName}
</h2>
</div>
<div className="flex shrink-0 items-center">
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full hover:bg-muted"
onClick={createNewChatSession}
title="新建聊天窗口"
>
<Plus className="h-4 w-4" />
</Button>
<div className="relative" ref={sessionMenuRef}>
<Button
variant="ghost"
size="icon"
className={cn(
"h-8 w-8 rounded-full hover:bg-muted",
isSessionMenuOpen && "bg-muted"
)}
onClick={() => setIsSessionMenuOpen((open) => !open)}
title="聊天窗口记录"
>
<History className="h-4 w-4" />
</Button>
{isSessionMenuOpen && (
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-white dark:bg-zinc-950 shadow-xl p-2">
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
<div className="max-h-56 overflow-y-auto space-y-1">
{chatSessions.length === 0 ? (
<div className="px-2 py-2 text-xs text-muted-foreground">暂无记录</div>
) : (
chatSessions.map((session, index) => {
const isActive = session.id === activeChatId;
const timeLabel = new Date(session.updatedAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
return (
<button
key={session.id}
type="button"
className={cn(
"w-full text-left rounded-lg px-2.5 py-2 transition-colors",
isActive
? "bg-primary/10 text-primary"
: "hover:bg-muted"
)}
onClick={() => switchToSession(session.id)}
>
<div className="text-sm font-medium truncate">
{session.title || `对话 ${index + 1}`}
</div>
<div className="text-[11px] text-muted-foreground mt-0.5">
最近更新 {timeLabel}
</div>
</button>
);
})
)}
</div>
</div>
)}
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full hover:bg-muted"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log("[CanvasChatPanel] File list button clicked");
if (onOpenFileList) {
onOpenFileList();
}
}}
title="已生成文件列表"
>
<LayoutGrid className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full hover:bg-muted"
onClick={onClose}
disabled={!onClose}
title="收起聊天面板"
>
<ArrowRightToLine className="h-4 w-4" />
</Button>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full hover:bg-muted"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('[CanvasChatPanel] File list button clicked');
if (onOpenFileList) {
onOpenFileList();
}
}}
title="已生成文件列表"
>
<LayoutGrid className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full hover:bg-muted"
onClick={onClose}
disabled={!onClose}
title="收起聊天面板"
>
<ArrowRightToLine className="h-4 w-4" />
</Button>
</div>
</div>
</div>
) : null}
{/* 消息列表 */}
{isDetailEmptyState ? (
@ -8025,7 +8034,8 @@ export function CanvasChatPanel({
<div
key={`${activeChatId}-${messageViewportKey}`}
className={cn(
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-4",
"flex-1 min-h-0 overflow-y-auto overflow-x-hidden",
isWorkspacePresentation ? "p-6" : "p-4",
isDetailGenerationMode && renderableVisibleMessages.length === 0
? "pt-10"
: "pt-4"
@ -8659,7 +8669,14 @@ export function CanvasChatPanel({
) : null}
{/* 底部输入区域 - 混合输入(图片标签+文字可自由混合) */}
<div className="p-4 bg-white dark:bg-zinc-950 border-t">
<div
className={cn(
"border-t",
isWorkspacePresentation
? "bg-white/92 p-5 backdrop-blur-sm dark:bg-zinc-950/92"
: "bg-white p-4 dark:bg-zinc-950"
)}
>
<div
className="relative flex flex-col bg-card rounded-3xl border transition-all duration-200 focus-within:border-primary/50 focus-within:bg-background"
data-canvas-chat-input-anchor="true"