feat: add workflow configuration dashboard
This commit is contained in:
parent
1103ff957f
commit
aaba40b2d6
@ -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<typeof Input>,
|
||||
"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<WorkflowOptionContentSummary>(
|
||||
(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<number, number>();
|
||||
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({
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
候选卡片列表
|
||||
{blockedCandidatePanelExpanded
|
||||
? ` · 已展示 ${filteredBlockedCandidates.length}/${blockedCandidates.length}`
|
||||
: ""}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
@ -3934,11 +4249,14 @@ export function AgentWorkflowSettingsClient({
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-44 space-y-2 overflow-y-auto">
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto">
|
||||
{filteredBlockedCandidates.length > 0 ? (
|
||||
<>
|
||||
{visibleBlockedCandidates.map((candidate) => {
|
||||
{filteredBlockedCandidates.map((candidate) => {
|
||||
const active = (option.blockedOptionKeys || []).includes(candidate.key);
|
||||
const candidateStepAnalysis = workflowStepAnalysisLookup.get(
|
||||
candidate.stepKey
|
||||
);
|
||||
return (
|
||||
<button
|
||||
key={candidate.key}
|
||||
@ -3966,25 +4284,39 @@ export function AgentWorkflowSettingsClient({
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-medium">
|
||||
<span className="min-w-0 flex-1 space-y-1 pr-3">
|
||||
<span className="block whitespace-normal break-words font-medium leading-5">
|
||||
{candidate.label}
|
||||
</span>
|
||||
<span className="block text-[10px] text-muted-foreground">
|
||||
{getWorkflowLevelLabel(candidate.depth, "卡片")} · {candidate.key}
|
||||
<span className="block whitespace-normal break-words text-[10px] leading-4 text-muted-foreground">
|
||||
所属模块:
|
||||
{candidateStepAnalysis?.path || candidate.stepTitle} ·{" "}
|
||||
{getWorkflowLevelLabel(candidate.depth, "卡片")} ·{" "}
|
||||
{candidate.key}
|
||||
</span>
|
||||
</span>
|
||||
<Badge variant={active ? "default" : "outline"}>
|
||||
{active ? "已限制" : "可限制"}
|
||||
</Badge>
|
||||
<span className="flex shrink-0 flex-wrap justify-end gap-1">
|
||||
<Badge variant={active ? "default" : "outline"}>
|
||||
{active ? "已限制" : "可限制"}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={
|
||||
candidateStepAnalysis?.hasOwnContent
|
||||
? "secondary"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{candidateStepAnalysis?.hasOwnContent
|
||||
? "模块有内容"
|
||||
: "模块空内容"}
|
||||
</Badge>
|
||||
<Badge variant={candidate.hasContent ? "secondary" : "outline"}>
|
||||
{candidate.hasContent ? "卡片有内容" : "卡片空内容"}
|
||||
</Badge>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{hiddenBlockedCandidateCount > 0 ? (
|
||||
<div className="rounded-md border border-dashed bg-muted/40 px-3 py-2 text-center text-[11px] text-muted-foreground">
|
||||
还有 {hiddenBlockedCandidateCount} 个候选未显示,请输入关键词缩小范围
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
||||
@ -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 (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="overflow-hidden border-emerald-500/20 bg-gradient-to-br from-emerald-500/5 via-background to-sky-500/5">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Sparkles className="h-4 w-4 text-emerald-600" />
|
||||
工作流配置体检仪表盘
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
自动分析模块层级、卡片内容、Prompt 完成度和空模块,帮助你从蜘蛛网里找到下一步要补的地方。
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant={workflowDashboardMetrics.emptyBranchSteps.length > 0 ? "outline" : "secondary"}>
|
||||
{workflowDashboardMetrics.emptyBranchSteps.length > 0
|
||||
? `${workflowDashboardMetrics.emptyBranchSteps.length} 个分支完全空`
|
||||
: "没有完全空分支"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<div className="grid gap-3 md:grid-cols-5">
|
||||
{dashboardCards.map((item) => (
|
||||
<div key={item.label} className="rounded-xl border bg-background/80 p-4 shadow-sm">
|
||||
<div className="text-xs text-muted-foreground">{item.label}</div>
|
||||
<div className="mt-2 text-2xl font-semibold">{item.value}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{item.hint}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[1.05fr_1fr]">
|
||||
<div className="space-y-4 rounded-xl border bg-background/80 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Prompt 完成率</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
以所有层级卡片是否填写 Prompt 片段统计。
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-semibold text-emerald-600">{completionRate}%</div>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-emerald-500 transition-all"
|
||||
style={{ width: `${completionRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{workflowDashboardMetrics.levelCounts.length > 0 ? (
|
||||
workflowDashboardMetrics.levelCounts.map(([level, count]) => (
|
||||
<Badge key={level} variant="outline">
|
||||
{getWorkflowLevelLabel(level, "模块")}:{count}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">暂无模块层级数据</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-xl border bg-background/80 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">优先补内容的模块</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
优先显示整支为空或本级没有 Prompt/图片/说明的模块。
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">{priorityEmptySteps.length} 个待看</Badge>
|
||||
</div>
|
||||
{priorityEmptySteps.length > 0 ? (
|
||||
<div className="max-h-48 space-y-2 overflow-y-auto pr-1">
|
||||
{priorityEmptySteps.slice(0, 8).map((item) => (
|
||||
<div key={item.key} className="rounded-lg border bg-muted/20 px-3 py-2">
|
||||
<div className="break-words text-xs font-medium leading-5">{item.path}</div>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<Badge variant="outline">{getWorkflowLevelLabel(item.depth, "模块")}</Badge>
|
||||
<Badge variant={item.hasSubtreeContent ? "secondary" : "outline"}>
|
||||
{item.hasSubtreeContent ? "子级有内容" : "整支空内容"}
|
||||
</Badge>
|
||||
<Badge variant={item.hasOwnContent ? "secondary" : "outline"}>
|
||||
{item.hasOwnContent ? "本级有内容" : "本级空内容"}
|
||||
</Badge>
|
||||
<Badge variant="outline">{item.optionStats.total} 张卡片</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/20 px-3 py-6 text-center text-xs text-muted-foreground">
|
||||
当前模块内容完整度不错,可以继续检查互斥关系和禁用状态。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-xl border bg-background/80 p-4">
|
||||
<div className="mb-3 text-sm font-medium">最复杂分支</div>
|
||||
<div className="space-y-2">
|
||||
{workflowDashboardMetrics.denseSteps.map((item) => (
|
||||
<div key={item.key} className="flex items-start justify-between gap-3 text-xs">
|
||||
<span className="min-w-0 break-words leading-5">{item.path}</span>
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{item.subtreeStepCount} 模块 / {item.subtreeOptionCount} 卡片
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border bg-background/80 p-4">
|
||||
<div className="mb-3 text-sm font-medium">最深路径</div>
|
||||
<div className="space-y-2">
|
||||
{workflowDashboardMetrics.deepestSteps.map((item) => (
|
||||
<div key={item.key} className="flex items-start justify-between gap-3 text-xs">
|
||||
<span className="min-w-0 break-words leading-5">{item.path}</span>
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{getWorkflowLevelLabel(item.depth, "模块")}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-background/80 p-4">
|
||||
<div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium">空缺卡片明细</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
按缺失项数量排序,直接标出每张卡片缺名称、Prompt、说明或预览图。
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">
|
||||
{workflowDashboardMetrics.missingCardDetails.length} 张有空缺
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{workflowDashboardMetrics.missingPromptCards} 张缺 Prompt
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{workflowDashboardMetrics.fullyEmptyCards} 张完全空内容
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{workflowDashboardMetrics.missingCardDetails.length > 0 ? (
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto pr-1">
|
||||
{workflowDashboardMetrics.missingCardDetails.map((item) => (
|
||||
<div key={`${item.stepKey}:${item.key}`} className="rounded-lg border bg-muted/20 px-3 py-2">
|
||||
<div className="break-words text-xs font-medium leading-5">
|
||||
{item.stepPath} / {item.path}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<Badge variant="outline">{getWorkflowLevelLabel(item.depth, "卡片")}</Badge>
|
||||
<Badge variant={item.disabled ? "secondary" : "outline"}>
|
||||
{item.disabled ? "开发中" : "已启用"}
|
||||
</Badge>
|
||||
<Badge variant="outline">{item.childCount} 子卡片</Badge>
|
||||
{item.missingFields.map((field) => (
|
||||
<Badge key={field} variant="outline">
|
||||
缺{field}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/20 px-3 py-6 text-center text-xs text-muted-foreground">
|
||||
所有卡片的名称、Prompt、说明和预览图都已补齐。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderWorkflowStepDropZone = (
|
||||
targetStepKey: string | null,
|
||||
position: WorkflowStepDropPosition,
|
||||
@ -6069,6 +6618,8 @@ export function AgentWorkflowSettingsClient({
|
||||
variants={containerVariants}
|
||||
className="space-y-4"
|
||||
>
|
||||
{renderWorkflowDashboard()}
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user