feat: show workflow material picker in mode stage
This commit is contained in:
parent
590edd49f0
commit
b977242dc7
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" />
|
||||
正在搜索并导入图片,请稍候...
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user