feat(agent): add execution-plan skeleton workflow
This commit is contained in:
@@ -37,6 +37,11 @@ type AgentNodeData = {
|
||||
templateId?: string;
|
||||
canvasId?: string;
|
||||
modelId?: string;
|
||||
executionSteps?: Array<{ stepIndex?: number; stepTotal?: number }>;
|
||||
executionStepIndex?: number;
|
||||
executionStepTotal?: number;
|
||||
_executionStepIndex?: number;
|
||||
_executionStepTotal?: number;
|
||||
clarificationQuestions?: AgentClarificationQuestion[];
|
||||
clarificationAnswers?: AgentClarificationAnswerMap | Array<{ id: string; value: string }>;
|
||||
_status?: string;
|
||||
@@ -197,6 +202,51 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
const resolvedModelId = selectedModel?.id ?? DEFAULT_AGENT_MODEL_ID;
|
||||
const creditCost = selectedModel?.creditCost ?? 0;
|
||||
const clarificationQuestions = nodeData.clarificationQuestions ?? [];
|
||||
const isExecutionActive = nodeData._status === "analyzing" || nodeData._status === "executing";
|
||||
const executionProgressLine = useMemo(() => {
|
||||
if (nodeData._status !== "executing") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const statusMessage = typeof nodeData._statusMessage === "string" ? nodeData._statusMessage.trim() : "";
|
||||
if (statusMessage.length > 0) {
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
const totalFromSteps = Array.isArray(nodeData.executionSteps) ? nodeData.executionSteps.length : 0;
|
||||
const stepIndexCandidate =
|
||||
typeof nodeData.executionStepIndex === "number"
|
||||
? nodeData.executionStepIndex
|
||||
: nodeData._executionStepIndex;
|
||||
const stepTotalCandidate =
|
||||
typeof nodeData.executionStepTotal === "number"
|
||||
? nodeData.executionStepTotal
|
||||
: nodeData._executionStepTotal;
|
||||
const hasExecutionNumbers =
|
||||
typeof stepIndexCandidate === "number" &&
|
||||
Number.isFinite(stepIndexCandidate) &&
|
||||
typeof stepTotalCandidate === "number" &&
|
||||
Number.isFinite(stepTotalCandidate) &&
|
||||
stepTotalCandidate > 0;
|
||||
|
||||
if (hasExecutionNumbers) {
|
||||
return `Executing step ${Math.max(0, Math.floor(stepIndexCandidate)) + 1}/${Math.floor(stepTotalCandidate)}`;
|
||||
}
|
||||
|
||||
if (totalFromSteps > 0) {
|
||||
return `Executing planned outputs (${totalFromSteps} total)`;
|
||||
}
|
||||
|
||||
return "Executing planned outputs";
|
||||
}, [
|
||||
nodeData._executionStepIndex,
|
||||
nodeData._executionStepTotal,
|
||||
nodeData._status,
|
||||
nodeData._statusMessage,
|
||||
nodeData.executionStepIndex,
|
||||
nodeData.executionStepTotal,
|
||||
nodeData.executionSteps,
|
||||
]);
|
||||
|
||||
const persistNodeData = useCallback(
|
||||
(patch: Partial<AgentNodeData>) => {
|
||||
@@ -239,6 +289,10 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
);
|
||||
|
||||
const handleRunAgent = useCallback(async () => {
|
||||
if (isExecutionActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.isOffline) {
|
||||
toast.warning(
|
||||
"Offline aktuell nicht unterstuetzt",
|
||||
@@ -257,7 +311,7 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
nodeId: id as Id<"nodes">,
|
||||
modelId: resolvedModelId,
|
||||
});
|
||||
}, [nodeData.canvasId, id, resolvedModelId, runAgent, status.isOffline]);
|
||||
}, [isExecutionActive, nodeData.canvasId, id, resolvedModelId, runAgent, status.isOffline]);
|
||||
|
||||
const handleSubmitClarification = useCallback(async () => {
|
||||
if (status.isOffline) {
|
||||
@@ -298,6 +352,11 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
id="agent-in"
|
||||
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!h-3 !w-3 !bg-amber-500 !border-2 !border-background"
|
||||
/>
|
||||
|
||||
<div className="flex h-full flex-col gap-3 p-3">
|
||||
<header className="space-y-1">
|
||||
@@ -334,10 +393,15 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRunAgent()}
|
||||
className="nodrag w-full rounded-md bg-amber-600 px-3 py-2 text-sm font-medium text-white hover:bg-amber-700"
|
||||
disabled={isExecutionActive}
|
||||
aria-busy={isExecutionActive}
|
||||
className="nodrag w-full rounded-md bg-amber-600 px-3 py-2 text-sm font-medium text-white hover:bg-amber-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Run agent
|
||||
</button>
|
||||
{executionProgressLine ? (
|
||||
<p className="text-[11px] text-amber-800/90 dark:text-amber-200/90">{executionProgressLine}</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{clarificationQuestions.length > 0 ? (
|
||||
|
||||
@@ -5,6 +5,10 @@ import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
|
||||
type AgentOutputNodeData = {
|
||||
isSkeleton?: boolean;
|
||||
stepId?: string;
|
||||
stepIndex?: number;
|
||||
stepTotal?: number;
|
||||
title?: string;
|
||||
channel?: string;
|
||||
outputType?: string;
|
||||
@@ -17,6 +21,25 @@ type AgentOutputNodeType = Node<AgentOutputNodeData, "agent-output">;
|
||||
|
||||
export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutputNodeType>) {
|
||||
const nodeData = data as AgentOutputNodeData;
|
||||
const isSkeleton = nodeData.isSkeleton === true;
|
||||
const hasStepCounter =
|
||||
typeof nodeData.stepIndex === "number" &&
|
||||
Number.isFinite(nodeData.stepIndex) &&
|
||||
typeof nodeData.stepTotal === "number" &&
|
||||
Number.isFinite(nodeData.stepTotal) &&
|
||||
nodeData.stepTotal > 0;
|
||||
const safeStepIndex =
|
||||
typeof nodeData.stepIndex === "number" && Number.isFinite(nodeData.stepIndex)
|
||||
? Math.max(0, Math.floor(nodeData.stepIndex))
|
||||
: 0;
|
||||
const safeStepTotal =
|
||||
typeof nodeData.stepTotal === "number" && Number.isFinite(nodeData.stepTotal)
|
||||
? Math.max(1, Math.floor(nodeData.stepTotal))
|
||||
: 1;
|
||||
const stepCounter = hasStepCounter
|
||||
? `${safeStepIndex + 1}/${safeStepTotal}`
|
||||
: null;
|
||||
const resolvedTitle = nodeData.title ?? (isSkeleton ? "Planned output" : "Agent output");
|
||||
|
||||
return (
|
||||
<BaseNodeWrapper
|
||||
@@ -24,7 +47,7 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
||||
selected={selected}
|
||||
status={nodeData._status}
|
||||
statusMessage={nodeData._statusMessage}
|
||||
className="min-w-[300px] border-amber-500/30"
|
||||
className={`min-w-[300px] border-amber-500/30 ${isSkeleton ? "opacity-80" : ""}`}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
@@ -35,9 +58,22 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
||||
|
||||
<div className="flex h-full flex-col gap-3 p-3">
|
||||
<header className="space-y-1">
|
||||
<p className="truncate text-xs font-semibold text-foreground" title={nodeData.title}>
|
||||
{nodeData.title ?? "Agent output"}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-xs font-semibold text-foreground" title={resolvedTitle}>
|
||||
{resolvedTitle}
|
||||
</p>
|
||||
{isSkeleton ? (
|
||||
<span className="shrink-0 rounded-full border border-amber-500/50 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-800 dark:text-amber-200">
|
||||
Skeleton
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{isSkeleton ? (
|
||||
<p className="text-[11px] text-amber-700/90 dark:text-amber-300/90">
|
||||
Planned output{stepCounter ? ` - ${stepCounter}` : ""}
|
||||
{nodeData.stepId ? ` - ${nodeData.stepId}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<section className="space-y-1">
|
||||
@@ -62,9 +98,18 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Body
|
||||
</p>
|
||||
<p className="line-clamp-6 whitespace-pre-wrap text-xs text-foreground/90">
|
||||
{nodeData.body ?? ""}
|
||||
</p>
|
||||
{isSkeleton ? (
|
||||
<div
|
||||
data-testid="agent-output-skeleton-body"
|
||||
className="animate-pulse rounded-md border border-dashed border-amber-500/40 bg-gradient-to-r from-amber-500/10 via-amber-500/20 to-amber-500/10 p-3"
|
||||
>
|
||||
<p className="text-[11px] text-amber-800/90 dark:text-amber-200/90">Planned content</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="line-clamp-6 whitespace-pre-wrap text-xs text-foreground/90">
|
||||
{nodeData.body ?? ""}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</BaseNodeWrapper>
|
||||
|
||||
Reference in New Issue
Block a user