@copilotkit/react-core 1.55.0-next.9 → 1.55.1-next.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 +46 -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
|
@@ -3,6 +3,7 @@ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
|
|
3
3
|
import { useCopilotKit, useCopilotChatConfiguration } from "../providers";
|
|
4
4
|
import { useCallback, useMemo } from "react";
|
|
5
5
|
import { ReactActivityMessageRenderer } from "../types";
|
|
6
|
+
import { getThreadClone } from "./use-agent";
|
|
6
7
|
|
|
7
8
|
export function useRenderActivityMessage() {
|
|
8
9
|
const { copilotkit } = useCopilotKit();
|
|
@@ -51,7 +52,13 @@ export function useRenderActivityMessage() {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const Component = renderer.render;
|
|
54
|
-
|
|
55
|
+
// Prefer the per-thread clone so that handleAction in ReactSurfaceHost
|
|
56
|
+
// calls runAgent on the same agent instance that CopilotChat renders from.
|
|
57
|
+
// Without this, button clicks accumulate messages on the registry agent
|
|
58
|
+
// while CopilotChat displays from the clone — responses appear to vanish.
|
|
59
|
+
const registryAgent = copilotkit.getAgent(agentId);
|
|
60
|
+
const agent =
|
|
61
|
+
getThreadClone(registryAgent, config?.threadId) ?? registryAgent;
|
|
55
62
|
|
|
56
63
|
return (
|
|
57
64
|
<Component
|
|
@@ -63,7 +70,7 @@ export function useRenderActivityMessage() {
|
|
|
63
70
|
/>
|
|
64
71
|
);
|
|
65
72
|
},
|
|
66
|
-
[agentId, copilotkit, findRenderer],
|
|
73
|
+
[agentId, config?.threadId, copilotkit, findRenderer],
|
|
67
74
|
);
|
|
68
75
|
|
|
69
76
|
return useMemo(
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
ɵselectThreadsIsLoading,
|
|
7
7
|
ɵselectHasNextPage,
|
|
8
8
|
ɵselectIsFetchingNextPage,
|
|
9
|
-
type ɵThread as CoreThread,
|
|
10
9
|
type ɵThreadRuntimeContext,
|
|
11
10
|
type ɵThreadStore,
|
|
12
11
|
} from "@copilotkit/core";
|
|
@@ -24,7 +23,14 @@ import {
|
|
|
24
23
|
* Each thread has a unique `id`, an optional human-readable `name`, and
|
|
25
24
|
* timestamp fields tracking creation and update times.
|
|
26
25
|
*/
|
|
27
|
-
export interface Thread
|
|
26
|
+
export interface Thread {
|
|
27
|
+
id: string;
|
|
28
|
+
agentId: string;
|
|
29
|
+
name: string | null;
|
|
30
|
+
archived: boolean;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* Configuration for the {@link useThreads} hook.
|
|
@@ -68,18 +74,18 @@ export interface UseThreadsResult {
|
|
|
68
74
|
error: Error | null;
|
|
69
75
|
/**
|
|
70
76
|
* `true` when there are more threads available to fetch via
|
|
71
|
-
* {@link
|
|
77
|
+
* {@link fetchMoreThreads}. Only meaningful when `limit` is set.
|
|
72
78
|
*/
|
|
73
|
-
|
|
79
|
+
hasMoreThreads: boolean;
|
|
74
80
|
/**
|
|
75
81
|
* `true` while a subsequent page of threads is being fetched.
|
|
76
82
|
*/
|
|
77
|
-
|
|
83
|
+
isFetchingMoreThreads: boolean;
|
|
78
84
|
/**
|
|
79
|
-
* Fetch the next page of threads. No-op when {@link
|
|
80
|
-
* `false` or a
|
|
85
|
+
* Fetch the next page of threads. No-op when {@link hasMoreThreads} is
|
|
86
|
+
* `false` or a fetch is already in progress.
|
|
81
87
|
*/
|
|
82
|
-
|
|
88
|
+
fetchMoreThreads: () => void;
|
|
83
89
|
/**
|
|
84
90
|
* Rename a thread on the platform.
|
|
85
91
|
* Resolves when the server confirms the update; rejects on failure.
|
|
@@ -169,11 +175,25 @@ export function useThreads({
|
|
|
169
175
|
}),
|
|
170
176
|
);
|
|
171
177
|
|
|
172
|
-
const
|
|
178
|
+
const coreThreads = useThreadStoreSelector(store, ɵselectThreads);
|
|
179
|
+
const threads: Thread[] = useMemo(
|
|
180
|
+
() =>
|
|
181
|
+
coreThreads.map(
|
|
182
|
+
({ id, agentId, name, archived, createdAt, updatedAt }) => ({
|
|
183
|
+
id,
|
|
184
|
+
agentId,
|
|
185
|
+
name,
|
|
186
|
+
archived,
|
|
187
|
+
createdAt,
|
|
188
|
+
updatedAt,
|
|
189
|
+
}),
|
|
190
|
+
),
|
|
191
|
+
[coreThreads],
|
|
192
|
+
);
|
|
173
193
|
const storeIsLoading = useThreadStoreSelector(store, ɵselectThreadsIsLoading);
|
|
174
194
|
const storeError = useThreadStoreSelector(store, ɵselectThreadsError);
|
|
175
|
-
const
|
|
176
|
-
const
|
|
195
|
+
const hasMoreThreads = useThreadStoreSelector(store, ɵselectHasNextPage);
|
|
196
|
+
const isFetchingMoreThreads = useThreadStoreSelector(
|
|
177
197
|
store,
|
|
178
198
|
ɵselectIsFetchingNextPage,
|
|
179
199
|
);
|
|
@@ -240,15 +260,15 @@ export function useThreads({
|
|
|
240
260
|
[store],
|
|
241
261
|
);
|
|
242
262
|
|
|
243
|
-
const
|
|
263
|
+
const fetchMoreThreads = useCallback(() => store.fetchNextPage(), [store]);
|
|
244
264
|
|
|
245
265
|
return {
|
|
246
266
|
threads,
|
|
247
267
|
isLoading,
|
|
248
268
|
error,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
269
|
+
hasMoreThreads,
|
|
270
|
+
isFetchingMoreThreads,
|
|
271
|
+
fetchMoreThreads,
|
|
252
272
|
renameThread,
|
|
253
273
|
archiveThread,
|
|
254
274
|
deleteThread,
|
package/src/v2/index.ts
CHANGED
|
@@ -15,8 +15,12 @@ export * from "./providers";
|
|
|
15
15
|
export * from "./types";
|
|
16
16
|
export * from "./lib/react-core";
|
|
17
17
|
export { createA2UIMessageRenderer } from "./a2ui/A2UIMessageRenderer";
|
|
18
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
A2UIMessageRendererOptions,
|
|
20
|
+
A2UIUserAction,
|
|
21
|
+
} from "./a2ui/A2UIMessageRenderer";
|
|
19
22
|
export type { Theme as A2UITheme } from "@copilotkit/a2ui-renderer";
|
|
23
|
+
export { defaultTheme as a2uiDefaultTheme } from "@copilotkit/a2ui-renderer";
|
|
20
24
|
|
|
21
25
|
// V1 backward-compat re-exports
|
|
22
26
|
export { CopilotKit } from "../components/copilot-provider/copilotkit";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
processPartialHtml,
|
|
4
|
+
extractCompleteStyles,
|
|
5
|
+
} from "../processPartialHtml";
|
|
6
|
+
|
|
7
|
+
describe("processPartialHtml", () => {
|
|
8
|
+
it("returns empty string for empty input", () => {
|
|
9
|
+
expect(processPartialHtml("")).toBe("");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("strips incomplete tag at end", () => {
|
|
13
|
+
expect(processPartialHtml('<div>Hello<span class="fo')).toBe("<div>Hello");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("strips complete <style> blocks", () => {
|
|
17
|
+
const input =
|
|
18
|
+
"<div>Hello</div><style>.foo { color: red; }</style><p>World</p>";
|
|
19
|
+
expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("strips complete <script> blocks", () => {
|
|
23
|
+
const input = '<div>Hello</div><script>alert("hi")</script><p>World</p>';
|
|
24
|
+
expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("strips incomplete <style> block", () => {
|
|
28
|
+
const input = "<div>Hello</div><style>.foo { color:";
|
|
29
|
+
expect(processPartialHtml(input)).toBe("<div>Hello</div>");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("strips incomplete <script> block", () => {
|
|
33
|
+
const input = '<div>Hello</div><script>const x = "val';
|
|
34
|
+
expect(processPartialHtml(input)).toBe("<div>Hello</div>");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("strips incomplete HTML entities", () => {
|
|
38
|
+
expect(processPartialHtml("<p>Hello &")).toBe("<p>Hello ");
|
|
39
|
+
expect(processPartialHtml("<p>Hello {")).toBe("<p>Hello ");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("preserves complete entities", () => {
|
|
43
|
+
expect(processPartialHtml("<p>Hello & World</p>")).toBe(
|
|
44
|
+
"<p>Hello & World</p>",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("extracts body content from full HTML document", () => {
|
|
49
|
+
const input =
|
|
50
|
+
"<html><head><title>Test</title></head><body><p>Content</p></body></html>";
|
|
51
|
+
expect(processPartialHtml(input)).toBe("<p>Content</p>");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("handles <body> with attributes", () => {
|
|
55
|
+
const input = '<body class="dark"><p>Content</p></body>';
|
|
56
|
+
expect(processPartialHtml(input)).toBe("<p>Content</p>");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("handles no <body> tag — returns full processed string", () => {
|
|
60
|
+
const input = "<div><p>Just content</p></div>";
|
|
61
|
+
expect(processPartialHtml(input)).toBe("<div><p>Just content</p></div>");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles combined edge cases: full document with styles, scripts, and incomplete tag", () => {
|
|
65
|
+
const input =
|
|
66
|
+
'<html><head><style>body { margin: 0; }</style></head><body><div>Hello</div><script>console.log("x")</script><p>World</p><span class="in';
|
|
67
|
+
expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("handles body content with incomplete style at end", () => {
|
|
71
|
+
const input = "<body><div>Content</div><style>.partial {";
|
|
72
|
+
expect(processPartialHtml(input)).toBe("<div>Content</div>");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("extractCompleteStyles", () => {
|
|
77
|
+
it("returns empty string for no styles", () => {
|
|
78
|
+
expect(extractCompleteStyles("<div>Hello</div>")).toBe("");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns empty string for empty input", () => {
|
|
82
|
+
expect(extractCompleteStyles("")).toBe("");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("extracts a single complete style block", () => {
|
|
86
|
+
const input =
|
|
87
|
+
"<div>Hello</div><style>.foo { color: red; }</style><p>World</p>";
|
|
88
|
+
expect(extractCompleteStyles(input)).toBe(
|
|
89
|
+
"<style>.foo { color: red; }</style>",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("extracts multiple complete style blocks", () => {
|
|
94
|
+
const input = "<style>a{}</style><div>X</div><style>b{}</style>";
|
|
95
|
+
expect(extractCompleteStyles(input)).toBe(
|
|
96
|
+
"<style>a{}</style><style>b{}</style>",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("ignores incomplete style blocks", () => {
|
|
101
|
+
const input = "<style>.complete{}</style><style>.incomplete {";
|
|
102
|
+
expect(extractCompleteStyles(input)).toBe("<style>.complete{}</style>");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("extracts styles from head", () => {
|
|
106
|
+
const input =
|
|
107
|
+
"<head><style>body { margin: 0; }</style></head><body><p>Hi</p></body>";
|
|
108
|
+
expect(extractCompleteStyles(input)).toBe(
|
|
109
|
+
"<style>body { margin: 0; }</style>",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { useShallowStableRef } from "../slots";
|
|
4
|
+
|
|
5
|
+
describe("useShallowStableRef", () => {
|
|
6
|
+
it("returns the same reference when called twice with shallowly equal plain objects", () => {
|
|
7
|
+
const initial = { a: 1 };
|
|
8
|
+
const { result, rerender } = renderHook(
|
|
9
|
+
({ value }: { value: { a: number } }) => useShallowStableRef(value),
|
|
10
|
+
{ initialProps: { value: initial } },
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const firstRef = result.current;
|
|
14
|
+
rerender({ value: { a: 1 } }); // new object, same shape
|
|
15
|
+
expect(result.current).toBe(firstRef);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("updates the reference when the value changes", () => {
|
|
19
|
+
const { result, rerender } = renderHook(
|
|
20
|
+
({ value }: { value: { a: number } }) => useShallowStableRef(value),
|
|
21
|
+
{ initialProps: { value: { a: 1 } } },
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const firstRef = result.current;
|
|
25
|
+
rerender({ value: { a: 2 } });
|
|
26
|
+
expect(result.current).not.toBe(firstRef);
|
|
27
|
+
expect(result.current).toEqual({ a: 2 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("handles undefined without crashing", () => {
|
|
31
|
+
const { result } = renderHook(() =>
|
|
32
|
+
useShallowStableRef(undefined as unknown as { a: number }),
|
|
33
|
+
);
|
|
34
|
+
expect(result.current).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("handles null without crashing", () => {
|
|
38
|
+
const { result } = renderHook(() =>
|
|
39
|
+
useShallowStableRef(null as unknown as { a: number }),
|
|
40
|
+
);
|
|
41
|
+
expect(result.current).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("does not shallow-compare arrays — treats them by reference", () => {
|
|
45
|
+
const arr1 = [1, 2, 3];
|
|
46
|
+
const { result, rerender } = renderHook(
|
|
47
|
+
({ value }: { value: number[] }) => useShallowStableRef(value),
|
|
48
|
+
{ initialProps: { value: arr1 } },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const firstRef = result.current;
|
|
52
|
+
rerender({ value: [1, 2, 3] }); // new array, same contents
|
|
53
|
+
// arrays are not plain objects — reference should update
|
|
54
|
+
expect(result.current).not.toBe(firstRef);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts all complete `<style>` blocks from the raw HTML.
|
|
3
|
+
* Returns the concatenated style tags, suitable for injection into `<head>`.
|
|
4
|
+
*/
|
|
5
|
+
export function extractCompleteStyles(html: string): string {
|
|
6
|
+
const matches = html.match(/<style\b[^>]*>[\s\S]*?<\/style>/gi);
|
|
7
|
+
return matches ? matches.join("") : "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Processes raw accumulated HTML for safe preview via innerHTML injection.
|
|
12
|
+
* Pure function, no DOM dependencies.
|
|
13
|
+
*
|
|
14
|
+
* Pipeline (order matters):
|
|
15
|
+
* 1. Strip incomplete tag at end
|
|
16
|
+
* 2. Strip complete <style>, <script>, and <head> blocks
|
|
17
|
+
* 3. Strip incomplete <style>/<script>/<head> blocks
|
|
18
|
+
* 4. Strip incomplete HTML entities
|
|
19
|
+
* 5. Extract body content (or use full string if no <body>)
|
|
20
|
+
*/
|
|
21
|
+
export function processPartialHtml(html: string): string {
|
|
22
|
+
let result = html;
|
|
23
|
+
|
|
24
|
+
// 1. Strip incomplete tag at end — e.g. `<div class="fo`
|
|
25
|
+
result = result.replace(/<[^>]*$/, "");
|
|
26
|
+
|
|
27
|
+
// 2. Strip complete <style>, <script>, and <head> blocks
|
|
28
|
+
result = result.replace(/<(style|script|head)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
29
|
+
|
|
30
|
+
// 3. Strip incomplete <style>/<script>/<head> blocks (opening tag, no close)
|
|
31
|
+
result = result.replace(/<(style|script|head)\b[^>]*>[\s\S]*$/gi, "");
|
|
32
|
+
|
|
33
|
+
// 4. Strip incomplete HTML entities — e.g. `&` without semicolon
|
|
34
|
+
result = result.replace(/&[a-zA-Z0-9#]*$/, "");
|
|
35
|
+
|
|
36
|
+
// 5. Extract body content
|
|
37
|
+
const bodyMatch = result.match(/<body[^>]*>([\s\S]*)/i);
|
|
38
|
+
if (bodyMatch) {
|
|
39
|
+
result = bodyMatch[1]!;
|
|
40
|
+
// Strip </body> and everything after
|
|
41
|
+
result = result.replace(/<\/body>[\s\S]*/i, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
package/src/v2/lib/slots.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
2
|
import { twMerge } from "tailwind-merge";
|
|
3
3
|
|
|
4
4
|
/** Existing union (unchanged) */
|
|
@@ -26,6 +26,47 @@ export function shallowEqual<T extends Record<string, unknown>>(
|
|
|
26
26
|
return true;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Returns true only for plain JS objects (`{}`), excluding arrays, Dates,
|
|
31
|
+
* class instances, and other exotic objects that happen to have typeof "object".
|
|
32
|
+
*/
|
|
33
|
+
function isPlainObject(obj: unknown): obj is Record<string, unknown> {
|
|
34
|
+
return (
|
|
35
|
+
obj !== null &&
|
|
36
|
+
typeof obj === "object" &&
|
|
37
|
+
Object.prototype.toString.call(obj) === "[object Object]"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the same reference as long as the value is shallowly equal to the
|
|
43
|
+
* previous render's value.
|
|
44
|
+
*
|
|
45
|
+
* - Identical references bail out immediately (O(1)).
|
|
46
|
+
* - Plain objects ({}) are shallow-compared key-by-key.
|
|
47
|
+
* - Arrays, Dates, class instances, functions, and primitives are compared by
|
|
48
|
+
* reference only — shallowEqual is never called on non-plain objects, which
|
|
49
|
+
* avoids incorrect equality for e.g. [1,2] vs [1,2] (different arrays).
|
|
50
|
+
*
|
|
51
|
+
* Typical use: stabilize inline slot props so MemoizedSlotWrapper's shallow
|
|
52
|
+
* equality check isn't defeated by a new object reference on every render.
|
|
53
|
+
*/
|
|
54
|
+
export function useShallowStableRef<T>(value: T): T {
|
|
55
|
+
const ref = useRef(value);
|
|
56
|
+
|
|
57
|
+
// 1. Identical reference — bail early, no comparison needed.
|
|
58
|
+
if (ref.current === value) return ref.current;
|
|
59
|
+
|
|
60
|
+
// 2. Both are plain objects — shallow-compare to detect structural equality.
|
|
61
|
+
if (isPlainObject(ref.current) && isPlainObject(value)) {
|
|
62
|
+
if (shallowEqual(ref.current, value)) return ref.current;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Different values (or non-comparable types) — update the ref.
|
|
66
|
+
ref.current = value;
|
|
67
|
+
return ref.current;
|
|
68
|
+
}
|
|
69
|
+
|
|
29
70
|
/** Utility: concrete React elements for every slot */
|
|
30
71
|
type SlotElements<S> = { [K in keyof S]: React.ReactElement };
|
|
31
72
|
|
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
useState,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared";
|
|
12
|
+
import { useShallowStableRef } from "../lib/slots";
|
|
12
13
|
|
|
13
14
|
// Default labels
|
|
14
15
|
export const CopilotChatDefaultLabels = {
|
|
@@ -16,7 +17,7 @@ export const CopilotChatDefaultLabels = {
|
|
|
16
17
|
chatInputToolbarStartTranscribeButtonLabel: "Transcribe",
|
|
17
18
|
chatInputToolbarCancelTranscribeButtonLabel: "Cancel",
|
|
18
19
|
chatInputToolbarFinishTranscribeButtonLabel: "Finish",
|
|
19
|
-
chatInputToolbarAddButtonLabel: "Add
|
|
20
|
+
chatInputToolbarAddButtonLabel: "Add attachments",
|
|
20
21
|
chatInputToolbarToolsButtonLabel: "Tools",
|
|
21
22
|
assistantMessageToolbarCopyCodeLabel: "Copy",
|
|
22
23
|
assistantMessageToolbarCopyCodeCopiedLabel: "Copied",
|
|
@@ -65,13 +66,18 @@ export const CopilotChatConfigurationProvider: React.FC<
|
|
|
65
66
|
> = ({ children, labels, agentId, threadId, isModalDefaultOpen }) => {
|
|
66
67
|
const parentConfig = useContext(CopilotChatConfiguration);
|
|
67
68
|
|
|
69
|
+
// Stabilize labels references so that inline objects (new reference on every
|
|
70
|
+
// parent render) don't invalidate mergedLabels and churn the context value.
|
|
71
|
+
// parentConfig?.labels is already stabilized by the parent provider's own
|
|
72
|
+
// useShallowStableRef, so we only need to stabilize the local labels prop.
|
|
73
|
+
const stableLabels = useShallowStableRef(labels);
|
|
68
74
|
const mergedLabels: CopilotChatLabels = useMemo(
|
|
69
75
|
() => ({
|
|
70
76
|
...CopilotChatDefaultLabels,
|
|
71
77
|
...(parentConfig?.labels ?? {}),
|
|
72
|
-
...(
|
|
78
|
+
...(stableLabels ?? {}),
|
|
73
79
|
}),
|
|
74
|
-
[
|
|
80
|
+
[stableLabels, parentConfig?.labels],
|
|
75
81
|
);
|
|
76
82
|
|
|
77
83
|
const resolvedAgentId = agentId ?? parentConfig?.agentId ?? DEFAULT_AGENT_ID;
|