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_DRAFT_STORAGE_KEY = "agent-workflow-settings-draft:v1";
|
||||||
const WORKFLOW_SETTINGS_STALE_DRAFT_STORAGE_KEY = "agent-workflow-settings-stale-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_SETTINGS_DRAFT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
const WORKFLOW_BLOCKED_CANDIDATE_RENDER_LIMIT = 60;
|
|
||||||
|
|
||||||
type WorkflowStepDropPosition = "before" | "after" | "inside";
|
type WorkflowStepDropPosition = "before" | "after" | "inside";
|
||||||
|
|
||||||
@ -187,6 +186,42 @@ interface AgentWorkflowSettingsDraft {
|
|||||||
state: AgentWorkflowSettingsDraftState;
|
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<
|
type BufferedInputProps = Omit<
|
||||||
ComponentProps<typeof Input>,
|
ComponentProps<typeof Input>,
|
||||||
"defaultValue" | "onChange" | "value"
|
"defaultValue" | "onChange" | "value"
|
||||||
@ -513,6 +548,191 @@ function collectWorkflowOptionKeys(options: AgentWorkflowOptionUiConfig[]): stri
|
|||||||
return flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key);
|
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(
|
function collectWorkflowOptionRelations(
|
||||||
options: AgentWorkflowOptionUiConfig[],
|
options: AgentWorkflowOptionUiConfig[],
|
||||||
ancestors: string[] = []
|
ancestors: string[] = []
|
||||||
@ -520,6 +740,13 @@ function collectWorkflowOptionRelations(
|
|||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
|
childCount: number;
|
||||||
|
hasPrompt: boolean;
|
||||||
|
hasDescription: boolean;
|
||||||
|
hasPreview: boolean;
|
||||||
|
hasContent: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
blockedCount: number;
|
||||||
}> {
|
}> {
|
||||||
return options.flatMap((option) => {
|
return options.flatMap((option) => {
|
||||||
const currentPath = [...ancestors, option.buttonName || option.key];
|
const currentPath = [...ancestors, option.buttonName || option.key];
|
||||||
@ -528,6 +755,13 @@ function collectWorkflowOptionRelations(
|
|||||||
key: option.key,
|
key: option.key,
|
||||||
label: currentPath.join(" / "),
|
label: currentPath.join(" / "),
|
||||||
depth: currentPath.length,
|
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),
|
...collectWorkflowOptionRelations(option.children || [], currentPath),
|
||||||
];
|
];
|
||||||
@ -1848,6 +2082,83 @@ export function AgentWorkflowSettingsClient({
|
|||||||
() => new Map(workflowOptionRelations.map((item) => [item.key, item] as const)),
|
() => new Map(workflowOptionRelations.map((item) => [item.key, item] as const)),
|
||||||
[workflowOptionRelations]
|
[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(
|
const canPasteCopiedWorkflowStepAsRoot = Boolean(
|
||||||
copiedWorkflowStep &&
|
copiedWorkflowStep &&
|
||||||
getWorkflowStepSubtreeDepth(copiedWorkflowStep) <= AGENT_WORKFLOW_MAX_TREE_DEPTH
|
getWorkflowStepSubtreeDepth(copiedWorkflowStep) <= AGENT_WORKFLOW_MAX_TREE_DEPTH
|
||||||
@ -3622,17 +3933,18 @@ export function AgentWorkflowSettingsClient({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [candidate.label, candidate.stepTitle, candidate.key].some((value) =>
|
const candidateStepAnalysis = workflowStepAnalysisLookup.get(candidate.stepKey);
|
||||||
value.toLowerCase().includes(normalizedBlockedSearchValue)
|
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 canMoveUp = depth === 1 && indexPath[0] > 1;
|
||||||
const canMoveDown = depth === 1 && indexPath[0] < (step.options || []).length;
|
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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
候选卡片列表
|
候选卡片列表
|
||||||
|
{blockedCandidatePanelExpanded
|
||||||
|
? ` · 已展示 ${filteredBlockedCandidates.length}/${blockedCandidates.length}`
|
||||||
|
: ""}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@ -3934,11 +4249,14 @@ export function AgentWorkflowSettingsClient({
|
|||||||
className="h-8 pl-8 text-xs"
|
className="h-8 pl-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 ? (
|
{filteredBlockedCandidates.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{visibleBlockedCandidates.map((candidate) => {
|
{filteredBlockedCandidates.map((candidate) => {
|
||||||
const active = (option.blockedOptionKeys || []).includes(candidate.key);
|
const active = (option.blockedOptionKeys || []).includes(candidate.key);
|
||||||
|
const candidateStepAnalysis = workflowStepAnalysisLookup.get(
|
||||||
|
candidate.stepKey
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={candidate.key}
|
key={candidate.key}
|
||||||
@ -3966,25 +4284,39 @@ export function AgentWorkflowSettingsClient({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0 flex-1 space-y-1 pr-3">
|
||||||
<span className="block truncate font-medium">
|
<span className="block whitespace-normal break-words font-medium leading-5">
|
||||||
{candidate.label}
|
{candidate.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-[10px] text-muted-foreground">
|
<span className="block whitespace-normal break-words text-[10px] leading-4 text-muted-foreground">
|
||||||
{getWorkflowLevelLabel(candidate.depth, "卡片")} · {candidate.key}
|
所属模块:
|
||||||
|
{candidateStepAnalysis?.path || candidate.stepTitle} ·{" "}
|
||||||
|
{getWorkflowLevelLabel(candidate.depth, "卡片")} ·{" "}
|
||||||
|
{candidate.key}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<Badge variant={active ? "default" : "outline"}>
|
<span className="flex shrink-0 flex-wrap justify-end gap-1">
|
||||||
{active ? "已限制" : "可限制"}
|
<Badge variant={active ? "default" : "outline"}>
|
||||||
</Badge>
|
{active ? "已限制" : "可限制"}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
candidateStepAnalysis?.hasOwnContent
|
||||||
|
? "secondary"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{candidateStepAnalysis?.hasOwnContent
|
||||||
|
? "模块有内容"
|
||||||
|
: "模块空内容"}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={candidate.hasContent ? "secondary" : "outline"}>
|
||||||
|
{candidate.hasContent ? "卡片有内容" : "卡片空内容"}
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
</button>
|
</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">
|
<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 = (
|
const renderWorkflowStepDropZone = (
|
||||||
targetStepKey: string | null,
|
targetStepKey: string | null,
|
||||||
position: WorkflowStepDropPosition,
|
position: WorkflowStepDropPosition,
|
||||||
@ -6069,6 +6618,8 @@ export function AgentWorkflowSettingsClient({
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
{renderWorkflowDashboard()}
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"
|
className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user