feat(agent): add structured outputs and media archive support
This commit is contained in:
@@ -68,8 +68,8 @@ Alle verfügbaren Node-Typen sind in `lib/canvas-node-catalog.ts` definiert:
|
||||
| `video-prompt` | 2 | ✅ | ai-output | source: `video-prompt-out`, target: `video-prompt-in` |
|
||||
| `ai-text` | 2 | 🔲 | ai-output | source: `text-out`, target: `text-in` |
|
||||
| `ai-video` | 2 | ✅ (systemOutput) | ai-output | source: `video-out`, target: `video-in` |
|
||||
| `agent` | 2 | ✅ | control | target: `agent-in` (input-only MVP) |
|
||||
| `agent-output` | 3 | 🔲 | ai-output | systemOutput: true |
|
||||
| `agent` | 2 | ✅ | control | target: `agent-in`, source (default) |
|
||||
| `agent-output` | 2 | ✅ (systemOutput) | ai-output | target: `agent-output-in` |
|
||||
| `crop` | 2 | 🔲 | transform | 🔲 |
|
||||
| `bg-remove` | 2 | 🔲 | transform | 🔲 |
|
||||
| `upscale` | 2 | 🔲 | transform | 🔲 |
|
||||
@@ -215,7 +215,17 @@ Im **Light Mode** wird der eigentliche Edge-`stroke` ebenfalls aus dieser Akzent
|
||||
| `ai-image-node.tsx` | KI-Bild-Output-Node mit Bildvorschau, Metadaten, Retry |
|
||||
| `video-prompt-node.tsx` | KI-Video-Steuer-Node mit Modell-/Dauer-Selector, Credit-Anzeige, Generate-Button |
|
||||
| `ai-video-node.tsx` | KI-Video-Output-Node mit Video-Player, Metadaten, Retry-Button |
|
||||
| `agent-node.tsx` | Statischer Agent-Input-Node (Campaign Distributor) mit Kanal-/Input-/Output-Metadaten |
|
||||
| `agent-node.tsx` | Definitionsgetriebener Agent-Node mit Briefing, Constraints, Model-Auswahl, Run/Resume und Clarification-Flow |
|
||||
| `agent-output-node.tsx` | Agent-Ausgabe-Node fuer Skeletons plus strukturierte Deliverables (`sections`, `metadata`, `qualityChecks`, `previewText`) mit `body`-Fallback |
|
||||
|
||||
---
|
||||
|
||||
## Agent Runtime Nodes (aktuell)
|
||||
|
||||
- `agent-node.tsx` liest Template-Metadaten ueber `getAgentTemplate(...)` (projektiert aus `lib/agent-definitions.ts`).
|
||||
- Node-Daten enthalten `briefConstraints`, `clarificationQuestions`, `clarificationAnswers`, `executionSteps` und Laufstatus.
|
||||
- Run startet `api.agents.runAgent`, Clarification-Submit nutzt `api.agents.resumeAgent`.
|
||||
- `agent-output-node.tsx` rendert strukturierte Outputs bevorzugt (Sections/Metadata/Quality Checks/Preview) und faellt auf JSON oder Plain-Text-`body` zurueck.
|
||||
|
||||
---
|
||||
|
||||
@@ -281,6 +291,6 @@ useCanvasData (use-canvas-data.ts)
|
||||
- **Optimistic IDs:** Temporäre Nodes/Edges erhalten IDs mit `optimistic_` / `optimistic_edge_`-Prefix, werden durch echte Convex-IDs ersetzt, sobald die Mutation abgeschlossen ist.
|
||||
- **Node-Taxonomie:** Alle Node-Typen sind in `lib/canvas-node-catalog.ts` definiert. Phase-2/3 Nodes haben `implemented: false` und `disabledHint`.
|
||||
- **Video-Connection-Policy:** `video-prompt` darf **nur** mit `ai-video` verbunden werden (und umgekehrt). `text → video-prompt` ist erlaubt (Prompt-Quelle). `ai-video → compare` ist erlaubt.
|
||||
- **Agent-MVP:** `agent` ist aktuell input-only (`agent-in`), ohne ausgehenden Handle. Er akzeptiert nur Content-/Kontext-Quellen (z. B. `render`, `compare`, `text`, `image`), keine Prompt-Steuerknoten.
|
||||
- **Agent-Flow:** `agent` akzeptiert nur Content-/Kontext-Quellen (z. B. `render`, `compare`, `text`, `image`) als Input; ausgehende Kanten sind fuer `agent -> agent-output` vorgesehen.
|
||||
- **Convex Generated Types:** `api.ai.generateVideo` wird u. U. nicht in `convex/_generated/api.d.ts` exportiert. Der Code verwendet `api as unknown as {...}` als Workaround. Ein `npx convex dev`-Zyklus würde die Typen korrekt generieren.
|
||||
- **Canvas Graph Query:** Der Canvas nutzt `canvasGraph.get` (aus `convex/canvasGraph.ts`) statt separater `nodes.list`/`edges.list` Queries. Optimistic Updates laufen über `canvas-graph-query-cache.ts`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Bot } from "lucide-react";
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useAction } from "convex/react";
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
DEFAULT_AGENT_MODEL_ID,
|
||||
getAgentModel,
|
||||
getAvailableAgentModels,
|
||||
type AgentModelId,
|
||||
} from "@/lib/agent-models";
|
||||
import {
|
||||
type AgentClarificationAnswerMap,
|
||||
@@ -88,11 +87,16 @@ function useSafeSubscription() {
|
||||
}
|
||||
}
|
||||
|
||||
function useSafeAction(reference: FunctionReference<"action", "public", any, unknown>) {
|
||||
function useSafeAction<Args extends Record<string, unknown>, Output>(
|
||||
reference: FunctionReference<"action", "public", Args, Output>,
|
||||
) {
|
||||
try {
|
||||
return useAction(reference);
|
||||
} catch {
|
||||
return async (_args: any) => undefined;
|
||||
return async (args: Args): Promise<Output | undefined> => {
|
||||
void args;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +187,10 @@ function CompactList({ items }: { items: readonly string[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function toTemplateTranslationKey(templateId: string): string {
|
||||
return templateId.replace(/-([a-z])/g, (_match, letter: string) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeType>) {
|
||||
const t = useTranslations("agentNode");
|
||||
const locale = useLocale();
|
||||
@@ -195,13 +203,35 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
const userTier = normalizePublicTier(subscription?.tier ?? "free");
|
||||
|
||||
const availableModels = useMemo(() => getAvailableAgentModels(userTier), [userTier]);
|
||||
const [modelId, setModelId] = useState(nodeData.modelId ?? DEFAULT_AGENT_MODEL_ID);
|
||||
const [clarificationAnswers, setClarificationAnswers] = useState<AgentClarificationAnswerMap>(
|
||||
normalizeClarificationAnswers(nodeData.clarificationAnswers),
|
||||
const clarificationAnswersFromNode = useMemo(
|
||||
() => normalizeClarificationAnswers(nodeData.clarificationAnswers),
|
||||
[nodeData.clarificationAnswers],
|
||||
);
|
||||
const [briefConstraints, setBriefConstraints] = useState<AgentBriefConstraints>(
|
||||
normalizeBriefConstraints(nodeData.briefConstraints),
|
||||
const briefConstraintsFromNode = useMemo(
|
||||
() => normalizeBriefConstraints(nodeData.briefConstraints),
|
||||
[nodeData.briefConstraints],
|
||||
);
|
||||
const nodeModelId =
|
||||
typeof nodeData.modelId === "string" && nodeData.modelId.trim().length > 0
|
||||
? nodeData.modelId
|
||||
: DEFAULT_AGENT_MODEL_ID;
|
||||
const [modelDraftId, setModelDraftId] = useState<string | null>(null);
|
||||
const [clarificationAnswersDraft, setClarificationAnswersDraft] =
|
||||
useState<AgentClarificationAnswerMap | null>(null);
|
||||
const [briefConstraintsDraft, setBriefConstraintsDraft] =
|
||||
useState<AgentBriefConstraints | null>(null);
|
||||
|
||||
const modelId = modelDraftId === nodeModelId ? nodeModelId : modelDraftId ?? nodeModelId;
|
||||
const clarificationAnswers =
|
||||
clarificationAnswersDraft &&
|
||||
areAnswerMapsEqual(clarificationAnswersDraft, clarificationAnswersFromNode)
|
||||
? clarificationAnswersFromNode
|
||||
: clarificationAnswersDraft ?? clarificationAnswersFromNode;
|
||||
const briefConstraints =
|
||||
briefConstraintsDraft &&
|
||||
areBriefConstraintsEqual(briefConstraintsDraft, briefConstraintsFromNode)
|
||||
? briefConstraintsFromNode
|
||||
: briefConstraintsDraft ?? briefConstraintsFromNode;
|
||||
|
||||
const agentActionsApi = api as unknown as {
|
||||
agents: {
|
||||
@@ -234,57 +264,30 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
const resumeAgent = useSafeAction(agentActionsApi.agents.resumeAgent);
|
||||
const normalizedLocale = locale === "en" ? "en" : "de";
|
||||
|
||||
useEffect(() => {
|
||||
setModelId(nodeData.modelId ?? DEFAULT_AGENT_MODEL_ID);
|
||||
}, [nodeData.modelId]);
|
||||
|
||||
useEffect(() => {
|
||||
const normalized = normalizeClarificationAnswers(nodeData.clarificationAnswers);
|
||||
setClarificationAnswers((current) => {
|
||||
if (areAnswerMapsEqual(current, normalized)) {
|
||||
return current;
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
}, [nodeData.clarificationAnswers]);
|
||||
|
||||
useEffect(() => {
|
||||
const normalized = normalizeBriefConstraints(nodeData.briefConstraints);
|
||||
setBriefConstraints((current) => {
|
||||
if (areBriefConstraintsEqual(current, normalized)) {
|
||||
return current;
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
}, [nodeData.briefConstraints]);
|
||||
|
||||
useEffect(() => {
|
||||
if (availableModels.length === 0) {
|
||||
return;
|
||||
}
|
||||
const resolvedModelId = useMemo(() => {
|
||||
if (availableModels.some((model) => model.id === modelId)) {
|
||||
return;
|
||||
return modelId;
|
||||
}
|
||||
|
||||
const nextModelId = availableModels[0]!.id;
|
||||
setModelId(nextModelId);
|
||||
return availableModels[0]?.id ?? DEFAULT_AGENT_MODEL_ID;
|
||||
}, [availableModels, modelId]);
|
||||
|
||||
const selectedModel =
|
||||
getAgentModel(modelId) ??
|
||||
getAgentModel(resolvedModelId) ??
|
||||
availableModels[0] ??
|
||||
getAgentModel(DEFAULT_AGENT_MODEL_ID);
|
||||
const resolvedModelId = selectedModel?.id ?? DEFAULT_AGENT_MODEL_ID;
|
||||
const creditCost = selectedModel?.creditCost ?? 0;
|
||||
const clarificationQuestions = nodeData.clarificationQuestions ?? [];
|
||||
const templateTranslationKey = `templates.${toTemplateTranslationKey(template?.id ?? DEFAULT_AGENT_TEMPLATE_ID)}`;
|
||||
const translatedTemplateName = t(`${templateTranslationKey}.name`);
|
||||
const translatedTemplateDescription = t(`${templateTranslationKey}.description`);
|
||||
const templateName =
|
||||
template?.id === "campaign-distributor"
|
||||
? t("templates.campaignDistributor.name")
|
||||
: (template?.name ?? "");
|
||||
translatedTemplateName === `${templateTranslationKey}.name`
|
||||
? (template?.name ?? "")
|
||||
: translatedTemplateName;
|
||||
const templateDescription =
|
||||
template?.id === "campaign-distributor"
|
||||
? t("templates.campaignDistributor.description")
|
||||
: (template?.description ?? "");
|
||||
translatedTemplateDescription === `${templateTranslationKey}.description`
|
||||
? (template?.description ?? "")
|
||||
: translatedTemplateDescription;
|
||||
const isExecutionActive = nodeData._status === "analyzing" || nodeData._status === "executing";
|
||||
const executionProgressLine = useMemo(() => {
|
||||
if (nodeData._status !== "executing") {
|
||||
@@ -373,7 +376,7 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
(value: string) => {
|
||||
setModelId(value);
|
||||
setModelDraftId(value);
|
||||
void persistNodeData({ modelId: value });
|
||||
},
|
||||
[persistNodeData],
|
||||
@@ -381,33 +384,29 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
|
||||
const handleClarificationAnswerChange = useCallback(
|
||||
(questionId: string, value: string) => {
|
||||
setClarificationAnswers((prev) => {
|
||||
const next = {
|
||||
...prev,
|
||||
[questionId]: value,
|
||||
};
|
||||
void persistNodeData({ clarificationAnswers: next });
|
||||
return next;
|
||||
});
|
||||
const next = {
|
||||
...clarificationAnswers,
|
||||
[questionId]: value,
|
||||
};
|
||||
setClarificationAnswersDraft(next);
|
||||
void persistNodeData({ clarificationAnswers: next });
|
||||
},
|
||||
[persistNodeData],
|
||||
[clarificationAnswers, persistNodeData],
|
||||
);
|
||||
|
||||
const handleBriefConstraintsChange = useCallback(
|
||||
(patch: Partial<AgentBriefConstraints>) => {
|
||||
setBriefConstraints((prev) => {
|
||||
const next = {
|
||||
...prev,
|
||||
...patch,
|
||||
};
|
||||
void persistNodeData({ briefConstraints: next });
|
||||
return next;
|
||||
});
|
||||
const next = {
|
||||
...briefConstraints,
|
||||
...patch,
|
||||
};
|
||||
setBriefConstraintsDraft(next);
|
||||
void persistNodeData({ briefConstraints: next });
|
||||
},
|
||||
[persistNodeData],
|
||||
[briefConstraints, persistNodeData],
|
||||
);
|
||||
|
||||
const handleRunAgent = useCallback(async () => {
|
||||
const handleRunAgent = async () => {
|
||||
if (isExecutionActive) {
|
||||
return;
|
||||
}
|
||||
@@ -431,9 +430,9 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
modelId: resolvedModelId,
|
||||
locale: normalizedLocale,
|
||||
});
|
||||
}, [isExecutionActive, nodeData.canvasId, id, normalizedLocale, resolvedModelId, runAgent, status.isOffline, t]);
|
||||
};
|
||||
|
||||
const handleSubmitClarification = useCallback(async () => {
|
||||
const handleSubmitClarification = async () => {
|
||||
if (status.isOffline) {
|
||||
toast.warning(
|
||||
t("offlineTitle"),
|
||||
@@ -453,7 +452,7 @@ export default function AgentNode({ id, data, selected }: NodeProps<AgentNodeTyp
|
||||
clarificationAnswers,
|
||||
locale: normalizedLocale,
|
||||
});
|
||||
}, [clarificationAnswers, nodeData.canvasId, id, normalizedLocale, resumeAgent, status.isOffline, t]);
|
||||
};
|
||||
|
||||
if (!template) {
|
||||
return null;
|
||||
|
||||
@@ -12,6 +12,15 @@ type AgentOutputNodeData = {
|
||||
stepTotal?: number;
|
||||
title?: string;
|
||||
channel?: string;
|
||||
artifactType?: string;
|
||||
previewText?: string;
|
||||
sections?: Array<{
|
||||
id?: string;
|
||||
label?: string;
|
||||
content?: string;
|
||||
}>;
|
||||
metadata?: Record<string, string | string[] | unknown>;
|
||||
qualityChecks?: string[];
|
||||
outputType?: string;
|
||||
body?: string;
|
||||
_status?: string;
|
||||
@@ -40,6 +49,70 @@ function tryFormatJsonBody(body: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSections(raw: AgentOutputNodeData["sections"]) {
|
||||
if (!Array.isArray(raw)) {
|
||||
return [] as Array<{ id: string; label: string; content: string }>;
|
||||
}
|
||||
|
||||
const sections: Array<{ id: string; label: string; content: string }> = [];
|
||||
for (const item of raw) {
|
||||
const label = typeof item?.label === "string" ? item.label.trim() : "";
|
||||
const content = typeof item?.content === "string" ? item.content.trim() : "";
|
||||
if (label === "" || content === "") {
|
||||
continue;
|
||||
}
|
||||
const id = typeof item.id === "string" && item.id.trim() !== "" ? item.id.trim() : label;
|
||||
sections.push({ id, label, content });
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
function normalizeMetadata(raw: AgentOutputNodeData["metadata"]) {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return [] as Array<[string, string]>;
|
||||
}
|
||||
|
||||
const entries: Array<[string, string]> = [];
|
||||
for (const [rawKey, rawValue] of Object.entries(raw)) {
|
||||
const key = rawKey.trim();
|
||||
if (key === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof rawValue === "string") {
|
||||
const value = rawValue.trim();
|
||||
if (value !== "") {
|
||||
entries.push([key, value]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(rawValue)) {
|
||||
const values = rawValue
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value !== "");
|
||||
if (values.length > 0) {
|
||||
entries.push([key, values.join(", ")]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function normalizeQualityChecks(raw: AgentOutputNodeData["qualityChecks"]): string[] {
|
||||
if (!Array.isArray(raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return raw
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value !== "");
|
||||
}
|
||||
|
||||
export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutputNodeType>) {
|
||||
const t = useTranslations("agentOutputNode");
|
||||
const nodeData = data as AgentOutputNodeData;
|
||||
@@ -65,6 +138,16 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
||||
nodeData.title ??
|
||||
(isSkeleton ? t("plannedOutputDefaultTitle") : t("defaultTitle"));
|
||||
const body = nodeData.body ?? "";
|
||||
const artifactType = nodeData.artifactType ?? nodeData.outputType ?? "";
|
||||
const sections = normalizeSections(nodeData.sections);
|
||||
const metadataEntries = normalizeMetadata(nodeData.metadata);
|
||||
const qualityChecks = normalizeQualityChecks(nodeData.qualityChecks);
|
||||
const previewText =
|
||||
typeof nodeData.previewText === "string" && nodeData.previewText.trim() !== ""
|
||||
? nodeData.previewText.trim()
|
||||
: sections[0]?.content ?? "";
|
||||
const hasStructuredOutput =
|
||||
sections.length > 0 || metadataEntries.length > 0 || qualityChecks.length > 0 || previewText !== "";
|
||||
const formattedJsonBody = isSkeleton ? null : tryFormatJsonBody(body);
|
||||
|
||||
return (
|
||||
@@ -110,44 +193,108 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("channelLabel")}</p>
|
||||
<p className="truncate text-xs font-medium text-foreground/90" title={nodeData.channel}>
|
||||
{nodeData.channel ?? "-"}
|
||||
{nodeData.channel ?? t("emptyValue")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("typeLabel")}</p>
|
||||
<p className="truncate text-xs font-medium text-foreground/90" title={nodeData.outputType}>
|
||||
{nodeData.outputType ?? "-"}
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("artifactTypeLabel")}</p>
|
||||
<p className="truncate text-xs font-medium text-foreground/90" title={artifactType}>
|
||||
{artifactType || t("emptyValue")}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{t("bodyLabel")}
|
||||
</p>
|
||||
{isSkeleton ? (
|
||||
{isSkeleton ? (
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{t("bodyLabel")}
|
||||
</p>
|
||||
<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">{t("plannedContent")}</p>
|
||||
</div>
|
||||
) : formattedJsonBody ? (
|
||||
</section>
|
||||
) : hasStructuredOutput ? (
|
||||
<>
|
||||
{sections.length > 0 ? (
|
||||
<section data-testid="agent-output-sections" className="space-y-1.5">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("sectionsLabel")}</p>
|
||||
<div className="space-y-1.5">
|
||||
{sections.map((section) => (
|
||||
<div key={section.id} className="rounded-md border border-border/70 bg-background/70 p-2">
|
||||
<p className="text-[11px] font-semibold text-foreground/90">{section.label}</p>
|
||||
<p className="whitespace-pre-wrap break-words text-[12px] leading-relaxed text-foreground/90">
|
||||
{section.content}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{metadataEntries.length > 0 ? (
|
||||
<section data-testid="agent-output-metadata" className="space-y-1.5">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("metadataLabel")}</p>
|
||||
<div className="space-y-1 text-[12px] text-foreground/90">
|
||||
{metadataEntries.map(([key, value]) => (
|
||||
<p key={key} className="break-words">
|
||||
<span className="font-semibold">{key}</span>: {value}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{qualityChecks.length > 0 ? (
|
||||
<section data-testid="agent-output-quality-checks" className="space-y-1.5">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("qualityChecksLabel")}</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{qualityChecks.map((qualityCheck) => (
|
||||
<span
|
||||
key={qualityCheck}
|
||||
className="rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-800 dark:text-amber-200"
|
||||
>
|
||||
{qualityCheck}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section data-testid="agent-output-preview" className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">{t("previewLabel")}</p>
|
||||
<div className="max-h-40 overflow-auto rounded-md border border-border/70 bg-background/70 p-3 text-[13px] leading-relaxed text-foreground/90">
|
||||
<p className="whitespace-pre-wrap break-words">{previewText || t("previewFallback")}</p>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : formattedJsonBody ? (
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{t("bodyLabel")}
|
||||
</p>
|
||||
<pre
|
||||
data-testid="agent-output-json-body"
|
||||
className="max-h-48 overflow-auto rounded-md border border-border/80 bg-muted/40 p-3 font-mono text-[11px] leading-relaxed text-foreground/95"
|
||||
>
|
||||
<code>{formattedJsonBody}</code>
|
||||
</pre>
|
||||
) : (
|
||||
</section>
|
||||
) : (
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{t("bodyLabel")}
|
||||
</p>
|
||||
<div
|
||||
data-testid="agent-output-text-body"
|
||||
className="max-h-48 overflow-auto rounded-md border border-border/70 bg-background/70 p-3 text-[13px] leading-relaxed text-foreground/90"
|
||||
>
|
||||
<p className="whitespace-pre-wrap break-words">{body}</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</BaseNodeWrapper>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user