diff --git a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx index 42609a805..de2ff5c14 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -159,7 +159,6 @@ const WORKFLOW_STEP_DRAG_DATA_TYPE = "application/x-agent-workflow-step"; const WORKFLOW_SETTINGS_DRAFT_STORAGE_KEY = "agent-workflow-settings-draft:v1"; const WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY = "agent-workflow-settings-stale-draft:v1"; const WORKFLOW_SETTINGS_DRAFT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; -const WORKFLOW_BLOCKED_CANDIDATE_RENDER_LIMIT = 60; type WorkflowStepDropPosition = "before" | "after" | "inside"; @@ -187,6 +186,42 @@ interface AgentWorkflowSettingsDraft { state: AgentWorkflowSettingsDraftState; } +interface WorkflowOptionContentSummary { + total: number; + withPrompt: number; + withDescription: number; + withPreview: number; + disabled: number; + withMutualExclusion: number; +} + +interface WorkflowStepAnalysis { + key: string; + title: string; + path: string; + depth: number; + childStepCount: number; + directRootOptionCount: number; + optionStats: WorkflowOptionContentSummary; + subtreeStepCount: number; + subtreeOptionCount: number; + hasOwnContent: boolean; + hasSubtreeContent: boolean; + enabled: boolean; +} + +interface WorkflowOptionGapAnalysis { + key: string; + label: string; + path: string; + stepKey: string; + stepPath: string; + depth: number; + missingFields: string[]; + disabled: boolean; + childCount: number; +} + type BufferedInputProps = Omit< ComponentProps, "defaultValue" | "onChange" | "value" @@ -513,6 +548,191 @@ function collectWorkflowOptionKeys(options: AgentWorkflowOptionUiConfig[]): stri return flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key); } +function hasWorkflowOptionPrompt(option: AgentWorkflowOptionUiConfig) { + return Boolean(String(option.promptFragment || "").trim()); +} + +function hasWorkflowOptionDescription(option: AgentWorkflowOptionUiConfig) { + return Boolean(String(option.description || "").trim()); +} + +function hasWorkflowOptionPreview(option: AgentWorkflowOptionUiConfig) { + return Boolean(String(option.previewImageUrl || "").trim()); +} + +function hasWorkflowOptionConfiguredContent(option: AgentWorkflowOptionUiConfig) { + return ( + hasWorkflowOptionPrompt(option) || + hasWorkflowOptionDescription(option) || + hasWorkflowOptionPreview(option) + ); +} + +function getWorkflowOptionMissingFields(option: AgentWorkflowOptionUiConfig): string[] { + const missingFields: string[] = []; + + if (!String(option.buttonName || "").trim()) { + missingFields.push("名称"); + } + if (!hasWorkflowOptionPrompt(option)) { + missingFields.push("Prompt"); + } + if (!hasWorkflowOptionDescription(option)) { + missingFields.push("说明"); + } + if (!hasWorkflowOptionPreview(option)) { + missingFields.push("预览图"); + } + + return missingFields; +} + +function createEmptyWorkflowOptionContentSummary(): WorkflowOptionContentSummary { + return { + total: 0, + withPrompt: 0, + withDescription: 0, + withPreview: 0, + disabled: 0, + withMutualExclusion: 0, + }; +} + +function analyzeWorkflowOptionContent( + options: AgentWorkflowOptionUiConfig[] +): WorkflowOptionContentSummary { + return flattenAgentWorkflowOptionTree(options).reduce( + (summary, { option }) => ({ + total: summary.total + 1, + withPrompt: summary.withPrompt + (hasWorkflowOptionPrompt(option) ? 1 : 0), + withDescription: + summary.withDescription + (hasWorkflowOptionDescription(option) ? 1 : 0), + withPreview: summary.withPreview + (hasWorkflowOptionPreview(option) ? 1 : 0), + disabled: summary.disabled + (option.enabled === false ? 1 : 0), + withMutualExclusion: + summary.withMutualExclusion + ((option.blockedOptionKeys || []).length > 0 ? 1 : 0), + }), + createEmptyWorkflowOptionContentSummary() + ); +} + +function buildWorkflowStepAnalysisBranch( + step: AgentWorkflowStepUiConfig, + depth: number, + parentPath: string[] +): { + items: WorkflowStepAnalysis[]; + subtreeStepCount: number; + subtreeOptionCount: number; + hasSubtreeContent: boolean; +} { + const title = step.title || step.key; + const pathParts = [...parentPath, title]; + const childBranches = [...(step.children || [])] + .sort((left, right) => left.sortOrder - right.sortOrder) + .map((childStep) => buildWorkflowStepAnalysisBranch(childStep, depth + 1, pathParts)); + const optionStats = analyzeWorkflowOptionContent(step.options || []); + const childStepCount = (step.children || []).length; + const subtreeStepCount = + 1 + childBranches.reduce((total, child) => total + child.subtreeStepCount, 0); + const subtreeOptionCount = + optionStats.total + + childBranches.reduce((total, child) => total + child.subtreeOptionCount, 0); + const hasOwnContent = + optionStats.withPrompt > 0 || optionStats.withDescription > 0 || optionStats.withPreview > 0; + const hasSubtreeContent = + hasOwnContent || childBranches.some((child) => child.hasSubtreeContent); + + const current: WorkflowStepAnalysis = { + key: step.key, + title, + path: pathParts.join(" / "), + depth, + childStepCount, + directRootOptionCount: (step.options || []).length, + optionStats, + subtreeStepCount, + subtreeOptionCount, + hasOwnContent, + hasSubtreeContent, + enabled: step.enabled !== false, + }; + + return { + items: [current, ...childBranches.flatMap((child) => child.items)], + subtreeStepCount, + subtreeOptionCount, + hasSubtreeContent, + }; +} + +function buildWorkflowStepAnalyses( + steps: AgentWorkflowStepUiConfig[] +): WorkflowStepAnalysis[] { + return [...steps] + .sort((left, right) => left.sortOrder - right.sortOrder) + .flatMap((step) => buildWorkflowStepAnalysisBranch(step, 1, []).items); +} + +function buildWorkflowOptionGapDetailsFromOptions( + options: AgentWorkflowOptionUiConfig[], + stepKey: string, + stepPath: string, + ancestors: string[] = [] +): WorkflowOptionGapAnalysis[] { + return [...options] + .sort((left, right) => left.sortOrder - right.sortOrder) + .flatMap((option) => { + const label = option.buttonName || option.key; + const pathParts = [...ancestors, label]; + const missingFields = getWorkflowOptionMissingFields(option); + const current = + missingFields.length > 0 + ? [ + { + key: option.key, + label, + path: pathParts.join(" / "), + stepKey, + stepPath, + depth: pathParts.length, + missingFields, + disabled: option.enabled === false, + childCount: (option.children || []).length, + }, + ] + : []; + + return [ + ...current, + ...buildWorkflowOptionGapDetailsFromOptions( + option.children || [], + stepKey, + stepPath, + pathParts + ), + ]; + }); +} + +function buildWorkflowOptionGapDetails( + steps: AgentWorkflowStepUiConfig[], + parentPath: string[] = [] +): WorkflowOptionGapAnalysis[] { + return [...steps] + .sort((left, right) => left.sortOrder - right.sortOrder) + .flatMap((step) => { + const stepTitle = step.title || step.key; + const stepPathParts = [...parentPath, stepTitle]; + const stepPath = stepPathParts.join(" / "); + + return [ + ...buildWorkflowOptionGapDetailsFromOptions(step.options || [], step.key, stepPath), + ...buildWorkflowOptionGapDetails(step.children || [], stepPathParts), + ]; + }); +} + function collectWorkflowOptionRelations( options: AgentWorkflowOptionUiConfig[], ancestors: string[] = [] @@ -520,6 +740,13 @@ function collectWorkflowOptionRelations( key: string; label: string; depth: number; + childCount: number; + hasPrompt: boolean; + hasDescription: boolean; + hasPreview: boolean; + hasContent: boolean; + disabled: boolean; + blockedCount: number; }> { return options.flatMap((option) => { const currentPath = [...ancestors, option.buttonName || option.key]; @@ -528,6 +755,13 @@ function collectWorkflowOptionRelations( key: option.key, label: currentPath.join(" / "), depth: currentPath.length, + childCount: (option.children || []).length, + hasPrompt: hasWorkflowOptionPrompt(option), + hasDescription: hasWorkflowOptionDescription(option), + hasPreview: hasWorkflowOptionPreview(option), + hasContent: hasWorkflowOptionConfiguredContent(option), + disabled: option.enabled === false, + blockedCount: (option.blockedOptionKeys || []).length, }, ...collectWorkflowOptionRelations(option.children || [], currentPath), ]; @@ -1848,6 +2082,83 @@ export function AgentWorkflowSettingsClient({ () => new Map(workflowOptionRelations.map((item) => [item.key, item] as const)), [workflowOptionRelations] ); + const workflowStepAnalyses = useMemo( + () => buildWorkflowStepAnalyses(deferredWorkflowSteps || []), + [deferredWorkflowSteps] + ); + const workflowStepAnalysisLookup = useMemo( + () => new Map(workflowStepAnalyses.map((item) => [item.key, item] as const)), + [workflowStepAnalyses] + ); + const workflowOptionGapDetails = useMemo( + () => + buildWorkflowOptionGapDetails(deferredWorkflowSteps || []).sort( + (left, right) => + right.missingFields.length - left.missingFields.length || + right.depth - left.depth || + left.stepPath.localeCompare(right.stepPath, "zh-CN") || + left.path.localeCompare(right.path, "zh-CN") + ), + [deferredWorkflowSteps] + ); + const workflowDashboardMetrics = useMemo(() => { + const levelCounts = new Map(); + let totalCards = 0; + let promptCards = 0; + let disabledCards = 0; + let mutualExclusionCards = 0; + + for (const item of workflowStepAnalyses) { + levelCounts.set(item.depth, (levelCounts.get(item.depth) || 0) + 1); + totalCards += item.optionStats.total; + promptCards += item.optionStats.withPrompt; + disabledCards += item.optionStats.disabled; + mutualExclusionCards += item.optionStats.withMutualExclusion; + } + + const emptyOwnContentSteps = workflowStepAnalyses.filter((item) => !item.hasOwnContent); + const emptyBranchSteps = workflowStepAnalyses.filter((item) => !item.hasSubtreeContent); + const leafEmptySteps = workflowStepAnalyses.filter( + (item) => item.childStepCount === 0 && !item.hasOwnContent + ); + const denseSteps = [...workflowStepAnalyses] + .sort( + (left, right) => + right.subtreeOptionCount + + right.subtreeStepCount - + (left.subtreeOptionCount + left.subtreeStepCount) + ) + .slice(0, 5); + const deepestSteps = [...workflowStepAnalyses] + .sort((left, right) => right.depth - left.depth) + .slice(0, 5); + + return { + totalSteps: workflowStepAnalyses.length, + totalCards, + promptCards, + disabledCards, + mutualExclusionCards, + missingCardDetails: workflowOptionGapDetails, + missingPromptCards: workflowOptionGapDetails.filter((item) => + item.missingFields.includes("Prompt") + ).length, + fullyEmptyCards: workflowOptionGapDetails.filter( + (item) => + item.missingFields.includes("Prompt") && + item.missingFields.includes("说明") && + item.missingFields.includes("预览图") + ).length, + maxDepth: workflowStepAnalyses.reduce((max, item) => Math.max(max, item.depth), 0), + promptCompletionRate: totalCards > 0 ? Math.round((promptCards / totalCards) * 100) : 0, + levelCounts: Array.from(levelCounts.entries()).sort(([left], [right]) => left - right), + emptyOwnContentSteps, + emptyBranchSteps, + leafEmptySteps, + denseSteps, + deepestSteps, + }; + }, [workflowOptionGapDetails, workflowStepAnalyses]); const canPasteCopiedWorkflowStepAsRoot = Boolean( copiedWorkflowStep && getWorkflowStepSubtreeDepth(copiedWorkflowStep) <= AGENT_WORKFLOW_MAX_TREE_DEPTH @@ -3622,17 +3933,18 @@ export function AgentWorkflowSettingsClient({ return true; } - return [candidate.label, candidate.stepTitle, candidate.key].some((value) => - value.toLowerCase().includes(normalizedBlockedSearchValue) - ); + const candidateStepAnalysis = workflowStepAnalysisLookup.get(candidate.stepKey); + return [ + candidate.label, + candidate.stepTitle, + candidate.key, + candidateStepAnalysis?.path || "", + candidateStepAnalysis?.hasOwnContent ? "模块有内容" : "模块空内容", + candidate.hasContent ? "卡片有内容" : "卡片空内容", + candidate.hasPrompt ? "有prompt" : "空prompt", + ].some((value) => value.toLowerCase().includes(normalizedBlockedSearchValue)); }) : []; - const visibleBlockedCandidates = filteredBlockedCandidates.slice( - 0, - WORKFLOW_BLOCKED_CANDIDATE_RENDER_LIMIT - ); - const hiddenBlockedCandidateCount = - filteredBlockedCandidates.length - visibleBlockedCandidates.length; const canMoveUp = depth === 1 && indexPath[0] > 1; const canMoveDown = depth === 1 && indexPath[0] < (step.options || []).length; @@ -3902,6 +4214,9 @@ export function AgentWorkflowSettingsClient({
候选卡片列表 + {blockedCandidatePanelExpanded + ? ` · 已展示 ${filteredBlockedCandidates.length}/${blockedCandidates.length}` + : ""}
-
+
{filteredBlockedCandidates.length > 0 ? ( <> - {visibleBlockedCandidates.map((candidate) => { + {filteredBlockedCandidates.map((candidate) => { const active = (option.blockedOptionKeys || []).includes(candidate.key); + const candidateStepAnalysis = workflowStepAnalysisLookup.get( + candidate.stepKey + ); return ( ); })} - {hiddenBlockedCandidateCount > 0 ? ( -
- 还有 {hiddenBlockedCandidateCount} 个候选未显示,请输入关键词缩小范围 -
- ) : null} ) : (
@@ -4038,6 +4370,223 @@ export function AgentWorkflowSettingsClient({ ); }; + const renderWorkflowDashboard = () => { + const completionRate = workflowDashboardMetrics.promptCompletionRate; + const priorityEmptySteps = + workflowDashboardMetrics.emptyBranchSteps.length > 0 + ? workflowDashboardMetrics.emptyBranchSteps + : workflowDashboardMetrics.leafEmptySteps.length > 0 + ? workflowDashboardMetrics.leafEmptySteps + : workflowDashboardMetrics.emptyOwnContentSteps; + const dashboardCards = [ + { + label: "模块总数", + value: workflowDashboardMetrics.totalSteps, + hint: `最大 ${workflowDashboardMetrics.maxDepth || 0} 级`, + }, + { + label: "卡片总数", + value: workflowDashboardMetrics.totalCards, + hint: `${workflowDashboardMetrics.promptCards} 张已有 Prompt`, + }, + { + label: "空缺卡片", + value: workflowDashboardMetrics.missingCardDetails.length, + hint: `${workflowDashboardMetrics.fullyEmptyCards} 张完全空内容`, + }, + { + label: "空内容模块", + value: workflowDashboardMetrics.emptyOwnContentSteps.length, + hint: `${workflowDashboardMetrics.emptyBranchSteps.length} 个整支为空`, + }, + { + label: "互斥覆盖", + value: workflowDashboardMetrics.mutualExclusionCards, + hint: `${workflowDashboardMetrics.disabledCards} 张禁用/开发中`, + }, + ]; + + return ( + + + +
+
+ + + 工作流配置体检仪表盘 + + + 自动分析模块层级、卡片内容、Prompt 完成度和空模块,帮助你从蜘蛛网里找到下一步要补的地方。 + +
+ 0 ? "outline" : "secondary"}> + {workflowDashboardMetrics.emptyBranchSteps.length > 0 + ? `${workflowDashboardMetrics.emptyBranchSteps.length} 个分支完全空` + : "没有完全空分支"} + +
+
+ +
+ {dashboardCards.map((item) => ( +
+
{item.label}
+
{item.value}
+
{item.hint}
+
+ ))} +
+ +
+
+
+
+
Prompt 完成率
+
+ 以所有层级卡片是否填写 Prompt 片段统计。 +
+
+
{completionRate}%
+
+
+
+
+
+ {workflowDashboardMetrics.levelCounts.length > 0 ? ( + workflowDashboardMetrics.levelCounts.map(([level, count]) => ( + + {getWorkflowLevelLabel(level, "模块")}:{count} + + )) + ) : ( + 暂无模块层级数据 + )} +
+
+ +
+
+
+
优先补内容的模块
+
+ 优先显示整支为空或本级没有 Prompt/图片/说明的模块。 +
+
+ {priorityEmptySteps.length} 个待看 +
+ {priorityEmptySteps.length > 0 ? ( +
+ {priorityEmptySteps.slice(0, 8).map((item) => ( +
+
{item.path}
+
+ {getWorkflowLevelLabel(item.depth, "模块")} + + {item.hasSubtreeContent ? "子级有内容" : "整支空内容"} + + + {item.hasOwnContent ? "本级有内容" : "本级空内容"} + + {item.optionStats.total} 张卡片 +
+
+ ))} +
+ ) : ( +
+ 当前模块内容完整度不错,可以继续检查互斥关系和禁用状态。 +
+ )} +
+
+ +
+
+
最复杂分支
+
+ {workflowDashboardMetrics.denseSteps.map((item) => ( +
+ {item.path} + + {item.subtreeStepCount} 模块 / {item.subtreeOptionCount} 卡片 + +
+ ))} +
+
+
+
最深路径
+
+ {workflowDashboardMetrics.deepestSteps.map((item) => ( +
+ {item.path} + + {getWorkflowLevelLabel(item.depth, "模块")} + +
+ ))} +
+
+
+ +
+
+
+
空缺卡片明细
+
+ 按缺失项数量排序,直接标出每张卡片缺名称、Prompt、说明或预览图。 +
+
+
+ + {workflowDashboardMetrics.missingCardDetails.length} 张有空缺 + + + {workflowDashboardMetrics.missingPromptCards} 张缺 Prompt + + + {workflowDashboardMetrics.fullyEmptyCards} 张完全空内容 + +
+
+ {workflowDashboardMetrics.missingCardDetails.length > 0 ? ( +
+ {workflowDashboardMetrics.missingCardDetails.map((item) => ( +
+
+ {item.stepPath} / {item.path} +
+
+ {getWorkflowLevelLabel(item.depth, "卡片")} + + {item.disabled ? "开发中" : "已启用"} + + {item.childCount} 子卡片 + {item.missingFields.map((field) => ( + + 缺{field} + + ))} +
+
+ ))} +
+ ) : ( +
+ 所有卡片的名称、Prompt、说明和预览图都已补齐。 +
+ )} +
+ + + + ); + }; + const renderWorkflowStepDropZone = ( targetStepKey: string | null, position: WorkflowStepDropPosition, @@ -6069,6 +6618,8 @@ export function AgentWorkflowSettingsClient({ variants={containerVariants} className="space-y-4" > + {renderWorkflowDashboard()} +