feat: expand workflow tree depth and child navigation
This commit is contained in:
parent
de142d0be7
commit
5407ad751a
@ -26,6 +26,7 @@ import {
|
||||
Video,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
AGENT_WORKFLOW_MAX_OPTION_DEPTH,
|
||||
AGENT_WORKFLOW_MAX_TREE_DEPTH,
|
||||
countAgentWorkflowOptions,
|
||||
countAgentWorkflowOptionTree,
|
||||
@ -104,10 +105,20 @@ const itemVariants = {
|
||||
visible: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
const WORKFLOW_LEVEL_LABELS = ["一级模块", "二级模块", "三级模块"];
|
||||
const WORKFLOW_OPTION_LEVEL_LABELS = ["一级卡片", "二级卡片", "三级卡片"];
|
||||
const CHINESE_LEVEL_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八"];
|
||||
const WORKFLOW_LEVEL_LABELS = CHINESE_LEVEL_NUMERALS.map((item) => `${item}级模块`);
|
||||
const WORKFLOW_OPTION_LEVEL_LABELS = CHINESE_LEVEL_NUMERALS.map((item) => `${item}级卡片`);
|
||||
const PRESET_LIBRARY_LEVEL_LABELS = ["一级分类", "二级分类", "三级分类"];
|
||||
|
||||
function getWorkflowLevelLabel(depth: number, kind: "模块" | "卡片") {
|
||||
const labels =
|
||||
kind === "模块" ? WORKFLOW_LEVEL_LABELS : WORKFLOW_OPTION_LEVEL_LABELS;
|
||||
return (
|
||||
labels[Math.max(0, Math.min(depth - 1, labels.length - 1))] ||
|
||||
`第 ${depth} 级${kind}`
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeConfigOrdering(config: AgentWorkflowUiConfig): AgentWorkflowUiConfig {
|
||||
return {
|
||||
...config,
|
||||
@ -168,7 +179,7 @@ function createWorkflowStepDraft(
|
||||
stepIndex: number,
|
||||
existingStepKeys: string[]
|
||||
): AgentWorkflowStepUiConfig {
|
||||
const levelLabel = WORKFLOW_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, 2))] || "模块";
|
||||
const levelLabel = getWorkflowLevelLabel(depth, "模块");
|
||||
const stepKey = ensureUniqueWorkflowKey(`step_${depth}_${stepIndex + 1}`, existingStepKeys);
|
||||
return {
|
||||
key: stepKey,
|
||||
@ -880,7 +891,7 @@ export function AgentWorkflowSettingsClient({
|
||||
const parentDepth = flattenAgentWorkflowOptionTree(step.options || []).find(
|
||||
({ option }) => option.key === parentOptionKey
|
||||
)?.depth;
|
||||
if (!parentOption || !parentDepth || parentDepth >= 3) {
|
||||
if (!parentOption || !parentDepth || parentDepth >= AGENT_WORKFLOW_MAX_OPTION_DEPTH) {
|
||||
return step;
|
||||
}
|
||||
|
||||
@ -1196,13 +1207,11 @@ export function AgentWorkflowSettingsClient({
|
||||
): ReactNode => {
|
||||
const refKey = `${step.key}:${option.key}`;
|
||||
const isUploading = uploadingKey === refKey;
|
||||
const levelLabel =
|
||||
WORKFLOW_OPTION_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, WORKFLOW_OPTION_LEVEL_LABELS.length - 1))] ||
|
||||
`第 ${depth} 级卡片`;
|
||||
const levelLabel = getWorkflowLevelLabel(depth, "卡片");
|
||||
const sortedChildren = [...(option.children || [])].sort(
|
||||
(left, right) => left.sortOrder - right.sortOrder
|
||||
);
|
||||
const canAddChild = depth < 3;
|
||||
const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH;
|
||||
|
||||
return (
|
||||
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
|
||||
@ -1334,7 +1343,7 @@ export function AgentWorkflowSettingsClient({
|
||||
新增子卡片
|
||||
</Button>
|
||||
) : (
|
||||
<Badge variant="outline">已达三级</Badge>
|
||||
<Badge variant="outline">{`已达 ${AGENT_WORKFLOW_MAX_OPTION_DEPTH} 级`}</Badge>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
@ -1363,7 +1372,7 @@ export function AgentWorkflowSettingsClient({
|
||||
<div>
|
||||
<div className="text-sm font-medium">子卡片配置</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
当前卡片下可继续建立二级、三级卡片母子关系。
|
||||
{`当前卡片下可继续建立最多 ${AGENT_WORKFLOW_MAX_OPTION_DEPTH} 级卡片母子关系。`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1394,9 +1403,7 @@ export function AgentWorkflowSettingsClient({
|
||||
indexPath: number[]
|
||||
) => {
|
||||
const stepLabel = indexPath.join(".");
|
||||
const levelLabel =
|
||||
WORKFLOW_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, WORKFLOW_LEVEL_LABELS.length - 1))] ||
|
||||
`第 ${depth} 级模块`;
|
||||
const levelLabel = getWorkflowLevelLabel(depth, "模块");
|
||||
const rootOptions = [...(step.options || [])].sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
const optionCardCount = countAgentWorkflowOptionTree(step.options || []);
|
||||
|
||||
|
||||
@ -111,6 +111,50 @@ function getWorkflowStepSelectionSummary(params: {
|
||||
return selectedOptions.map((option) => option.buttonName).join(" / ");
|
||||
}
|
||||
|
||||
function getWorkflowStepOptionLevels(params: {
|
||||
step: AgentWorkflowUiConfig["steps"][number];
|
||||
selectionValue?: string;
|
||||
}) {
|
||||
const levels: Array<{
|
||||
depth: number;
|
||||
options: AgentWorkflowUiConfig["steps"][number]["options"];
|
||||
selectedKey: string;
|
||||
selectedOption: AgentWorkflowUiConfig["steps"][number]["options"][number] | null;
|
||||
}> = [];
|
||||
const selectedPath = getAgentWorkflowSelectedOptionPath({
|
||||
step: params.step,
|
||||
selectionValue: params.selectionValue,
|
||||
});
|
||||
|
||||
let depth = 1;
|
||||
let currentOptions = getEnabledAgentWorkflowOptionTree(params.step.options || []);
|
||||
|
||||
while (currentOptions.length > 0) {
|
||||
const selectedKey = selectedPath[depth - 1]?.key || "";
|
||||
const selectedOption =
|
||||
currentOptions.find((option) => option.key === selectedKey) || null;
|
||||
|
||||
levels.push({
|
||||
depth,
|
||||
options: currentOptions,
|
||||
selectedKey,
|
||||
selectedOption,
|
||||
});
|
||||
|
||||
const nextOptions = getEnabledAgentWorkflowOptionTree(
|
||||
selectedOption?.children || []
|
||||
);
|
||||
if (!selectedOption || nextOptions.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentOptions = nextOptions;
|
||||
depth += 1;
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
function StepOptionCard({
|
||||
option,
|
||||
step,
|
||||
@ -346,6 +390,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
return Object.fromEntries(entries) as Record<string, string>;
|
||||
}, [resolvedConfig.steps, selections]);
|
||||
const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState<string>("");
|
||||
const [activeEmbeddedOptionDepth, setActiveEmbeddedOptionDepth] = useState(1);
|
||||
const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState<string | null>(null);
|
||||
const activeEmbeddedStepIndex = Math.max(
|
||||
flattenedResolvedSteps.findIndex(({ step }) => step.key === activeEmbeddedStepKey),
|
||||
@ -353,6 +398,21 @@ export function AgentWorkflowPanelDarkV2({
|
||||
);
|
||||
const activeEmbeddedStepEntry =
|
||||
flattenedResolvedSteps[activeEmbeddedStepIndex] || flattenedResolvedSteps[0] || null;
|
||||
const activeEmbeddedOptionLevels = useMemo(
|
||||
() =>
|
||||
activeEmbeddedStepEntry
|
||||
? getWorkflowStepOptionLevels({
|
||||
step: activeEmbeddedStepEntry.step,
|
||||
selectionValue: selections[activeEmbeddedStepEntry.step.key],
|
||||
})
|
||||
: [],
|
||||
[activeEmbeddedStepEntry, selections]
|
||||
);
|
||||
const activeEmbeddedOptionLevel =
|
||||
activeEmbeddedOptionLevels.find((level) => level.depth === activeEmbeddedOptionDepth) ||
|
||||
activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1] ||
|
||||
activeEmbeddedOptionLevels[0] ||
|
||||
null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!embedded) {
|
||||
@ -376,6 +436,33 @@ export function AgentWorkflowPanelDarkV2({
|
||||
setActiveEmbeddedStepKey(flattenedResolvedSteps[0]?.step.key || "");
|
||||
}, [activeEmbeddedStepKey, embedded, flattenedResolvedSteps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embedded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeEmbeddedStepEntry) {
|
||||
if (activeEmbeddedOptionDepth !== 1) {
|
||||
setActiveEmbeddedOptionDepth(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const firstIncompleteDepth =
|
||||
activeEmbeddedOptionLevels.find((level) => !level.selectedKey)?.depth ||
|
||||
activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1]?.depth ||
|
||||
1;
|
||||
|
||||
setActiveEmbeddedOptionDepth((current) => {
|
||||
const maxDepth =
|
||||
activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1]?.depth || 1;
|
||||
if (current < 1 || current > maxDepth) {
|
||||
return firstIncompleteDepth;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}, [activeEmbeddedOptionDepth, activeEmbeddedOptionLevels, activeEmbeddedStepEntry, embedded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embedded || !activeEmbeddedStepEntry) {
|
||||
return;
|
||||
@ -405,6 +492,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
|
||||
if (nextStep && nextStep.step.key !== activeEmbeddedStepKey) {
|
||||
setActiveEmbeddedStepKey(nextStep.step.key);
|
||||
setActiveEmbeddedOptionDepth(1);
|
||||
}
|
||||
|
||||
setPendingAutoAdvanceStepKey(null);
|
||||
@ -611,12 +699,141 @@ export function AgentWorkflowPanelDarkV2({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeEmbeddedOptionLevels.length > 0 ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-black/20 p-3">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-white/78">子节点流程</div>
|
||||
<div className="text-[11px] text-white/38">
|
||||
当前步骤的子节点会在这里逐级展开,减少单屏内容堆积。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-[11px] text-white/55">
|
||||
共 {activeEmbeddedOptionLevels.length} 级
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{activeEmbeddedOptionLevels.map((level, levelIndex) => {
|
||||
const isActive = level.depth === activeEmbeddedOptionDepth;
|
||||
const summaryText =
|
||||
level.selectedOption?.buttonName || "待选择";
|
||||
return (
|
||||
<Fragment key={`${activeEmbeddedStepEntry.step.key}-level-${level.depth}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveEmbeddedOptionDepth(level.depth)}
|
||||
className={cn(
|
||||
"group min-w-0 rounded-2xl border px-3 py-2 text-left transition-all",
|
||||
isActive
|
||||
? "border-emerald-300/35 bg-emerald-500/[0.12] shadow-[0_0_18px_rgba(52,211,153,0.12)]"
|
||||
: "border-white/10 bg-white/[0.03] hover:border-white/18 hover:bg-white/[0.05]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold",
|
||||
isActive
|
||||
? "bg-emerald-300 text-black"
|
||||
: "bg-white/[0.06] text-white/55"
|
||||
)}
|
||||
>
|
||||
{level.depth}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-[12px] font-medium",
|
||||
isActive ? "text-white" : "text-white/68"
|
||||
)}
|
||||
>
|
||||
{level.depth === 1 ? "主卡片" : `${level.depth}级子卡片`}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-1 pl-7 text-[11px]",
|
||||
isActive ? "text-emerald-100/76" : "text-white/36"
|
||||
)}
|
||||
>
|
||||
{summaryText}
|
||||
</div>
|
||||
</button>
|
||||
{levelIndex < activeEmbeddedOptionLevels.length - 1 ? (
|
||||
<div className="h-px w-4 bg-white/10" />
|
||||
) : null}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{renderOptionLevels(
|
||||
activeEmbeddedStepEntry.step,
|
||||
getEnabledAgentWorkflowOptionTree(activeEmbeddedStepEntry.step.options || [])
|
||||
)}
|
||||
{activeEmbeddedOptionLevel ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full border border-white/12 px-2.5 py-1 text-[11px] font-medium text-white/45">
|
||||
{activeEmbeddedOptionLevel.depth === 1
|
||||
? "主卡片"
|
||||
: `${activeEmbeddedOptionLevel.depth}级子卡片`}
|
||||
</span>
|
||||
<span className="text-xs text-white/35">
|
||||
{activeEmbeddedOptionLevel.depth === 1
|
||||
? "先选择当前步骤的主卡片"
|
||||
: "当前层级只展示这一层的子节点卡片,选完再进入下一层"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{activeEmbeddedOptionLevel.options.map((option, optionIndex) => {
|
||||
const currentSelectedKey = activeEmbeddedOptionLevel.selectedKey;
|
||||
const nextChildren = getEnabledAgentWorkflowOptionTree(
|
||||
option.children || []
|
||||
);
|
||||
|
||||
return (
|
||||
<StepOptionCard
|
||||
key={`${option.key}-${activeEmbeddedOptionLevel.depth}`}
|
||||
option={option}
|
||||
step={activeEmbeddedStepEntry.step}
|
||||
selected={currentSelectedKey === option.key}
|
||||
onClick={() => {
|
||||
const selectedPathKeys = activeEmbeddedOptionLevels
|
||||
.slice(0, activeEmbeddedOptionLevel.depth - 1)
|
||||
.map((level) => level.selectedKey)
|
||||
.filter(Boolean);
|
||||
const nextKeys =
|
||||
currentSelectedKey === option.key
|
||||
? selectedPathKeys
|
||||
: [...selectedPathKeys, option.key];
|
||||
|
||||
setPendingAutoAdvanceStepKey(activeEmbeddedStepEntry.step.key);
|
||||
onSelectionsChange({
|
||||
...selections,
|
||||
[activeEmbeddedStepEntry.step.key]:
|
||||
nextKeys.length > 0
|
||||
? serializeAgentWorkflowSelectionPath(nextKeys)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (currentSelectedKey !== option.key && nextChildren.length > 0) {
|
||||
setActiveEmbeddedOptionDepth(
|
||||
activeEmbeddedOptionLevel.depth + 1
|
||||
);
|
||||
} else {
|
||||
setActiveEmbeddedOptionDepth(
|
||||
activeEmbeddedOptionLevel.depth
|
||||
);
|
||||
}
|
||||
}}
|
||||
index={optionIndex}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</motion.section>
|
||||
) : null
|
||||
) : (
|
||||
@ -638,7 +855,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
{step.title}
|
||||
{depth > 1 ? (
|
||||
<span className="rounded-full border border-white/12 px-2 py-0.5 text-[11px] font-medium text-white/45">
|
||||
{depth === 2 ? "二级模块" : "三级模块"}
|
||||
{`${depth}级模块`}
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
|
||||
@ -6,8 +6,8 @@ import type {
|
||||
} from "@/types/agent";
|
||||
|
||||
export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
||||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 3;
|
||||
export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 3;
|
||||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 8;
|
||||
export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 8;
|
||||
|
||||
export interface FlattenedAgentWorkflowStep {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user