feat: streamline agent detail selection workflow
This commit is contained in:
parent
dc1665d7ba
commit
467de8ff1e
@ -2247,6 +2247,198 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
</div>
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
|
|
||||||
|
{isDetailReplacementPreset ? (
|
||||||
|
<motion.section
|
||||||
|
className="relative overflow-hidden rounded-[28px] border border-white/12 bg-white/[0.04] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||||
|
initial={{ opacity: 0, scale: 0.98 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.08 }}
|
||||||
|
>
|
||||||
|
<div className="border-b border-white/12 px-5 py-4">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-white">参考详情搜索</div>
|
||||||
|
<div className="text-xs text-white/40">
|
||||||
|
按 SPU 查询候选整套,并选择一套{detailReferenceSourceLabel}参考图进入下方结果区。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/55">
|
||||||
|
当前已选 {detailReplacementReferenceCards.length} 张
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 p-5">
|
||||||
|
<div className="inline-flex rounded-full border border-emerald-400/35 bg-emerald-500/[0.10] px-3 py-1.5 text-xs text-emerald-200">
|
||||||
|
按SPU查询
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 lg:flex-row">
|
||||||
|
<Input
|
||||||
|
value={detailReplacementQueryInput}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateDetailReplacementState({
|
||||||
|
query: e.target.value,
|
||||||
|
queryType: "spu",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder={`输入 SPU,查询任务整套中的${detailReferenceSourceLabel}参考图`}
|
||||||
|
className="h-12 rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDetailReplacementSearch}
|
||||||
|
disabled={detailReplacementSearchState.isLoading}
|
||||||
|
className="h-12 rounded-[22px] bg-white/[0.04] px-5 text-sm text-white hover:bg-white/[0.08]"
|
||||||
|
>
|
||||||
|
{detailReplacementSearchState.isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
查询中
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
`查询${detailReferenceSourceLabel}`
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{detailReplacementReferenceCards.length > 0 ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClearDetailReferenceCards}
|
||||||
|
className="h-12 rounded-[22px] border-white/10 bg-white/[0.03] px-5 text-sm text-white hover:bg-white/[0.08]"
|
||||||
|
>
|
||||||
|
清空参考图
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim()) ? (
|
||||||
|
<div className="rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-white/48">
|
||||||
|
当前参考图查询:
|
||||||
|
SPU /
|
||||||
|
{detailReplacementSearchState.lastQuery ||
|
||||||
|
detailReplacementQueryInput.trim().toUpperCase()}
|
||||||
|
{workflowStoreName ? (
|
||||||
|
<span className="ml-2 text-white/40">{`店铺 / ${workflowStoreName}`}</span>
|
||||||
|
) : null}
|
||||||
|
<span className="ml-2 text-emerald-200/70">
|
||||||
|
{`已接入真实${detailReferenceSourceLabel}参考图接口`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{detailReplacementCandidates.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center justify-between gap-3">
|
||||||
|
<div className="text-xs text-white/42">
|
||||||
|
候选整套素材 {detailReplacementCandidates.length} 套
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-emerald-200/70">
|
||||||
|
默认载入最新一套,可随时切换
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="agent-dark-scrollbar grid max-h-[360px] grid-cols-1 gap-3 overflow-y-auto pr-1 xl:grid-cols-2">
|
||||||
|
{detailReplacementCandidates.map((candidate) => {
|
||||||
|
const isSelected = detailReplacementSelectedCandidateId === candidate.id;
|
||||||
|
const previewUrls = getCandidatePreviewUrls(
|
||||||
|
candidate,
|
||||||
|
detailReferenceTarget,
|
||||||
|
4
|
||||||
|
);
|
||||||
|
const primaryPreviewUrl =
|
||||||
|
previewUrls[0] || getCandidatePreviewUrl(candidate, detailReferenceTarget);
|
||||||
|
const uploadedAtText = (() => {
|
||||||
|
const raw = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null;
|
||||||
|
if (!raw || Number.isNaN(raw.getTime())) return "未记录";
|
||||||
|
return raw.toLocaleString("zh-CN", {
|
||||||
|
timeZone: "Asia/Shanghai",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
const summaryBadges = [
|
||||||
|
`${detailReferenceSourceLabel}`,
|
||||||
|
`${getCandidateTargetCount(candidate, detailReferenceTarget)}张`,
|
||||||
|
candidate.categoryName || null,
|
||||||
|
].filter((item): item is string => Boolean(item));
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={candidate.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectDetailReplacementCandidate(candidate.id)}
|
||||||
|
className={cn(
|
||||||
|
"group relative min-w-0 overflow-hidden rounded-2xl border p-2.5 text-left transition-all duration-300",
|
||||||
|
isSelected
|
||||||
|
? "border-emerald-400/40 bg-emerald-500/[0.08] shadow-[0_0_24px_rgba(16,185,129,0.14)]"
|
||||||
|
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-stretch gap-3">
|
||||||
|
<div className="relative w-[88px] min-h-[112px] shrink-0 overflow-hidden rounded-[20px] border border-white/10 bg-black/30">
|
||||||
|
<div className="h-full w-full">
|
||||||
|
{primaryPreviewUrl ? (
|
||||||
|
<img
|
||||||
|
src={buildPreviewImageUrl(primaryPreviewUrl, 360)}
|
||||||
|
alt={candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
|
||||||
|
className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full bg-[linear-gradient(180deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/35 to-transparent" />
|
||||||
|
<div className="absolute left-1.5 top-1.5 rounded-full border border-white/10 bg-black/45 px-1.5 py-0.5 text-[9px] text-white/78">
|
||||||
|
{getCandidateTargetCount(candidate, detailReferenceTarget)}张
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex min-h-[112px] min-w-0 flex-1 flex-col justify-between pr-7">
|
||||||
|
<div className="line-clamp-2 text-[13px] font-semibold leading-5 text-white">
|
||||||
|
{candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
{summaryBadges.map((badge, badgeIndex) => (
|
||||||
|
<span
|
||||||
|
key={`${candidate.id}-badge-${badgeIndex}-${badge}`}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full border px-2 py-1 text-[9px] leading-none",
|
||||||
|
badgeIndex === 0
|
||||||
|
? "border-emerald-400/25 bg-emerald-500/[0.12] text-emerald-100"
|
||||||
|
: "border-white/10 bg-white/[0.04] text-white/62"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-[10px] leading-4 text-white/45">
|
||||||
|
<div className="truncate">
|
||||||
|
场景名称:{candidate.sceneName || candidate.productModel || "未命名整套"}
|
||||||
|
</div>
|
||||||
|
<div className="truncate">
|
||||||
|
任务编号:{candidate.taskNo || "未记录"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-col gap-1 text-[10px] text-white/50">
|
||||||
|
<div className="truncate">店铺:{candidate.shopName || "未记录"}</div>
|
||||||
|
<div className="truncate">更新时间:{uploadedAtText}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isSelected ? (
|
||||||
|
<div className="absolute right-4 top-4 h-3 w-3 rounded-full bg-emerald-400 shadow-[0_0_0_4px_rgba(16,185,129,0.18),0_0_18px_rgba(52,211,153,0.45)]" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<motion.section
|
<motion.section
|
||||||
id="agent-anchor-results"
|
id="agent-anchor-results"
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -2414,174 +2606,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="inline-flex rounded-full border border-emerald-400/35 bg-emerald-500/[0.10] px-3 py-1.5 text-xs text-emerald-200">
|
|
||||||
按SPU查询
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3 flex flex-col gap-2 sm:flex-row">
|
|
||||||
<Input
|
|
||||||
value={detailReplacementQueryInput}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateDetailReplacementState({
|
|
||||||
query: e.target.value,
|
|
||||||
queryType: "spu",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder={`输入 SPU,查询任务整套中的${detailReferenceSourceLabel}参考图`}
|
|
||||||
className="h-12 rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleDetailReplacementSearch}
|
|
||||||
disabled={detailReplacementSearchState.isLoading}
|
|
||||||
className="h-12 rounded-[22px] bg-white/[0.04] px-5 text-sm text-white hover:bg-white/[0.08]"
|
|
||||||
>
|
|
||||||
{detailReplacementSearchState.isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
查询中
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
`查询${detailReferenceSourceLabel}`
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
{detailReplacementReferenceCards.length > 0 ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleClearDetailReferenceCards}
|
|
||||||
className="h-12 rounded-[22px] border-white/10 bg-white/[0.03] px-5 text-sm text-white hover:bg-white/[0.08]"
|
|
||||||
>
|
|
||||||
清空参考图
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim()) ? (
|
|
||||||
<div className="mb-3 rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-white/48">
|
|
||||||
当前参考图查询:
|
|
||||||
SPU /
|
|
||||||
{detailReplacementSearchState.lastQuery ||
|
|
||||||
detailReplacementQueryInput.trim().toUpperCase()}
|
|
||||||
{workflowStoreName ? (
|
|
||||||
<span className="ml-2 text-white/40">{`店铺 / ${workflowStoreName}`}</span>
|
|
||||||
) : null}
|
|
||||||
<span className="ml-2 text-emerald-200/70">
|
|
||||||
{`已接入真实${detailReferenceSourceLabel}参考图接口`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{detailReplacementCandidates.length > 0 ? (
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="mb-2 flex items-center justify-between gap-3">
|
|
||||||
<div className="text-xs text-white/42">
|
|
||||||
候选整套素材 {detailReplacementCandidates.length} 套
|
|
||||||
</div>
|
|
||||||
<div className="text-[11px] text-emerald-200/70">
|
|
||||||
默认载入最新一套,可随时切换
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="agent-dark-scrollbar grid max-h-[360px] grid-cols-1 gap-3 overflow-y-auto pr-1 xl:grid-cols-2">
|
|
||||||
{detailReplacementCandidates.map((candidate) => {
|
|
||||||
const isSelected = detailReplacementSelectedCandidateId === candidate.id;
|
|
||||||
const previewUrls = getCandidatePreviewUrls(
|
|
||||||
candidate,
|
|
||||||
detailReferenceTarget,
|
|
||||||
4
|
|
||||||
);
|
|
||||||
const primaryPreviewUrl = previewUrls[0] || getCandidatePreviewUrl(candidate, detailReferenceTarget);
|
|
||||||
const uploadedAtText = (() => {
|
|
||||||
const raw = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null;
|
|
||||||
if (!raw || Number.isNaN(raw.getTime())) return "未记录";
|
|
||||||
return raw.toLocaleString("zh-CN", {
|
|
||||||
timeZone: "Asia/Shanghai",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
const summaryBadges = [
|
|
||||||
`${detailReferenceSourceLabel}`,
|
|
||||||
`${getCandidateTargetCount(candidate, detailReferenceTarget)}张`,
|
|
||||||
candidate.categoryName || null,
|
|
||||||
].filter((item): item is string => Boolean(item));
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={candidate.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSelectDetailReplacementCandidate(candidate.id)}
|
|
||||||
className={cn(
|
|
||||||
"group relative min-w-0 overflow-hidden rounded-2xl border p-2.5 text-left transition-all duration-300",
|
|
||||||
isSelected
|
|
||||||
? "border-emerald-400/40 bg-emerald-500/[0.08] shadow-[0_0_24px_rgba(16,185,129,0.14)]"
|
|
||||||
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-stretch gap-3">
|
|
||||||
<div className="relative w-[88px] min-h-[112px] shrink-0 overflow-hidden rounded-[20px] border border-white/10 bg-black/30">
|
|
||||||
<div className="h-full w-full">
|
|
||||||
{primaryPreviewUrl ? (
|
|
||||||
<img
|
|
||||||
src={buildPreviewImageUrl(primaryPreviewUrl, 360)}
|
|
||||||
alt={candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
|
|
||||||
className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full bg-[linear-gradient(180deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/35 to-transparent" />
|
|
||||||
<div className="absolute left-1.5 top-1.5 rounded-full border border-white/10 bg-black/45 px-1.5 py-0.5 text-[9px] text-white/78">
|
|
||||||
{getCandidateTargetCount(candidate, detailReferenceTarget)}张
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-h-[112px] min-w-0 flex-1 flex-col justify-between pr-7">
|
|
||||||
<div className="line-clamp-2 text-[13px] font-semibold leading-5 text-white">
|
|
||||||
{candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
||||||
{summaryBadges.map((badge, badgeIndex) => (
|
|
||||||
<span
|
|
||||||
key={`${candidate.id}-badge-${badgeIndex}-${badge}`}
|
|
||||||
className={cn(
|
|
||||||
"rounded-full border px-2 py-1 text-[9px] leading-none",
|
|
||||||
badgeIndex === 0
|
|
||||||
? "border-emerald-400/25 bg-emerald-500/[0.12] text-emerald-100"
|
|
||||||
: "border-white/10 bg-white/[0.04] text-white/62"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{badge}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-[10px] leading-4 text-white/45">
|
|
||||||
<div className="truncate">
|
|
||||||
场景名称:{candidate.sceneName || candidate.productModel || "未命名整套"}
|
|
||||||
</div>
|
|
||||||
<div className="truncate">
|
|
||||||
任务编号:{candidate.taskNo || "未记录"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex flex-col gap-1 text-[10px] text-white/50">
|
|
||||||
<div className="truncate">店铺:{candidate.shopName || "未记录"}</div>
|
|
||||||
<div className="truncate">更新时间:{uploadedAtText}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isSelected ? (
|
|
||||||
<div className="absolute right-4 top-4 h-3 w-3 rounded-full bg-emerald-400 shadow-[0_0_0_4px_rgba(16,185,129,0.18),0_0_18px_rgba(52,211,153,0.45)]" />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
{detailReplacementReferenceCards.length > 0 ? (
|
{detailReplacementReferenceCards.length > 0 ? (
|
||||||
detailReplacementReferenceCards.map((card, index) => (
|
detailReplacementReferenceCards.map((card, index) => (
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Check, ImageIcon, Loader2, RefreshCw, Search } from "lucide-react";
|
import { Check, ChevronDown, ChevronUp, ImageIcon, Loader2, RefreshCw, Search } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@ -59,10 +59,19 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
theme = "light",
|
theme = "light",
|
||||||
}: AgentWorkflowMaterialPickerProps) {
|
}: AgentWorkflowMaterialPickerProps) {
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
const [isImagePreviewExpanded, setIsImagePreviewExpanded] = useState(true);
|
||||||
|
const [hasManualSelectionInteraction, setHasManualSelectionInteraction] = useState(false);
|
||||||
const selectedSet = useMemo(
|
const selectedSet = useMemo(
|
||||||
() => new Set((selectedImageIds || []).map((item) => String(item || "").trim()).filter(Boolean)),
|
() => new Set((selectedImageIds || []).map((item) => String(item || "").trim()).filter(Boolean)),
|
||||||
[selectedImageIds]
|
[selectedImageIds]
|
||||||
);
|
);
|
||||||
|
const recommendedImageIds = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(result?.recommendedImageIds)
|
||||||
|
? result.recommendedImageIds.map((item) => String(item || "").trim()).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
[result?.recommendedImageIds]
|
||||||
|
);
|
||||||
const selectedCount = result?.images
|
const selectedCount = result?.images
|
||||||
? result.images.filter((item) => selectedSet.has(item.id)).length
|
? result.images.filter((item) => selectedSet.has(item.id)).length
|
||||||
: 0;
|
: 0;
|
||||||
@ -102,8 +111,31 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
].filter((item) => Boolean(item.value));
|
].filter((item) => Boolean(item.value));
|
||||||
const remainingSelectableCount =
|
const remainingSelectableCount =
|
||||||
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
|
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
|
||||||
|
const hasSystemRecommendedSelection =
|
||||||
|
result?.queryType === "sku" &&
|
||||||
|
recommendedImageIds.length > 0 &&
|
||||||
|
selectedImageIds.length === recommendedImageIds.length &&
|
||||||
|
recommendedImageIds.every((id) => selectedSet.has(id));
|
||||||
|
const isAutoSelectedResult =
|
||||||
|
Boolean(hasSystemRecommendedSelection) && !hasManualSelectionInteraction;
|
||||||
|
const previewResetKey = useMemo(
|
||||||
|
() =>
|
||||||
|
JSON.stringify({
|
||||||
|
queryType: result?.queryType || "",
|
||||||
|
matchedSku: result?.matchedSku || "",
|
||||||
|
imageIds: result?.images?.map((image) => image.id) || [],
|
||||||
|
recommendedImageIds,
|
||||||
|
}),
|
||||||
|
[recommendedImageIds, result?.images, result?.matchedSku, result?.queryType]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasManualSelectionInteraction(false);
|
||||||
|
setIsImagePreviewExpanded(!hasSystemRecommendedSelection);
|
||||||
|
}, [hasSystemRecommendedSelection, previewResetKey]);
|
||||||
|
|
||||||
const handleToggleImage = (imageId: string) => {
|
const handleToggleImage = (imageId: string) => {
|
||||||
|
setHasManualSelectionInteraction(true);
|
||||||
const next = new Set(selectedImageIds);
|
const next = new Set(selectedImageIds);
|
||||||
if (next.has(imageId)) {
|
if (next.has(imageId)) {
|
||||||
next.delete(imageId);
|
next.delete(imageId);
|
||||||
@ -118,6 +150,7 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
|
setHasManualSelectionInteraction(true);
|
||||||
const allIds =
|
const allIds =
|
||||||
result?.images
|
result?.images
|
||||||
?.filter((item) => Boolean(String(item.imageUrl || "").trim()))
|
?.filter((item) => Boolean(String(item.imageUrl || "").trim()))
|
||||||
@ -324,7 +357,10 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onSelectedImageIdsChange([])}
|
onClick={() => {
|
||||||
|
setHasManualSelectionInteraction(true);
|
||||||
|
onSelectedImageIdsChange([]);
|
||||||
|
}}
|
||||||
disabled={selectedCount === 0 && !hasSelectableImages}
|
disabled={selectedCount === 0 && !hasSelectableImages}
|
||||||
className={cn(
|
className={cn(
|
||||||
isDark &&
|
isDark &&
|
||||||
@ -429,91 +465,133 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{result.images?.length ? (
|
{result.images?.length ? (
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="space-y-3">
|
||||||
{result.images.map((image) => {
|
{isAutoSelectedResult ? (
|
||||||
const selected = selectedSet.has(image.id);
|
<div
|
||||||
const isPlaceholder = !String(image.imageUrl || "").trim();
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={image.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (isPlaceholder) return;
|
|
||||||
handleToggleImage(image.id);
|
|
||||||
}}
|
|
||||||
disabled={isPlaceholder}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"group overflow-hidden rounded-[24px] border bg-background text-left transition-all",
|
"flex flex-wrap items-center justify-between gap-3 rounded-[20px] border px-3 py-2.5",
|
||||||
isPlaceholder && "cursor-wait",
|
isDark
|
||||||
selected
|
? "border-emerald-500/18 bg-emerald-500/[0.06] text-emerald-100"
|
||||||
? isDark
|
: "border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||||
? "border-emerald-400/70 bg-white/[0.03] shadow-[0_0_0_1px_rgba(52,211,153,0.2)]"
|
|
||||||
: "border-primary shadow-[0_0_0_1px_rgba(59,130,246,0.18)]"
|
|
||||||
: isDark
|
|
||||||
? "border-white/12 bg-white/[0.02] hover:border-white/25 hover:shadow-sm"
|
|
||||||
: "border-border hover:border-primary/40 hover:shadow-sm"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div className="text-xs leading-5">
|
||||||
|
系统已按权重自动选中 {selectedCount} 张 SKU 图,默认收起预览;需要检查或微调时再手动展开。
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsImagePreviewExpanded((current) => !current)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative aspect-square overflow-hidden",
|
"shrink-0",
|
||||||
isDark ? "bg-white/[0.04]" : "bg-muted/40"
|
isDark &&
|
||||||
|
"border-white/12 bg-white/[0.04] text-white/82 hover:bg-white/[0.10] hover:text-white"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isPlaceholder ? (
|
{isImagePreviewExpanded ? (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-3 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]">
|
<>
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-white/55" />
|
<ChevronUp className="mr-2 h-4 w-4" />
|
||||||
<div className="h-2 w-20 rounded-full bg-white/10" />
|
收起预览
|
||||||
<div className="h-2 w-14 rounded-full bg-white/10" />
|
</>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<img
|
<>
|
||||||
src={image.imageUrl}
|
<ChevronDown className="mr-2 h-4 w-4" />
|
||||||
alt={image.label}
|
展开预览
|
||||||
className="h-full w-full object-cover"
|
</>
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
onError={(event) => {
|
|
||||||
event.currentTarget.style.display = "none";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-2 py-2">
|
</Button>
|
||||||
<span className="rounded-full bg-black/60 px-2 py-1 text-[11px] text-white">
|
</div>
|
||||||
{isPlaceholder ? "加载中" : image.group === "fixed" ? "固定角度" : "其他图片"}
|
) : null}
|
||||||
</span>
|
|
||||||
{!isPlaceholder ? (
|
{(!isAutoSelectedResult || isImagePreviewExpanded) ? (
|
||||||
<span
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{result.images.map((image) => {
|
||||||
|
const selected = selectedSet.has(image.id);
|
||||||
|
const isPlaceholder = !String(image.imageUrl || "").trim();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={image.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (isPlaceholder) return;
|
||||||
|
handleToggleImage(image.id);
|
||||||
|
}}
|
||||||
|
disabled={isPlaceholder}
|
||||||
|
className={cn(
|
||||||
|
"group overflow-hidden rounded-[24px] border bg-background text-left transition-all",
|
||||||
|
isPlaceholder && "cursor-wait",
|
||||||
|
selected
|
||||||
|
? isDark
|
||||||
|
? "border-emerald-400/70 bg-white/[0.03] shadow-[0_0_0_1px_rgba(52,211,153,0.2)]"
|
||||||
|
: "border-primary shadow-[0_0_0_1px_rgba(59,130,246,0.18)]"
|
||||||
|
: isDark
|
||||||
|
? "border-white/12 bg-white/[0.02] hover:border-white/25 hover:shadow-sm"
|
||||||
|
: "border-border hover:border-primary/40 hover:shadow-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-6 w-6 items-center justify-center rounded-full border text-white transition-colors",
|
"relative aspect-square overflow-hidden",
|
||||||
selected
|
isDark ? "bg-white/[0.04]" : "bg-muted/40"
|
||||||
? isDark
|
|
||||||
? "border-emerald-400 bg-emerald-400 text-black"
|
|
||||||
: "border-primary bg-primary"
|
|
||||||
: "border-white/60 bg-black/35 opacity-0 group-hover:opacity-100"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Check className="h-3.5 w-3.5" />
|
{isPlaceholder ? (
|
||||||
</span>
|
<div className="flex h-full w-full flex-col items-center justify-center gap-3 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]">
|
||||||
) : null}
|
<Loader2 className="h-6 w-6 animate-spin text-white/55" />
|
||||||
</div>
|
<div className="h-2 w-20 rounded-full bg-white/10" />
|
||||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 via-black/25 to-transparent px-3 py-2 text-[11px] text-white">
|
<div className="h-2 w-14 rounded-full bg-white/10" />
|
||||||
{isPlaceholder ? "白底图加载中..." : image.label}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<img
|
||||||
<div className="space-y-1 px-3 py-2">
|
src={image.imageUrl}
|
||||||
<div className={cn("truncate text-xs font-medium", isDark ? "text-white/88" : "")}>
|
alt={image.label}
|
||||||
{isPlaceholder ? "正在获取 SKU 图片..." : image.fileName}
|
className="h-full w-full object-cover"
|
||||||
</div>
|
loading="lazy"
|
||||||
<div className={cn("text-[11px]", isDark ? "text-white/60" : "text-muted-foreground")}>
|
decoding="async"
|
||||||
{isPlaceholder
|
onError={(event) => {
|
||||||
? "已先展示预览卡位,图片会逐步加载出来。"
|
event.currentTarget.style.display = "none";
|
||||||
: `系统会先按权重自动选图,你也可以点击${selected ? "取消" : "调整"}。`}
|
}}
|
||||||
</div>
|
/>
|
||||||
</div>
|
)}
|
||||||
</button>
|
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-2 py-2">
|
||||||
);
|
<span className="rounded-full bg-black/60 px-2 py-1 text-[11px] text-white">
|
||||||
})}
|
{isPlaceholder ? "加载中" : image.group === "fixed" ? "固定角度" : "其他图片"}
|
||||||
|
</span>
|
||||||
|
{!isPlaceholder ? (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex h-6 w-6 items-center justify-center rounded-full border text-white transition-colors",
|
||||||
|
selected
|
||||||
|
? isDark
|
||||||
|
? "border-emerald-400 bg-emerald-400 text-black"
|
||||||
|
: "border-primary bg-primary"
|
||||||
|
: "border-white/60 bg-black/35 opacity-0 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 via-black/25 to-transparent px-3 py-2 text-[11px] text-white">
|
||||||
|
{isPlaceholder ? "白底图加载中..." : image.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 px-3 py-2">
|
||||||
|
<div className={cn("truncate text-xs font-medium", isDark ? "text-white/88" : "")}>
|
||||||
|
{isPlaceholder ? "正在获取 SKU 图片..." : image.fileName}
|
||||||
|
</div>
|
||||||
|
<div className={cn("text-[11px]", isDark ? "text-white/60" : "text-muted-foreground")}>
|
||||||
|
{isPlaceholder
|
||||||
|
? "已先展示预览卡位,图片会逐步加载出来。"
|
||||||
|
: `系统会先按权重自动选图,你也可以点击${selected ? "取消" : "调整"}。`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -102,38 +102,6 @@ export function AgentWorkflowMaterialSelectionPanel({
|
|||||||
typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0
|
typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0
|
||||||
? Math.max(productSelectionLimit - primarySelectedCount, secondarySelectedCount, 0)
|
? Math.max(productSelectionLimit - primarySelectedCount, secondarySelectedCount, 0)
|
||||||
: null;
|
: null;
|
||||||
const hasAnyQueryInput =
|
|
||||||
Boolean(String(inputs.primarySku || "").trim()) ||
|
|
||||||
Boolean(String(inputs.secondarySku || "").trim());
|
|
||||||
const hasSkuSelectionPending =
|
|
||||||
Boolean(materials.primary.result?.skuOptions?.length) ||
|
|
||||||
Boolean(materials.secondary.result?.skuOptions?.length);
|
|
||||||
const hasAnyMaterialResult =
|
|
||||||
Boolean(materials.primary.result?.images?.length) ||
|
|
||||||
Boolean(materials.secondary.result?.images?.length) ||
|
|
||||||
hasSkuSelectionPending;
|
|
||||||
const hasAnySelectedImages =
|
|
||||||
materials.primary.selectedImageIds.length > 0 || materials.secondary.selectedImageIds.length > 0;
|
|
||||||
const materialGuideSteps = [
|
|
||||||
{
|
|
||||||
badge: "01",
|
|
||||||
label: "输入型号",
|
|
||||||
description: "先选择按 SPU 或 SKU 搜索,再输入商品型号。",
|
|
||||||
status: hasAnyQueryInput ? "completed" : "current",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
badge: "02",
|
|
||||||
label: "选择SKU",
|
|
||||||
description: "如果是 SPU 查询,先从返回的 SKU 列表里点选具体 SKU。",
|
|
||||||
status: hasSkuSelectionPending ? "current" : hasAnyMaterialResult ? "completed" : "pending",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
badge: "03",
|
|
||||||
label: "自动选图",
|
|
||||||
description: "系统会按角度权重自动选图并映射到产品槽位,你也可以手动微调。",
|
|
||||||
status: hasAnySelectedImages ? "completed" : hasAnyMaterialResult ? "current" : "pending",
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasSecondaryMaterialContent) {
|
if (hasSecondaryMaterialContent) {
|
||||||
@ -273,71 +241,6 @@ export function AgentWorkflowMaterialSelectionPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"space-y-3 rounded-[24px] border px-4 py-4",
|
|
||||||
isDark ? "border-white/10 bg-black/25" : "border-[#dbe3ff] bg-white"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "text-[#334155]")}>
|
|
||||||
选图流程
|
|
||||||
</div>
|
|
||||||
<div className={cn("text-xs leading-5", isDark ? "text-white/50" : "text-[#64748b]")}>
|
|
||||||
按这 3 步操作就能完成输入图准备,避免来回试错。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-3">
|
|
||||||
{materialGuideSteps.map((step) => (
|
|
||||||
<div
|
|
||||||
key={step.badge}
|
|
||||||
className={cn(
|
|
||||||
"rounded-[20px] border px-4 py-3",
|
|
||||||
step.status === "completed"
|
|
||||||
? isDark
|
|
||||||
? "border-emerald-400/20 bg-emerald-500/[0.08]"
|
|
||||||
: "border-emerald-200 bg-emerald-50"
|
|
||||||
: step.status === "current"
|
|
||||||
? isDark
|
|
||||||
? "border-amber-400/20 bg-amber-500/[0.08]"
|
|
||||||
: "border-amber-200 bg-amber-50"
|
|
||||||
: isDark
|
|
||||||
? "border-white/10 bg-white/[0.03]"
|
|
||||||
: "border-[#dbe3ff] bg-[#f8faff]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-2.5 py-1 text-[11px] font-semibold",
|
|
||||||
step.status === "completed"
|
|
||||||
? isDark
|
|
||||||
? "bg-emerald-300 text-black"
|
|
||||||
: "bg-emerald-600 text-white"
|
|
||||||
: step.status === "current"
|
|
||||||
? isDark
|
|
||||||
? "bg-amber-300 text-black"
|
|
||||||
: "bg-amber-500 text-white"
|
|
||||||
: isDark
|
|
||||||
? "bg-white/10 text-white/60"
|
|
||||||
: "bg-white text-[#64748b]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{step.badge}
|
|
||||||
</span>
|
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "text-[#334155]")}>
|
|
||||||
{step.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={cn("mt-2 text-xs leading-5", isDark ? "text-white/50" : "text-[#64748b]")}>
|
|
||||||
{step.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AgentWorkflowMaterialPicker
|
<AgentWorkflowMaterialPicker
|
||||||
value={inputs.primarySku}
|
value={inputs.primarySku}
|
||||||
queryMode={materialSearchMode.primary}
|
queryMode={materialSearchMode.primary}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user