feat: make mode stage a full workspace
This commit is contained in:
parent
b977242dc7
commit
7ce4d4cdc2
@ -957,78 +957,99 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 flex pt-[72px]">
|
<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 w-full bg-[#f5f7fb] dark:bg-zinc-950">
|
||||||
<div className="flex h-full flex-col">
|
<div className="w-[520px] shrink-0 border-r bg-white dark:bg-zinc-950">
|
||||||
<div className="border-b p-4">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="border-b p-4">
|
||||||
<div>
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
<div>
|
||||||
<Wand2 className="h-4 w-4 text-amber-500" />
|
<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">
|
</div>
|
||||||
先在本页选择生图模式,再使用上方 SKU / SPU 取图,右侧会自动进入对应图片槽位。
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
</div>
|
先在本页选择生图模式,再使用上方 SKU / SPU 取图,右侧全屏工作区会自动进入对应图片槽位并展示结果。
|
||||||
<div className="mt-2 inline-flex items-center rounded-full bg-muted px-2.5 py-1 text-[11px] text-muted-foreground">
|
</div>
|
||||||
第 2 步 / 2 步
|
<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>
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentAgentStage("workflow")}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-1 h-4 w-4" />
|
||||||
|
返回上一步
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{!workflowProgress.isComplete ? (
|
||||||
type="button"
|
<div className="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
||||||
variant="outline"
|
第一步尚未完成,你也可以在当前直接继续生图模式选择。
|
||||||
size="sm"
|
</div>
|
||||||
onClick={() => setCurrentAgentStage("workflow")}
|
) : null}
|
||||||
>
|
|
||||||
<ArrowLeft className="mr-1 h-4 w-4" />
|
|
||||||
返回上一步
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{!workflowProgress.isComplete ? (
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||||
<div className="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
<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>
|
||||||
) : 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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div className="min-w-0 flex-1">
|
||||||
className="shrink-0 border-l bg-white dark:bg-zinc-950"
|
<div className="flex h-full flex-col">
|
||||||
style={{ width: FLOATING_PANEL_WIDTH }}
|
<div className="flex items-start justify-between gap-3">
|
||||||
>
|
<div>
|
||||||
<div className="h-full overflow-hidden">
|
<div className="text-sm font-semibold">执行工作区</div>
|
||||||
<CanvasChatPanel
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
chatId={`canvas-${id}`}
|
这里不再是侧边对话框,而是全屏生图工作台。生成结果、槽位操作和补充指令都在这里完成。
|
||||||
onClose={() => {}}
|
</div>
|
||||||
selectedAgentPresetId={isAgentFeatureEnabled ? selectedAgentPresetId : null}
|
</div>
|
||||||
chatScope="agent"
|
<div className="flex items-center gap-2">
|
||||||
workflowContext={workflowContext}
|
{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -114,6 +114,7 @@ interface CanvasChatPanelProps {
|
|||||||
selectedAgentPresetId?: string | null;
|
selectedAgentPresetId?: string | null;
|
||||||
chatScope?: "canvas" | "agent";
|
chatScope?: "canvas" | "agent";
|
||||||
workflowContext?: AgentWorkflowComposerContext | null;
|
workflowContext?: AgentWorkflowComposerContext | null;
|
||||||
|
presentation?: "panel" | "workspace";
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoStoryboardStreamData =
|
type VideoStoryboardStreamData =
|
||||||
@ -1686,6 +1687,7 @@ export function CanvasChatPanel({
|
|||||||
selectedAgentPresetId,
|
selectedAgentPresetId,
|
||||||
chatScope = "canvas",
|
chatScope = "canvas",
|
||||||
workflowContext = null,
|
workflowContext = null,
|
||||||
|
presentation = "panel",
|
||||||
}: CanvasChatPanelProps) {
|
}: CanvasChatPanelProps) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
@ -7882,9 +7884,15 @@ export function CanvasChatPanel({
|
|||||||
const fallbackTitle = String(projectTitle || "").trim();
|
const fallbackTitle = String(projectTitle || "").trim();
|
||||||
return fallbackTitle || "未命名画布";
|
return fallbackTitle || "未命名画布";
|
||||||
}, [activeSessionTitle, projectTitle]);
|
}, [activeSessionTitle, projectTitle]);
|
||||||
|
const isWorkspacePresentation = presentation === "workspace";
|
||||||
|
|
||||||
return (
|
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 && (
|
{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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Header - Minimal with Icons */}
|
{!isWorkspacePresentation ? (
|
||||||
<div className="z-20 flex items-center justify-between gap-3 p-4 pb-2">
|
<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">
|
<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]">
|
<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" />
|
<MessageCircle className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
<h2
|
<h2
|
||||||
className="min-w-0 flex-1 truncate text-[14px] font-semibold leading-tight tracking-tight text-[#111827]"
|
className="min-w-0 flex-1 truncate text-[14px] font-semibold leading-tight tracking-tight text-[#111827]"
|
||||||
title={displayCanvasName}
|
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="聊天窗口记录"
|
|
||||||
>
|
>
|
||||||
<History className="h-4 w-4" />
|
{displayCanvasName}
|
||||||
</Button>
|
</h2>
|
||||||
{isSessionMenuOpen && (
|
</div>
|
||||||
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-white dark:bg-zinc-950 shadow-xl p-2">
|
<div className="flex shrink-0 items-center">
|
||||||
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
|
<div className="flex items-center gap-1">
|
||||||
<div className="max-h-56 overflow-y-auto space-y-1">
|
<Button
|
||||||
{chatSessions.length === 0 ? (
|
variant="ghost"
|
||||||
<div className="px-2 py-2 text-xs text-muted-foreground">暂无记录</div>
|
size="icon"
|
||||||
) : (
|
className="h-8 w-8 rounded-full hover:bg-muted"
|
||||||
chatSessions.map((session, index) => {
|
onClick={createNewChatSession}
|
||||||
const isActive = session.id === activeChatId;
|
title="新建聊天窗口"
|
||||||
const timeLabel = new Date(session.updatedAt).toLocaleTimeString([], {
|
>
|
||||||
hour: "2-digit",
|
<Plus className="h-4 w-4" />
|
||||||
minute: "2-digit",
|
</Button>
|
||||||
});
|
<div className="relative" ref={sessionMenuRef}>
|
||||||
return (
|
<Button
|
||||||
<button
|
variant="ghost"
|
||||||
key={session.id}
|
size="icon"
|
||||||
type="button"
|
className={cn(
|
||||||
className={cn(
|
"h-8 w-8 rounded-full hover:bg-muted",
|
||||||
"w-full text-left rounded-lg px-2.5 py-2 transition-colors",
|
isSessionMenuOpen && "bg-muted"
|
||||||
isActive
|
)}
|
||||||
? "bg-primary/10 text-primary"
|
onClick={() => setIsSessionMenuOpen((open) => !open)}
|
||||||
: "hover:bg-muted"
|
title="聊天窗口记录"
|
||||||
)}
|
>
|
||||||
onClick={() => switchToSession(session.id)}
|
<History className="h-4 w-4" />
|
||||||
>
|
</Button>
|
||||||
<div className="text-sm font-medium truncate">
|
{isSessionMenuOpen && (
|
||||||
{session.title || `对话 ${index + 1}`}
|
<div className="absolute right-0 top-10 w-64 rounded-xl border bg-white dark:bg-zinc-950 shadow-xl p-2">
|
||||||
</div>
|
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
|
||||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
<div className="max-h-56 overflow-y-auto space-y-1">
|
||||||
最近更新 {timeLabel}
|
{chatSessions.length === 0 ? (
|
||||||
</div>
|
<div className="px-2 py-2 text-xs text-muted-foreground">暂无记录</div>
|
||||||
</button>
|
) : (
|
||||||
);
|
chatSessions.map((session, index) => {
|
||||||
})
|
const isActive = session.id === activeChatId;
|
||||||
)}
|
const timeLabel = new Date(session.updatedAt).toLocaleTimeString([], {
|
||||||
</div>
|
hour: "2-digit",
|
||||||
</div>
|
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>
|
</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}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 消息列表 */}
|
{/* 消息列表 */}
|
||||||
{isDetailEmptyState ? (
|
{isDetailEmptyState ? (
|
||||||
@ -8025,7 +8034,8 @@ export function CanvasChatPanel({
|
|||||||
<div
|
<div
|
||||||
key={`${activeChatId}-${messageViewportKey}`}
|
key={`${activeChatId}-${messageViewportKey}`}
|
||||||
className={cn(
|
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
|
isDetailGenerationMode && renderableVisibleMessages.length === 0
|
||||||
? "pt-10"
|
? "pt-10"
|
||||||
: "pt-4"
|
: "pt-4"
|
||||||
@ -8659,7 +8669,14 @@ export function CanvasChatPanel({
|
|||||||
) : null}
|
) : 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
|
<div
|
||||||
className="relative flex flex-col bg-card rounded-3xl border transition-all duration-200 focus-within:border-primary/50 focus-within:bg-background"
|
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"
|
data-canvas-chat-input-anchor="true"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user