feat(agent): localize generated agent workflow
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import BaseNodeWrapper from "./base-node-wrapper";
|
||||
|
||||
@@ -19,7 +20,28 @@ type AgentOutputNodeData = {
|
||||
|
||||
type AgentOutputNodeType = Node<AgentOutputNodeData, "agent-output">;
|
||||
|
||||
function tryFormatJsonBody(body: string): string | null {
|
||||
const trimmed = body.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const looksLikeJsonObject = trimmed.startsWith("{") && trimmed.endsWith("}");
|
||||
const looksLikeJsonArray = trimmed.startsWith("[") && trimmed.endsWith("]");
|
||||
if (!looksLikeJsonObject && !looksLikeJsonArray) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutputNodeType>) {
|
||||
const t = useTranslations("agentOutputNode");
|
||||
const nodeData = data as AgentOutputNodeData;
|
||||
const isSkeleton = nodeData.isSkeleton === true;
|
||||
const hasStepCounter =
|
||||
@@ -39,7 +61,11 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
||||
const stepCounter = hasStepCounter
|
||||
? `${safeStepIndex + 1}/${safeStepTotal}`
|
||||
: null;
|
||||
const resolvedTitle = nodeData.title ?? (isSkeleton ? "Planned output" : "Agent output");
|
||||
const resolvedTitle =
|
||||
nodeData.title ??
|
||||
(isSkeleton ? t("plannedOutputDefaultTitle") : t("defaultTitle"));
|
||||
const body = nodeData.body ?? "";
|
||||
const formattedJsonBody = isSkeleton ? null : tryFormatJsonBody(body);
|
||||
|
||||
return (
|
||||
<BaseNodeWrapper
|
||||
@@ -63,52 +89,63 @@ export default function AgentOutputNode({ data, selected }: NodeProps<AgentOutpu
|
||||
{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>
|
||||
<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">
|
||||
{t("skeletonBadge")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{isSkeleton ? (
|
||||
<p className="text-[11px] text-amber-700/90 dark:text-amber-300/90">
|
||||
{t("plannedOutputLabel")}
|
||||
{stepCounter ? ` - ${stepCounter}` : ""}
|
||||
{nodeData.stepId ? ` - ${nodeData.stepId}` : ""}
|
||||
</p>
|
||||
) : 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">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Channel
|
||||
</p>
|
||||
<p className="truncate text-xs text-foreground/90" title={nodeData.channel}>
|
||||
{nodeData.channel ?? "-"}
|
||||
</p>
|
||||
<section
|
||||
data-testid="agent-output-meta-strip"
|
||||
className="grid grid-cols-2 gap-2 rounded-md border border-border/70 bg-muted/30 px-2 py-1.5"
|
||||
>
|
||||
<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 ?? "-"}
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Output Type
|
||||
</p>
|
||||
<p className="truncate text-xs text-foreground/90" title={nodeData.outputType}>
|
||||
{nodeData.outputType ?? "-"}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-1">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Body
|
||||
{t("bodyLabel")}
|
||||
</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>
|
||||
<p className="text-[11px] text-amber-800/90 dark:text-amber-200/90">{t("plannedContent")}</p>
|
||||
</div>
|
||||
) : formattedJsonBody ? (
|
||||
<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>
|
||||
) : (
|
||||
<p className="line-clamp-6 whitespace-pre-wrap text-xs text-foreground/90">
|
||||
{nodeData.body ?? ""}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user