@copilotkit/react-core 1.55.0-next.9 → 1.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -6
- package/dist/{copilotkit-DeOzjPsb.mjs → copilotkit-BY5S1-0P.mjs} +2402 -552
- package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
- package/dist/{copilotkit-BqcyhQjT.d.mts → copilotkit-BuhSUZHb.d.mts} +228 -17
- package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
- package/dist/{copilotkit-BDNjFNmk.cjs → copilotkit-Bz5-ImDl.cjs} +2421 -541
- package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
- package/dist/{copilotkit-l-IBF4Xp.d.cts → copilotkit-dwDWYpya.d.cts} +228 -17
- package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +1400 -238
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +13 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +3 -3
- package/dist/v2/index.d.mts +3 -3
- package/dist/v2/index.mjs +3 -2
- package/dist/v2/index.umd.js +2442 -552
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +62 -54
- package/scripts/scope-preflight.mjs +1 -2
- package/src/components/CopilotListeners.tsx +41 -8
- package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
- package/src/components/toast/toast-provider.tsx +269 -194
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
- package/src/v2/__tests__/utils/test-helpers.tsx +67 -0
- package/src/v2/a2ui/A2UICatalogContext.tsx +79 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +125 -37
- package/src/v2/a2ui/A2UIToolCallRenderer.tsx +290 -0
- package/src/v2/components/CopilotKitInspector.tsx +2 -0
- package/src/v2/components/OpenGenerativeUIRenderer.tsx +598 -0
- package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +665 -0
- package/src/v2/components/chat/CopilotChat.tsx +193 -50
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +17 -2
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +481 -0
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +139 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +146 -77
- package/src/v2/components/chat/CopilotChatMessageView.tsx +253 -149
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +1 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +54 -0
- package/src/v2/components/chat/CopilotChatView.tsx +179 -66
- package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +168 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +63 -2
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +544 -1
- package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +268 -0
- package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +249 -0
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +43 -2
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +138 -0
- package/src/v2/components/chat/index.ts +9 -0
- package/src/v2/components/chat/scroll-element-context.ts +13 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
- package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +54 -0
- package/src/v2/hooks/index.ts +5 -0
- package/src/v2/hooks/use-agent.tsx +95 -10
- package/src/v2/hooks/use-attachments.tsx +269 -0
- package/src/v2/hooks/use-frontend-tool.tsx +5 -2
- package/src/v2/hooks/use-render-activity-message.tsx +9 -2
- package/src/v2/hooks/use-threads.tsx +35 -15
- package/src/v2/index.ts +5 -1
- package/src/v2/lib/__tests__/processPartialHtml.test.ts +112 -0
- package/src/v2/lib/__tests__/slots.test.ts +56 -0
- package/src/v2/lib/processPartialHtml.ts +45 -0
- package/src/v2/lib/slots.tsx +42 -1
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +9 -3
- package/src/v2/providers/CopilotKitProvider.tsx +268 -32
- package/src/v2/providers/SandboxFunctionsContext.ts +10 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +198 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +71 -0
- package/src/v2/providers/index.ts +7 -0
- package/src/v2/styles/globals.css +2 -1
- package/src/v2/types/index.ts +1 -0
- package/src/v2/types/sandbox-function.ts +11 -0
- package/dist/copilotkit-BDNjFNmk.cjs.map +0 -1
- package/dist/copilotkit-BqcyhQjT.d.mts.map +0 -1
- package/dist/copilotkit-DeOzjPsb.mjs.map +0 -1
- package/dist/copilotkit-l-IBF4Xp.d.cts.map +0 -1
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +0 -46
|
@@ -164,6 +164,7 @@ export function renderWithCopilotKit({
|
|
|
164
164
|
humanInTheLoop,
|
|
165
165
|
agentId,
|
|
166
166
|
threadId,
|
|
167
|
+
defaultThrottleMs,
|
|
167
168
|
children,
|
|
168
169
|
}: {
|
|
169
170
|
agent?: AbstractAgent;
|
|
@@ -175,6 +176,7 @@ export function renderWithCopilotKit({
|
|
|
175
176
|
humanInTheLoop?: any[];
|
|
176
177
|
agentId?: string;
|
|
177
178
|
threadId?: string;
|
|
179
|
+
defaultThrottleMs?: number;
|
|
178
180
|
children?: React.ReactNode;
|
|
179
181
|
}): ReturnType<typeof render> {
|
|
180
182
|
const resolvedAgents = agents || (agent ? { default: agent } : undefined);
|
|
@@ -189,6 +191,7 @@ export function renderWithCopilotKit({
|
|
|
189
191
|
renderActivityMessages={renderActivityMessages}
|
|
190
192
|
frontendTools={frontendTools}
|
|
191
193
|
humanInTheLoop={humanInTheLoop}
|
|
194
|
+
defaultThrottleMs={defaultThrottleMs}
|
|
192
195
|
>
|
|
193
196
|
<CopilotChatConfigurationProvider
|
|
194
197
|
agentId={resolvedAgentId}
|
|
@@ -417,6 +420,70 @@ export function testId(prefix: string): string {
|
|
|
417
420
|
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
418
421
|
}
|
|
419
422
|
|
|
423
|
+
// Varied content lengths for realistic message sizes in perf tests.
|
|
424
|
+
const SAMPLE_ASSISTANT_TEXTS = [
|
|
425
|
+
"Sure! I'd be happy to help you with that.",
|
|
426
|
+
"The weather in San Francisco today is 65°F with partly cloudy skies.",
|
|
427
|
+
"Here are the main points from the meeting: 1) Roadmap review, 2) Bug triage, 3) Release planning.",
|
|
428
|
+
"To configure a custom agent, extend AbstractAgent and implement the run() method. Register it with CopilotKitProvider via the agents__unsafe_dev_only prop.",
|
|
429
|
+
"Here is a React component that fetches data from an API endpoint using useEffect and useState.",
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Generate a realistic sequence of BaseEvents for N assistant messages.
|
|
434
|
+
* Uses TEXT_MESSAGE_CHUNK (the only event type proven to create rendered messages
|
|
435
|
+
* in jsdom tests). Every 5th message includes a tool call.
|
|
436
|
+
*
|
|
437
|
+
* Wrap in RUN_STARTED / RUN_FINISHED yourself if you need a full run sequence:
|
|
438
|
+
* @example
|
|
439
|
+
* agent.emit(runStartedEvent());
|
|
440
|
+
* for (const event of generateMessages(100)) agent.emit(event);
|
|
441
|
+
* agent.emit(runFinishedEvent());
|
|
442
|
+
*/
|
|
443
|
+
export function generateMessages(n: number): BaseEvent[] {
|
|
444
|
+
const events: BaseEvent[] = [];
|
|
445
|
+
|
|
446
|
+
for (let i = 0; i < n; i++) {
|
|
447
|
+
const msgId = `gen-msg-${i}`;
|
|
448
|
+
const text = SAMPLE_ASSISTANT_TEXTS[i % SAMPLE_ASSISTANT_TEXTS.length];
|
|
449
|
+
|
|
450
|
+
// Stream content in ~20-char chunks to simulate real streaming
|
|
451
|
+
for (let offset = 0; offset < text.length; offset += 20) {
|
|
452
|
+
events.push(textChunkEvent(msgId, text.slice(offset, offset + 20)));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Every 5th message has a tool call for realistic variety
|
|
456
|
+
if (i % 5 === 4) {
|
|
457
|
+
const tcId = `gen-tc-${i}`;
|
|
458
|
+
const tcResult = `{"result":"tool output for message ${i}"}`;
|
|
459
|
+
events.push(
|
|
460
|
+
toolCallChunkEvent({
|
|
461
|
+
toolCallId: tcId,
|
|
462
|
+
toolCallName: "exampleTool",
|
|
463
|
+
parentMessageId: msgId,
|
|
464
|
+
delta: "",
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
events.push(
|
|
468
|
+
toolCallChunkEvent({
|
|
469
|
+
toolCallId: tcId,
|
|
470
|
+
parentMessageId: msgId,
|
|
471
|
+
delta: tcResult,
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
events.push(
|
|
475
|
+
toolCallResultEvent({
|
|
476
|
+
toolCallId: tcId,
|
|
477
|
+
messageId: `${msgId}-result`,
|
|
478
|
+
content: tcResult,
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return events;
|
|
485
|
+
}
|
|
486
|
+
|
|
420
487
|
/**
|
|
421
488
|
* Helper to emit a complete suggestion tool call with streaming chunks
|
|
422
489
|
*/
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildCatalogContextValue,
|
|
5
|
+
A2UI_SCHEMA_CONTEXT_DESCRIPTION,
|
|
6
|
+
extractCatalogComponentSchemas,
|
|
7
|
+
} from "@copilotkit/a2ui-renderer";
|
|
8
|
+
import {
|
|
9
|
+
A2UI_DEFAULT_GENERATION_GUIDELINES,
|
|
10
|
+
A2UI_DEFAULT_DESIGN_GUIDELINES,
|
|
11
|
+
} from "@copilotkit/shared";
|
|
12
|
+
import { useAgentContext } from "../hooks/use-agent-context";
|
|
13
|
+
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
|
14
|
+
import { useLayoutEffect, useMemo } from "react";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders agent context describing the available A2UI catalog and custom components.
|
|
18
|
+
* Only mount this component when A2UI is enabled.
|
|
19
|
+
*
|
|
20
|
+
* When `includeSchema` is true, the full component schemas (JSON Schema) are also
|
|
21
|
+
* sent as context using the same description key as the A2UI middleware, so the
|
|
22
|
+
* middleware can optionally overwrite it with a server-side schema.
|
|
23
|
+
*/
|
|
24
|
+
export function A2UICatalogContext({
|
|
25
|
+
catalog,
|
|
26
|
+
includeSchema,
|
|
27
|
+
}: {
|
|
28
|
+
catalog?: any;
|
|
29
|
+
includeSchema?: boolean;
|
|
30
|
+
}) {
|
|
31
|
+
const contextValue = buildCatalogContextValue(catalog);
|
|
32
|
+
|
|
33
|
+
useAgentContext({
|
|
34
|
+
description:
|
|
35
|
+
"A2UI catalog capabilities: available catalog IDs and custom component definitions the client can render.",
|
|
36
|
+
value: contextValue,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// When includeSchema is true, send full component schemas in the same format
|
|
40
|
+
// as the A2UI middleware so it can overwrite with a server-side schema if needed.
|
|
41
|
+
const { copilotkit } = useCopilotKit();
|
|
42
|
+
const schemaValue = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
includeSchema !== false
|
|
45
|
+
? JSON.stringify(extractCatalogComponentSchemas(catalog))
|
|
46
|
+
: null,
|
|
47
|
+
[catalog, includeSchema],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
if (!copilotkit || !schemaValue) return;
|
|
52
|
+
const ids: string[] = [];
|
|
53
|
+
ids.push(
|
|
54
|
+
copilotkit.addContext({
|
|
55
|
+
description: A2UI_SCHEMA_CONTEXT_DESCRIPTION,
|
|
56
|
+
value: schemaValue,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
ids.push(
|
|
60
|
+
copilotkit.addContext({
|
|
61
|
+
description:
|
|
62
|
+
"A2UI generation guidelines — protocol rules, tool arguments, path rules, data model format, and form/two-way-binding instructions.",
|
|
63
|
+
value: A2UI_DEFAULT_GENERATION_GUIDELINES,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
ids.push(
|
|
67
|
+
copilotkit.addContext({
|
|
68
|
+
description:
|
|
69
|
+
"A2UI design guidelines — visual design rules, component hierarchy tips, and action handler patterns.",
|
|
70
|
+
value: A2UI_DEFAULT_DESIGN_GUIDELINES,
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
return () => {
|
|
74
|
+
for (const id of ids) copilotkit.removeContext(id);
|
|
75
|
+
};
|
|
76
|
+
}, [copilotkit, schemaValue]);
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import {
|
|
6
6
|
A2UIProvider,
|
|
7
7
|
useA2UIActions,
|
|
8
|
+
useA2UIError,
|
|
8
9
|
A2UIRenderer,
|
|
9
10
|
initializeDefaultCatalog,
|
|
10
11
|
injectStyles,
|
|
@@ -12,6 +13,12 @@ import {
|
|
|
12
13
|
} from "@copilotkit/a2ui-renderer";
|
|
13
14
|
import type { Theme, A2UIClientEventMessage } from "@copilotkit/a2ui-renderer";
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The container key used to wrap A2UI operations for explicit detection.
|
|
18
|
+
* Must match A2UI_OPERATIONS_KEY in @ag-ui/a2ui-middleware and copilotkit.a2ui (Python).
|
|
19
|
+
*/
|
|
20
|
+
const A2UI_OPERATIONS_KEY = "a2ui_operations";
|
|
21
|
+
|
|
15
22
|
// Initialize the React renderer's component catalog and styles once
|
|
16
23
|
let initialized = false;
|
|
17
24
|
function ensureInitialized() {
|
|
@@ -22,14 +29,30 @@ function ensureInitialized() {
|
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
/**
|
|
33
|
+
* User action with dataContextPath, as dispatched by A2UI components.
|
|
34
|
+
*/
|
|
35
|
+
export type A2UIUserAction = {
|
|
36
|
+
name: string;
|
|
37
|
+
sourceComponentId: string;
|
|
38
|
+
surfaceId: string;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
context?: Record<string, unknown>;
|
|
41
|
+
dataContextPath?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
25
44
|
export type A2UIMessageRendererOptions = {
|
|
26
45
|
theme: Theme;
|
|
46
|
+
/** Optional component catalog to pass to A2UIProvider */
|
|
47
|
+
catalog?: any;
|
|
48
|
+
/** Optional custom loading component shown while A2UI surface is generating. */
|
|
49
|
+
loadingComponent?: React.ComponentType;
|
|
27
50
|
};
|
|
28
51
|
|
|
29
52
|
export function createA2UIMessageRenderer(
|
|
30
53
|
options: A2UIMessageRendererOptions,
|
|
31
54
|
): ReactActivityMessageRenderer<any> {
|
|
32
|
-
const { theme } = options;
|
|
55
|
+
const { theme, catalog, loadingComponent } = options;
|
|
33
56
|
|
|
34
57
|
return {
|
|
35
58
|
activityType: "a2ui-surface",
|
|
@@ -38,24 +61,20 @@ export function createA2UIMessageRenderer(
|
|
|
38
61
|
ensureInitialized();
|
|
39
62
|
|
|
40
63
|
const [operations, setOperations] = useState<any[]>([]);
|
|
41
|
-
const lastSignatureRef = useRef<string | null>(null);
|
|
42
64
|
const { copilotkit } = useCopilotKit();
|
|
43
65
|
|
|
66
|
+
const lastContentRef = useRef<unknown>(null);
|
|
44
67
|
useEffect(() => {
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const incoming = content.operations as any[];
|
|
52
|
-
const signature = stringifyOperations(incoming);
|
|
68
|
+
// Skip if same content reference
|
|
69
|
+
if (content === lastContentRef.current) return;
|
|
70
|
+
lastContentRef.current = content;
|
|
53
71
|
|
|
54
|
-
|
|
72
|
+
const incoming = content?.[A2UI_OPERATIONS_KEY];
|
|
73
|
+
if (!content || !Array.isArray(incoming)) {
|
|
74
|
+
setOperations([]);
|
|
55
75
|
return;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
lastSignatureRef.current = signature;
|
|
59
78
|
setOperations(incoming);
|
|
60
79
|
}, [content]);
|
|
61
80
|
|
|
@@ -77,7 +96,9 @@ export function createA2UIMessageRenderer(
|
|
|
77
96
|
}, [operations]);
|
|
78
97
|
|
|
79
98
|
if (!groupedOperations.size) {
|
|
80
|
-
|
|
99
|
+
// Show loading state while A2UI surface is being generated
|
|
100
|
+
const LoadingComponent = loadingComponent ?? DefaultA2UILoading;
|
|
101
|
+
return <LoadingComponent />;
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
return (
|
|
@@ -90,6 +111,7 @@ export function createA2UIMessageRenderer(
|
|
|
90
111
|
theme={theme}
|
|
91
112
|
agent={agent}
|
|
92
113
|
copilotkit={copilotkit}
|
|
114
|
+
catalog={catalog}
|
|
93
115
|
/>
|
|
94
116
|
))}
|
|
95
117
|
</div>
|
|
@@ -104,6 +126,8 @@ type ReactSurfaceHostProps = {
|
|
|
104
126
|
theme: Theme;
|
|
105
127
|
agent: any;
|
|
106
128
|
copilotkit: any;
|
|
129
|
+
/** Optional component catalog to pass to A2UIProvider */
|
|
130
|
+
catalog?: any;
|
|
107
131
|
};
|
|
108
132
|
|
|
109
133
|
/**
|
|
@@ -116,15 +140,16 @@ function ReactSurfaceHost({
|
|
|
116
140
|
theme,
|
|
117
141
|
agent,
|
|
118
142
|
copilotkit,
|
|
143
|
+
catalog,
|
|
119
144
|
}: ReactSurfaceHostProps) {
|
|
120
|
-
// Bridge: when the React renderer dispatches an action,
|
|
145
|
+
// Bridge: when the React renderer dispatches an action, forward to CopilotKit
|
|
121
146
|
const handleAction = useCallback(
|
|
122
147
|
async (message: A2UIClientEventMessage) => {
|
|
123
148
|
if (!agent) return;
|
|
124
149
|
|
|
125
|
-
|
|
126
|
-
console.info("[A2UI] Action dispatched", message.userAction);
|
|
150
|
+
const action = message.userAction as A2UIUserAction | undefined;
|
|
127
151
|
|
|
152
|
+
try {
|
|
128
153
|
copilotkit.setProperties({
|
|
129
154
|
...(copilotkit.properties ?? {}),
|
|
130
155
|
a2uiAction: message,
|
|
@@ -143,17 +168,33 @@ function ReactSurfaceHost({
|
|
|
143
168
|
|
|
144
169
|
return (
|
|
145
170
|
<div className="cpk:flex cpk:w-full cpk:flex-none cpk:flex-col cpk:gap-4">
|
|
146
|
-
<A2UIProvider onAction={handleAction} theme={theme}>
|
|
171
|
+
<A2UIProvider onAction={handleAction} theme={theme} catalog={catalog}>
|
|
147
172
|
<SurfaceMessageProcessor
|
|
148
173
|
surfaceId={surfaceId}
|
|
149
174
|
operations={operations}
|
|
150
175
|
/>
|
|
151
|
-
<
|
|
176
|
+
<A2UISurfaceOrError surfaceId={surfaceId} />
|
|
152
177
|
</A2UIProvider>
|
|
153
178
|
</div>
|
|
154
179
|
);
|
|
155
180
|
}
|
|
156
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Renders the A2UI surface, or an error message if processing failed.
|
|
184
|
+
* Must be a child of A2UIProvider to access the error state.
|
|
185
|
+
*/
|
|
186
|
+
function A2UISurfaceOrError({ surfaceId }: { surfaceId: string }) {
|
|
187
|
+
const error = useA2UIError();
|
|
188
|
+
if (error) {
|
|
189
|
+
return (
|
|
190
|
+
<div className="cpk:rounded-lg cpk:border cpk:border-red-200 cpk:bg-red-50 cpk:p-3 cpk:text-sm cpk:text-red-700">
|
|
191
|
+
A2UI render error: {error}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return <A2UIRenderer surfaceId={surfaceId} className="cpk:flex cpk:flex-1" />;
|
|
196
|
+
}
|
|
197
|
+
|
|
157
198
|
/**
|
|
158
199
|
* Processes A2UI operations into the provider's message processor.
|
|
159
200
|
* Must be a child of A2UIProvider to access the actions context.
|
|
@@ -165,20 +206,74 @@ function SurfaceMessageProcessor({
|
|
|
165
206
|
surfaceId: string;
|
|
166
207
|
operations: any[];
|
|
167
208
|
}) {
|
|
168
|
-
const { processMessages } = useA2UIActions();
|
|
169
|
-
const
|
|
170
|
-
|
|
209
|
+
const { processMessages, getSurface } = useA2UIActions();
|
|
210
|
+
const lastHashRef = useRef<string>("");
|
|
171
211
|
useEffect(() => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
212
|
+
// Skip if operations haven't actually changed (deep compare via hash).
|
|
213
|
+
// ACTIVITY_DELTA + ACTIVITY_SNAPSHOT can trigger multiple renders with
|
|
214
|
+
// the same logical content but different object references.
|
|
215
|
+
const hash = JSON.stringify(operations);
|
|
216
|
+
if (hash === lastHashRef.current) return;
|
|
217
|
+
lastHashRef.current = hash;
|
|
175
218
|
|
|
176
|
-
|
|
177
|
-
|
|
219
|
+
// Filter out createSurface if the surface already exists — the
|
|
220
|
+
// MessageProcessor throws on duplicate createSurface, but content
|
|
221
|
+
// snapshots always include the full operation list.
|
|
222
|
+
const existing = getSurface(surfaceId);
|
|
223
|
+
const ops = existing
|
|
224
|
+
? operations.filter((op) => !op?.createSurface)
|
|
225
|
+
: operations;
|
|
226
|
+
|
|
227
|
+
// Error handling is done inside A2UIProvider.processMessages
|
|
228
|
+
processMessages(ops);
|
|
229
|
+
}, [processMessages, getSurface, surfaceId, operations]);
|
|
178
230
|
|
|
179
231
|
return null;
|
|
180
232
|
}
|
|
181
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Default loading component shown while an A2UI surface is generating.
|
|
236
|
+
* Displays an animated shimmer skeleton.
|
|
237
|
+
*/
|
|
238
|
+
function DefaultA2UILoading() {
|
|
239
|
+
return (
|
|
240
|
+
<div
|
|
241
|
+
className="cpk:flex cpk:flex-col cpk:gap-3 cpk:rounded-xl cpk:border cpk:border-gray-100 cpk:bg-gray-50/50 cpk:p-5"
|
|
242
|
+
style={{ minHeight: 120 }}
|
|
243
|
+
>
|
|
244
|
+
<div className="cpk:flex cpk:items-center cpk:gap-2">
|
|
245
|
+
<div
|
|
246
|
+
className="cpk:h-3 cpk:w-3 cpk:rounded-full cpk:bg-gray-200"
|
|
247
|
+
style={{
|
|
248
|
+
animation: "cpk-a2ui-pulse 1.5s ease-in-out infinite",
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
<span className="cpk:text-xs cpk:font-medium cpk:text-gray-400">
|
|
252
|
+
Generating UI...
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="cpk:flex cpk:flex-col cpk:gap-2">
|
|
256
|
+
{[0.8, 0.6, 0.4].map((width, i) => (
|
|
257
|
+
<div
|
|
258
|
+
key={i}
|
|
259
|
+
className="cpk:h-3 cpk:rounded cpk:bg-gray-200/70"
|
|
260
|
+
style={{
|
|
261
|
+
width: `${width * 100}%`,
|
|
262
|
+
animation: `cpk-a2ui-pulse 1.5s ease-in-out ${i * 0.15}s infinite`,
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
<style>{`
|
|
268
|
+
@keyframes cpk-a2ui-pulse {
|
|
269
|
+
0%, 100% { opacity: 0.4; }
|
|
270
|
+
50% { opacity: 1; }
|
|
271
|
+
}
|
|
272
|
+
`}</style>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
182
277
|
function getOperationSurfaceId(operation: any): string | null {
|
|
183
278
|
if (!operation || typeof operation !== "object") {
|
|
184
279
|
return null;
|
|
@@ -188,19 +283,12 @@ function getOperationSurfaceId(operation: any): string | null {
|
|
|
188
283
|
return operation.surfaceId;
|
|
189
284
|
}
|
|
190
285
|
|
|
286
|
+
// v0.9 message keys
|
|
191
287
|
return (
|
|
192
|
-
operation?.
|
|
193
|
-
operation?.
|
|
194
|
-
operation?.
|
|
288
|
+
operation?.createSurface?.surfaceId ??
|
|
289
|
+
operation?.updateComponents?.surfaceId ??
|
|
290
|
+
operation?.updateDataModel?.surfaceId ??
|
|
195
291
|
operation?.deleteSurface?.surfaceId ??
|
|
196
292
|
null
|
|
197
293
|
);
|
|
198
294
|
}
|
|
199
|
-
|
|
200
|
-
function stringifyOperations(ops: any[]): string | null {
|
|
201
|
-
try {
|
|
202
|
-
return JSON.stringify(ops);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|