feat: show workflow material picker in mode stage
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 20:52:44 +08:00
parent 590edd49f0
commit b977242dc7
5 changed files with 205 additions and 57 deletions

View File

@ -62,6 +62,7 @@ import {
AgentWorkflowPanel,
type AgentWorkflowInputs,
} from "@/components/agents/AgentWorkflowPanel";
import { AgentWorkflowMaterialSelectionPanel } from "@/components/agents/AgentWorkflowMaterialSelectionPanel";
import { AgentPresetPanel } from "@/components/agents/AgentPresetPanel";
import { CanvasWorkspace } from "@/components/canvas";
import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel";
@ -966,7 +967,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
生图模式
</div>
<div className="mt-1 text-xs text-muted-foreground">
先选择生图模式,右侧再搜索 SKU / SPU 选图,并自动进入对应图片槽位。
先在本页选择生图模式,再使用上方 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 步
@ -988,17 +989,30 @@ export default function CanvasEditorPage({ params }: PageProps) {
</div>
) : null}
</div>
<div className="min-h-0 flex-1">
<AgentPresetPanel
embedded
selectedPresetId={selectedAgentPresetId}
onSelectPreset={setSelectedAgentPresetId}
title="生图模式"
description="先选择生图模式,右侧会出现对应槽位和 SKU / SPU 搜图入口"
searchPlaceholder="搜索生图模式"
publicTitle="公共生图模式"
privateTitle="我的生图模式"
/>
<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>
@ -1014,10 +1028,6 @@ export default function CanvasEditorPage({ params }: PageProps) {
selectedAgentPresetId={isAgentFeatureEnabled ? selectedAgentPresetId : null}
chatScope="agent"
workflowContext={workflowContext}
workflowInputs={workflowInputs}
onWorkflowInputsChange={setWorkflowInputs}
workflowMaterials={workflowMaterials}
onWorkflowMaterialsChange={setWorkflowMaterials}
/>
</div>
</div>

View File

@ -25,6 +25,7 @@ interface AgentPresetPanelProps {
selectedPresetId?: string | null;
onSelectPreset: (presetId: string | null) => void;
embedded?: boolean;
layout?: "fill" | "inline";
title?: string;
description?: string;
searchPlaceholder?: string;
@ -36,6 +37,7 @@ export function AgentPresetPanel({
selectedPresetId,
onSelectPreset,
embedded = false,
layout = "fill",
title = "智能体",
description = "选择智能体后,右侧对话框会按其要求显示上传槽位",
searchPlaceholder = "搜索智能体",
@ -164,7 +166,8 @@ export function AgentPresetPanel({
<>
<div
className={cn(
"flex h-full flex-col bg-white dark:bg-zinc-950",
"flex flex-col bg-white dark:bg-zinc-950",
layout === "fill" ? "h-full" : "h-auto",
embedded ? "" : "border-r"
)}
>
@ -191,7 +194,148 @@ export function AgentPresetPanel({
/>
</div>
<ScrollArea className="flex-1">
{layout === "fill" ? (
<ScrollArea className="flex-1">
<div className="space-y-5 p-4">
<section className="space-y-3">
<div className="space-y-3">
<div className="text-sm font-medium">{publicTitle}</div>
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Select
value={categoryFilters.productCategory || "__none__"}
onValueChange={(value) =>
setCategoryFilters((prev) => ({
...prev,
productCategory: value === "__none__" ? "" : value,
}))
}
>
<SelectTrigger>
<SelectValue placeholder="产品品类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">全部产品品类</SelectItem>
{categoryTreeConfig.productCategories.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={categoryFilters.audienceTag || "__none__"}
onValueChange={(value) =>
setCategoryFilters((prev) => ({
...prev,
audienceTag: value === "__none__" ? "" : value,
}))
}
>
<SelectTrigger>
<SelectValue placeholder="人群标签" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">全部人群标签</SelectItem>
{categoryTreeConfig.audienceTags.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={categoryFilters.usageScenario || "__none__"}
onValueChange={(value) =>
setCategoryFilters((prev) => ({
...prev,
usageScenario: value === "__none__" ? "" : value,
}))
}
>
<SelectTrigger>
<SelectValue placeholder="使用场景" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">全部使用场景</SelectItem>
{categoryTreeConfig.usageScenarios.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={categoryFilters.brand || "__none__"}
onValueChange={(value) =>
setCategoryFilters((prev) => ({
...prev,
brand: value === "__none__" ? "" : value,
}))
}
>
<SelectTrigger>
<SelectValue placeholder="品牌" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">全部品牌</SelectItem>
{categoryTreeConfig.brands.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : filteredPublicPresets.length > 0 ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
{filteredPublicPresets.map((preset) => (
<AgentPresetCard
key={preset.id}
preset={preset}
selected={selectedPresetId === preset.id}
onSelect={onSelectPreset}
onFork={handleForkPreset}
onEdit={setEditingPreset}
onDelete={handleDeletePreset}
/>
))}
</div>
) : (
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
暂无公共智能体
</div>
)}
</section>
<section className="space-y-3">
<div className="text-sm font-medium">{privateTitle}</div>
{isLoading ? null : filteredPrivatePresets.length > 0 ? (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
{filteredPrivatePresets.map((preset) => (
<AgentPresetCard
key={preset.id}
preset={preset}
selected={selectedPresetId === preset.id}
onSelect={onSelectPreset}
onEdit={setEditingPreset}
onDelete={handleDeletePreset}
/>
))}
</div>
) : (
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
还没有私有智能体,先新建一个吧
</div>
)}
</section>
</div>
</ScrollArea>
) : (
<div className="space-y-5 p-4">
<section className="space-y-3">
<div className="space-y-3">
@ -330,7 +474,7 @@ export function AgentPresetPanel({
)}
</section>
</div>
</ScrollArea>
)}
</div>
<AgentPresetDialog

View File

@ -14,6 +14,8 @@ interface AgentWorkflowMaterialPickerProps {
placeholder: string;
buttonLabel: string;
searchNote?: string;
disabled?: boolean;
disabledHint?: string;
isLoading?: boolean;
errorText?: string | null;
result?: AgentWorkflowMaterialSearchResult | null;
@ -30,6 +32,8 @@ export function AgentWorkflowMaterialPicker({
placeholder,
buttonLabel,
searchNote,
disabled = false,
disabledHint,
isLoading = false,
errorText,
result,
@ -72,14 +76,20 @@ export function AgentWorkflowMaterialPicker({
value={value}
onChange={(event) => onValueChange(event.target.value)}
placeholder={placeholder}
disabled={disabled}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
if (disabled) return;
onSearch();
}
}}
/>
<Button type="button" onClick={onSearch} disabled={isLoading || !value.trim()}>
<Button
type="button"
onClick={onSearch}
disabled={disabled || isLoading || !value.trim()}
>
{isLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
@ -92,14 +102,19 @@ export function AgentWorkflowMaterialPicker({
{searchNote ? (
<div className="text-xs text-muted-foreground">{searchNote}</div>
) : null}
{disabled && disabledHint ? (
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
{disabledHint}
</div>
) : null}
{errorText ? (
{!disabled && errorText ? (
<div className="rounded-xl border border-destructive/20 bg-destructive/5 px-3 py-2 text-xs text-destructive">
{errorText}
</div>
) : null}
{result ? (
{!disabled && result ? (
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
@ -198,7 +213,12 @@ export function AgentWorkflowMaterialPicker({
</div>
) : (
<div className="flex min-h-[124px] flex-col items-center justify-center rounded-2xl border border-dashed bg-background/60 px-4 text-center text-sm text-muted-foreground">
{isLoading ? (
{disabled ? (
<>
<ImageIcon className="mb-2 h-5 w-5 opacity-60" />
先选择生图模式,再搜索并选择素材图。
</>
) : isLoading ? (
<>
<Loader2 className="mb-2 h-5 w-5 animate-spin" />
正在搜索并导入图片,请稍候...

View File

@ -14,6 +14,8 @@ interface AgentWorkflowMaterialSelectionPanelProps {
onMaterialsChange: (next: AgentWorkflowMaterialSelections) => void;
title?: string;
description?: string;
disabled?: boolean;
disabledHint?: string;
}
export function AgentWorkflowMaterialSelectionPanel({
@ -23,6 +25,8 @@ export function AgentWorkflowMaterialSelectionPanel({
onMaterialsChange,
title = "SKU / SPU 选图",
description = "先确定生图模式,再搜索主 SKU 或搭配 SKU 的白底图。选中的图片会自动进入对应槽位。",
disabled = false,
disabledHint = "请先在下方选择一个生图模式,再启用 SKU / SPU 搜图。",
}: AgentWorkflowMaterialSelectionPanelProps) {
const [materialSearchState, setMaterialSearchState] = useState<{
primary: { isLoading: boolean; errorText: string | null };
@ -122,6 +126,8 @@ export function AgentWorkflowMaterialSelectionPanel({
placeholder="输入主 SKU / SPU"
buttonLabel="搜索主图"
searchNote="当前上游接口实际按可命中的 SKU 查图;若提供了 SPU 接口,后续可直接无缝切换。"
disabled={disabled}
disabledHint={disabledHint}
isLoading={materialSearchState.primary.isLoading}
errorText={materialSearchState.primary.errorText}
result={materials.primary.result}
@ -148,6 +154,8 @@ export function AgentWorkflowMaterialSelectionPanel({
placeholder="输入搭配 SKU"
buttonLabel="搜索搭配图"
searchNote="选中的搭配图会进入第二个产品槽位,你也可以在下面继续手动替换。"
disabled={disabled}
disabledHint={disabledHint}
isLoading={materialSearchState.secondary.isLoading}
errorText={materialSearchState.secondary.errorText}
result={materials.secondary.result}

View File

@ -9,7 +9,6 @@ import useSWR, { useSWRConfig } from "swr";
import { Send, Loader2, ImagePlus, Paperclip, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, Box, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { AgentWorkflowMaterialSelectionPanel } from "@/components/agents/AgentWorkflowMaterialSelectionPanel";
import {
Collapsible,
CollapsibleContent,
@ -52,7 +51,6 @@ import type { Attachment, ChatMessage } from "@/lib/types";
import type {
AgentPreset,
AgentWorkflowComposerContext,
AgentWorkflowMaterialSelections,
} from "@/types/agent";
import { type CanvasLaunchPayload, getCanvasLaunchStorageKey } from "@/lib/canvas-launch";
import {
@ -60,7 +58,6 @@ import {
normalizeCanvasSkillTags,
type CanvasSkillTag,
} from "@/lib/canvas-skill-tags";
import type { AgentWorkflowInputs } from "@/components/agents/AgentWorkflowPanel";
import { getCanvasSkillIconComponent } from "@/components/canvas/canvas-skill-icons";
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
import { VideoParamsBar, type VideoGenerationParams } from "@/components/video-params-bar";
@ -117,10 +114,6 @@ interface CanvasChatPanelProps {
selectedAgentPresetId?: string | null;
chatScope?: "canvas" | "agent";
workflowContext?: AgentWorkflowComposerContext | null;
workflowInputs?: AgentWorkflowInputs | null;
onWorkflowInputsChange?: ((next: AgentWorkflowInputs) => void) | null;
workflowMaterials?: AgentWorkflowMaterialSelections | null;
onWorkflowMaterialsChange?: ((next: AgentWorkflowMaterialSelections) => void) | null;
}
type VideoStoryboardStreamData =
@ -1693,10 +1686,6 @@ export function CanvasChatPanel({
selectedAgentPresetId,
chatScope = "canvas",
workflowContext = null,
workflowInputs = null,
onWorkflowInputsChange = null,
workflowMaterials = null,
onWorkflowMaterialsChange = null,
}: CanvasChatPanelProps) {
const { data: session } = useSession();
const [input, setInput] = useState("");
@ -2827,9 +2816,6 @@ export function CanvasChatPanel({
[agentPresetResponse]
);
const workflowReferenceImages = workflowContext?.referenceImages || [];
const canManageWorkflowMaterials = Boolean(
workflowInputs && workflowMaterials && onWorkflowInputsChange && onWorkflowMaterialsChange
);
const hasWorkflowContext = Boolean(
workflowContext &&
(String(workflowContext.summaryText || "").trim() ||
@ -9468,26 +9454,6 @@ export function CanvasChatPanel({
) : null}
</div>
)}
{!isDetailGenerationMode &&
activeComposerMode === "agent" &&
selectedAgentPreset &&
hasAgentUploadSlots &&
canManageWorkflowMaterials &&
workflowInputs &&
workflowMaterials &&
onWorkflowInputsChange &&
onWorkflowMaterialsChange ? (
<div className="mb-2">
<AgentWorkflowMaterialSelectionPanel
inputs={workflowInputs}
onInputsChange={onWorkflowInputsChange}
materials={workflowMaterials}
onMaterialsChange={onWorkflowMaterialsChange}
title="选择商品素材图"
description="已选生图模式后,再搜索主 SKU / 搭配 SKU。选中的图片会自动进入产品图槽位,模特图继续手动上传。"
/>
</div>
) : null}
<div
ref={textareaRef as any}
contentEditable={!isSubmitting}