feat(agent): add structured outputs and media archive support

This commit is contained in:
2026-04-10 19:01:04 +02:00
parent a1df097f9c
commit 9732022461
34 changed files with 3276 additions and 482 deletions

View File

@@ -28,7 +28,13 @@ const translations: Record<string, string> = {
"agentOutputNode.skeletonBadge": "Skeleton",
"agentOutputNode.plannedOutputLabel": "Planned output",
"agentOutputNode.channelLabel": "Channel",
"agentOutputNode.typeLabel": "Type",
"agentOutputNode.artifactTypeLabel": "Artifact type",
"agentOutputNode.sectionsLabel": "Sections",
"agentOutputNode.metadataLabel": "Metadata",
"agentOutputNode.qualityChecksLabel": "Quality checks",
"agentOutputNode.previewLabel": "Preview",
"agentOutputNode.previewFallback": "No preview available",
"agentOutputNode.emptyValue": "-",
"agentOutputNode.bodyLabel": "Body",
"agentOutputNode.plannedContent": "Planned content",
};
@@ -70,7 +76,7 @@ describe("AgentOutputNode", () => {
root = null;
});
it("renders title, channel, output type, and body", async () => {
it("renders structured output with artifact meta, sections, metadata, quality checks, and preview fallback", async () => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
@@ -90,8 +96,18 @@ describe("AgentOutputNode", () => {
data: {
title: "Instagram Caption",
channel: "instagram-feed",
outputType: "caption",
body: "A short punchy caption with hashtags",
artifactType: "caption-pack",
previewText: "A short punchy caption with hashtags",
sections: [
{ id: "hook", label: "Hook", content: "Launch day is here." },
{ id: "body", label: "Body", content: "Built for modern teams." },
],
metadata: {
objective: "Drive signups",
tags: ["launch", "product"],
},
qualityChecks: ["channel-fit", "cta-present"],
body: "Legacy body fallback",
_status: "done",
} as Record<string, unknown>,
positionAbsoluteX: 0,
@@ -102,10 +118,22 @@ describe("AgentOutputNode", () => {
expect(container.textContent).toContain("Instagram Caption");
expect(container.textContent).toContain("instagram-feed");
expect(container.textContent).toContain("caption");
expect(container.textContent).toContain("caption-pack");
expect(container.textContent).toContain("Sections");
expect(container.textContent).toContain("Hook");
expect(container.textContent).toContain("Launch day is here.");
expect(container.textContent).toContain("Metadata");
expect(container.textContent).toContain("objective");
expect(container.textContent).toContain("Drive signups");
expect(container.textContent).toContain("Quality checks");
expect(container.textContent).toContain("channel-fit");
expect(container.textContent).toContain("Preview");
expect(container.textContent).toContain("A short punchy caption with hashtags");
expect(container.querySelector('[data-testid="agent-output-meta-strip"]')).not.toBeNull();
expect(container.querySelector('[data-testid="agent-output-text-body"]')).not.toBeNull();
expect(container.querySelector('[data-testid="agent-output-sections"]')).not.toBeNull();
expect(container.querySelector('[data-testid="agent-output-metadata"]')).not.toBeNull();
expect(container.querySelector('[data-testid="agent-output-quality-checks"]')).not.toBeNull();
expect(container.querySelector('[data-testid="agent-output-preview"]')).not.toBeNull();
});
it("renders parseable json body in a pretty-printed code block", async () => {
@@ -128,7 +156,7 @@ describe("AgentOutputNode", () => {
data: {
title: "JSON output",
channel: "api",
outputType: "payload",
artifactType: "payload",
body: '{"post":"Hello","tags":["launch","news"]}',
_status: "done",
} as Record<string, unknown>,
@@ -145,6 +173,40 @@ describe("AgentOutputNode", () => {
expect(container.querySelector('[data-testid="agent-output-text-body"]')).toBeNull();
});
it("falls back to legacy text body when structured fields are absent", async () => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(
React.createElement(AgentOutputNode, {
id: "agent-output-legacy",
selected: false,
dragging: false,
draggable: true,
selectable: true,
deletable: true,
zIndex: 1,
isConnectable: true,
type: "agent-output",
data: {
title: "Legacy output",
channel: "linkedin",
artifactType: "post",
body: "Legacy body content",
} as Record<string, unknown>,
positionAbsoluteX: 0,
positionAbsoluteY: 0,
}),
);
});
expect(container.querySelector('[data-testid="agent-output-text-body"]')).not.toBeNull();
expect(container.textContent).toContain("Legacy body content");
expect(container.querySelector('[data-testid="agent-output-sections"]')).toBeNull();
});
it("renders input-only handle agent-output-in", async () => {
container = document.createElement("div");
document.body.appendChild(container);
@@ -165,7 +227,7 @@ describe("AgentOutputNode", () => {
data: {
title: "LinkedIn Post",
channel: "linkedin",
outputType: "post",
artifactType: "post",
body: "Body",
} as Record<string, unknown>,
positionAbsoluteX: 0,
@@ -197,7 +259,7 @@ describe("AgentOutputNode", () => {
data: {
title: "Planned headline",
channel: "linkedin",
outputType: "post",
artifactType: "post",
isSkeleton: true,
stepIndex: 1,
stepTotal: 4,