Files
copilotkit--copilotkit/examples/integrations/agent-spec/src/components/a2ui-activity-wrapper.tsx
T
2026-07-13 12:58:18 +08:00

125 lines
3.3 KiB
TypeScript

"use client";
import { useEffect, useMemo, useRef, useState } from "react";
function getOperationSurfaceId(operation: unknown): string | null {
if (!operation || typeof operation !== "object") {
return null;
}
if (
"surfaceId" in operation &&
typeof (operation as any).surfaceId === "string"
) {
return (operation as any).surfaceId;
}
const op = operation as any;
return (
op?.beginRendering?.surfaceId ??
op?.surfaceUpdate?.surfaceId ??
op?.dataModelUpdate?.surfaceId ??
op?.deleteSurface?.surfaceId ??
null
);
}
export function withA2UIActivityMessage(baseRenderer: any) {
function A2UIActivityMessage(props: any) {
const operations = props?.content?.operations as unknown[] | undefined;
const surfaceCount = useMemo(() => {
if (!Array.isArray(operations) || operations.length === 0) {
return 0;
}
const ids = new Set<string>();
for (const op of operations) {
const id = getOperationSurfaceId(op);
if (id) {
ids.add(id);
}
}
return ids.size || 1;
}, [operations]);
const activityLabel = useMemo(() => {
if (!Array.isArray(operations) || operations.length === 0)
return "Interactive UI";
return surfaceCount > 1
? `Interactive UI \u00B7 ${surfaceCount} surfaces`
: "Interactive UI";
}, [operations, surfaceCount]);
const [collapsed, setCollapsed] = useState(false);
const [flash, setFlash] = useState(false);
const lastSignatureRef = useRef<string | null>(null);
const signature = useMemo(() => {
if (!Array.isArray(operations)) {
return null;
}
try {
return JSON.stringify(operations);
} catch {
return String(operations.length);
}
}, [operations]);
useEffect(() => {
if (!signature) {
lastSignatureRef.current = null;
return;
}
if (lastSignatureRef.current && lastSignatureRef.current !== signature) {
lastSignatureRef.current = signature;
setFlash(true);
const timeout = window.setTimeout(() => setFlash(false), 700);
return () => window.clearTimeout(timeout);
}
lastSignatureRef.current = signature;
}, [signature]);
if (!Array.isArray(operations) || operations.length === 0) {
return null;
}
const Inner = baseRenderer.render as any;
const classes = [
"a2ui-activity-message",
collapsed ? "a2ui-activity-message--collapsed" : "",
flash ? "a2ui-activity-message--flash" : "",
]
.filter(Boolean)
.join(" ");
return (
<div className={classes} data-a2ui-activity-message>
<div className="a2ui-activity-header">
<div className="a2ui-activity-header-left">
<span className="a2ui-activity-dot" aria-hidden="true" />
<span>{activityLabel}</span>
</div>
<button
type="button"
className="a2ui-activity-toggle"
onClick={() => setCollapsed((v) => !v)}
aria-expanded={!collapsed}
>
{collapsed ? "Show" : "Hide"}
</button>
</div>
<div className="a2ui-activity-body">
<Inner {...props} />
</div>
</div>
);
}
return {
...baseRenderer,
render: A2UIActivityMessage,
};
}