feat: expand workflow tree depth and child navigation
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-25 18:47:58 +08:00
parent de142d0be7
commit 5407ad751a
3 changed files with 244 additions and 20 deletions

View File

@ -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 || []);

View File

@ -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>

View File

@ -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;