@copilotkit/react-core 1.55.0-next.8 → 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 +48 -5
- package/dist/{copilotkit-DNYSFuz5.mjs → copilotkit-BY5S1-0P.mjs} +2772 -858
- package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
- package/dist/{copilotkit-Dy5w3qEV.d.mts → copilotkit-BuhSUZHb.d.mts} +230 -17
- package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
- package/dist/{copilotkit-B3Mb1yVE.cjs → copilotkit-Bz5-ImDl.cjs} +2776 -832
- package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
- package/dist/{copilotkit-DBzgOMby.d.cts → copilotkit-dwDWYpya.d.cts} +230 -17
- package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
- package/dist/index.cjs +9 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +9 -4
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1624 -396
- 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 +2746 -790
- 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/__tests__/copilot-messages-key.test.tsx +92 -0
- package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
- package/src/components/copilot-provider/copilotkit.tsx +3 -3
- package/src/components/toast/toast-provider.tsx +269 -194
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +27 -16
- package/src/hooks/use-copilot-chat_internal.ts +15 -4
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
- package/src/v2/__tests__/utils/test-helpers.tsx +107 -7
- 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 +197 -52
- 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 +260 -151
- 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__/CopilotChatToolRendering.e2e.test.tsx +5 -2
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +5 -2
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +60 -3
- 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-context-timing.e2e.test.tsx +8 -0
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +327 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +13 -2
- package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +23 -4
- 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 +220 -15
- 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-render-custom-messages.tsx +6 -1
- 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-B3Mb1yVE.cjs.map +0 -1
- package/dist/copilotkit-DBzgOMby.d.cts.map +0 -1
- package/dist/copilotkit-DNYSFuz5.mjs.map +0 -1
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +0 -1
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +0 -46
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useContext,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useReducer,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
10
|
+
import { ScrollElementContext } from "./scroll-element-context";
|
|
2
11
|
import { WithSlots, renderSlot, isReactComponentType } from "../../lib/slots";
|
|
3
12
|
import CopilotChatAssistantMessage from "./CopilotChatAssistantMessage";
|
|
4
13
|
import CopilotChatUserMessage from "./CopilotChatUserMessage";
|
|
@@ -8,13 +17,43 @@ import {
|
|
|
8
17
|
AssistantMessage,
|
|
9
18
|
Message,
|
|
10
19
|
ReasoningMessage,
|
|
20
|
+
ToolMessage,
|
|
11
21
|
UserMessage,
|
|
12
22
|
} from "@ag-ui/core";
|
|
13
23
|
import { twMerge } from "tailwind-merge";
|
|
14
24
|
import { useRenderActivityMessage, useRenderCustomMessages } from "../../hooks";
|
|
25
|
+
import { getThreadClone } from "../../hooks/use-agent";
|
|
15
26
|
import { useCopilotKit } from "../../providers/CopilotKitProvider";
|
|
16
27
|
import { useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider";
|
|
17
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Resolves a slot value into a { Component, slotProps } pair, handling the three
|
|
31
|
+
* slot forms: a component type, a className string, or a partial-props object.
|
|
32
|
+
*/
|
|
33
|
+
function resolveSlotComponent<T extends React.ComponentType<any>>(
|
|
34
|
+
slot: unknown,
|
|
35
|
+
DefaultComponent: T,
|
|
36
|
+
): { Component: T; slotProps: Partial<React.ComponentProps<T>> | undefined } {
|
|
37
|
+
if (isReactComponentType(slot)) {
|
|
38
|
+
return { Component: slot as T, slotProps: undefined };
|
|
39
|
+
}
|
|
40
|
+
if (typeof slot === "string") {
|
|
41
|
+
return {
|
|
42
|
+
Component: DefaultComponent,
|
|
43
|
+
slotProps: { className: slot } as unknown as Partial<
|
|
44
|
+
React.ComponentProps<T>
|
|
45
|
+
>,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (slot && typeof slot === "object") {
|
|
49
|
+
return {
|
|
50
|
+
Component: DefaultComponent,
|
|
51
|
+
slotProps: slot as Partial<React.ComponentProps<T>>,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { Component: DefaultComponent, slotProps: undefined };
|
|
55
|
+
}
|
|
56
|
+
|
|
18
57
|
/**
|
|
19
58
|
* Memoized wrapper for assistant messages to prevent re-renders when other messages change.
|
|
20
59
|
*/
|
|
@@ -54,36 +93,32 @@ const MemoizedAssistantMessage = React.memo(
|
|
|
54
93
|
if (prevToolCalls?.length !== nextToolCalls?.length) return false;
|
|
55
94
|
if (prevToolCalls && nextToolCalls) {
|
|
56
95
|
for (let i = 0; i < prevToolCalls.length; i++) {
|
|
57
|
-
const prevTc = prevToolCalls[i]
|
|
58
|
-
const nextTc = nextToolCalls[i]
|
|
59
|
-
if (!prevTc || !nextTc) return false;
|
|
96
|
+
const prevTc = prevToolCalls[i]!;
|
|
97
|
+
const nextTc = nextToolCalls[i]!;
|
|
60
98
|
if (prevTc.id !== nextTc.id) return false;
|
|
61
99
|
if (prevTc.function.arguments !== nextTc.function.arguments)
|
|
62
100
|
return false;
|
|
63
101
|
}
|
|
64
102
|
}
|
|
65
103
|
|
|
66
|
-
// Check if tool results changed for this message's tool calls
|
|
67
|
-
// Tool results are separate messages with role="tool" that reference tool call IDs
|
|
104
|
+
// Check if tool results changed for this message's tool calls.
|
|
105
|
+
// Tool results are separate messages with role="tool" that reference tool call IDs.
|
|
68
106
|
if (prevToolCalls && prevToolCalls.length > 0) {
|
|
69
107
|
const toolCallIds = new Set(prevToolCalls.map((tc) => tc.id));
|
|
70
108
|
|
|
71
109
|
const prevToolResults = prevProps.messages.filter(
|
|
72
|
-
(m)
|
|
110
|
+
(m): m is ToolMessage =>
|
|
111
|
+
m.role === "tool" && toolCallIds.has(m.toolCallId),
|
|
73
112
|
);
|
|
74
113
|
const nextToolResults = nextProps.messages.filter(
|
|
75
|
-
(m)
|
|
114
|
+
(m): m is ToolMessage =>
|
|
115
|
+
m.role === "tool" && toolCallIds.has(m.toolCallId),
|
|
76
116
|
);
|
|
77
117
|
|
|
78
|
-
// If number of tool results changed, re-render
|
|
79
118
|
if (prevToolResults.length !== nextToolResults.length) return false;
|
|
80
119
|
|
|
81
|
-
// If any tool result content changed, re-render
|
|
82
120
|
for (let i = 0; i < prevToolResults.length; i++) {
|
|
83
|
-
if (
|
|
84
|
-
(prevToolResults[i] as any).content !==
|
|
85
|
-
(nextToolResults[i] as any).content
|
|
86
|
-
)
|
|
121
|
+
if (prevToolResults[i]!.content !== nextToolResults[i]!.content)
|
|
87
122
|
return false;
|
|
88
123
|
}
|
|
89
124
|
}
|
|
@@ -293,6 +328,11 @@ export type CopilotChatMessageViewProps = Omit<
|
|
|
293
328
|
}) => React.ReactElement;
|
|
294
329
|
};
|
|
295
330
|
|
|
331
|
+
// Above this many messages, activate TanStack Virtual to avoid mounting the
|
|
332
|
+
// full DOM tree. Below the threshold the overhead of virtualization isn't
|
|
333
|
+
// worth it and the simpler flat render is faster.
|
|
334
|
+
const VIRTUALIZE_THRESHOLD = 50;
|
|
335
|
+
|
|
296
336
|
export function CopilotChatMessageView({
|
|
297
337
|
messages = [],
|
|
298
338
|
assistantMessage,
|
|
@@ -313,14 +353,18 @@ export function CopilotChatMessageView({
|
|
|
313
353
|
// Subscribe to state changes so custom message renderers re-render when state updates.
|
|
314
354
|
useEffect(() => {
|
|
315
355
|
if (!config?.agentId) return;
|
|
316
|
-
const
|
|
356
|
+
const registryAgent = copilotkit.getAgent(config.agentId);
|
|
357
|
+
// Prefer the per-thread clone so that state changes from the running agent
|
|
358
|
+
// (which is the clone, not the registry) trigger re-renders.
|
|
359
|
+
const agent =
|
|
360
|
+
getThreadClone(registryAgent, config.threadId) ?? registryAgent;
|
|
317
361
|
if (!agent) return;
|
|
318
362
|
|
|
319
363
|
const subscription = agent.subscribe({
|
|
320
364
|
onStateChanged: forceUpdate,
|
|
321
365
|
});
|
|
322
366
|
return () => subscription.unsubscribe();
|
|
323
|
-
}, [config?.agentId, copilotkit, forceUpdate]);
|
|
367
|
+
}, [config?.agentId, config?.threadId, copilotkit, forceUpdate]);
|
|
324
368
|
|
|
325
369
|
// Subscribe to interrupt element changes for in-chat rendering.
|
|
326
370
|
const [interruptElement, setInterruptElement] =
|
|
@@ -358,9 +402,10 @@ export function CopilotChatMessageView({
|
|
|
358
402
|
// Deduplicate messages by id, keeping the last occurrence of each.
|
|
359
403
|
// During streaming, AbstractAgent.addMessage() can push duplicate messages
|
|
360
404
|
// (same id) which causes React "duplicate key" warnings and rendering glitches.
|
|
361
|
-
const deduplicatedMessages =
|
|
362
|
-
...new Map(messages.map((m) => [m.id, m])).values(),
|
|
363
|
-
|
|
405
|
+
const deduplicatedMessages = useMemo(
|
|
406
|
+
() => [...new Map(messages.map((m) => [m.id, m])).values()],
|
|
407
|
+
[messages],
|
|
408
|
+
);
|
|
364
409
|
|
|
365
410
|
if (
|
|
366
411
|
process.env.NODE_ENV === "development" &&
|
|
@@ -371,141 +416,174 @@ export function CopilotChatMessageView({
|
|
|
371
416
|
);
|
|
372
417
|
}
|
|
373
418
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
419
|
+
// Resolve slot values once per prop change rather than inside renderMessageBlock.
|
|
420
|
+
// resolveSlotComponent returns a new object every call when the slot is a CSS
|
|
421
|
+
// class string, which would defeat MemoizedAssistantMessage's slotProps
|
|
422
|
+
// reference-equality check and cause all completed messages to re-render.
|
|
423
|
+
const { Component: AssistantComponent, slotProps: assistantSlotProps } =
|
|
424
|
+
useMemo(
|
|
425
|
+
() => resolveSlotComponent(assistantMessage, CopilotChatAssistantMessage),
|
|
426
|
+
[assistantMessage],
|
|
427
|
+
);
|
|
428
|
+
const { Component: UserComponent, slotProps: userSlotProps } = useMemo(
|
|
429
|
+
() => resolveSlotComponent(userMessage, CopilotChatUserMessage),
|
|
430
|
+
[userMessage],
|
|
431
|
+
);
|
|
432
|
+
const { Component: ReasoningComponent, slotProps: reasoningSlotProps } =
|
|
433
|
+
useMemo(
|
|
434
|
+
() => resolveSlotComponent(reasoningMessage, CopilotChatReasoningMessage),
|
|
435
|
+
[reasoningMessage],
|
|
436
|
+
);
|
|
391
437
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
reasoningSlotProps = reasoningMessage as Partial<
|
|
476
|
-
React.ComponentProps<typeof CopilotChatReasoningMessage>
|
|
477
|
-
>;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
elements.push(
|
|
481
|
-
<MemoizedReasoningMessage
|
|
482
|
-
key={message.id}
|
|
483
|
-
message={message as ReasoningMessage}
|
|
484
|
-
messages={messages}
|
|
485
|
-
isRunning={isRunning}
|
|
486
|
-
ReasoningMessageComponent={ReasoningComponent}
|
|
487
|
-
slotProps={reasoningSlotProps}
|
|
488
|
-
/>,
|
|
489
|
-
);
|
|
490
|
-
}
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// Virtualization
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Receive the scroll container from context. ScrollView provides the element
|
|
442
|
+
// as state (not a ref) so this component re-renders reactively when the
|
|
443
|
+
// container first mounts. clientHeight === 0 means no real layout (jsdom) —
|
|
444
|
+
// skip virtualization so tests run the flat path.
|
|
445
|
+
const scrollElementFromCtx = useContext(ScrollElementContext);
|
|
446
|
+
const scrollElement =
|
|
447
|
+
scrollElementFromCtx && scrollElementFromCtx.clientHeight > 0
|
|
448
|
+
? scrollElementFromCtx
|
|
449
|
+
: null;
|
|
450
|
+
|
|
451
|
+
// Warn once in dev when a scroll element is provided but has no height —
|
|
452
|
+
// this silently disables virtualization (e.g. chat inside display:none tab).
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (
|
|
455
|
+
process.env.NODE_ENV !== "production" &&
|
|
456
|
+
scrollElementFromCtx &&
|
|
457
|
+
scrollElementFromCtx.clientHeight === 0
|
|
458
|
+
) {
|
|
459
|
+
console.warn(
|
|
460
|
+
"[CopilotKit] Chat scroll container has clientHeight=0 — virtualization disabled. " +
|
|
461
|
+
"Ensure the chat is rendered in a visible container with a non-zero height.",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}, [scrollElementFromCtx]);
|
|
465
|
+
|
|
466
|
+
// Virtualize only when we have a scroll element and enough messages. The
|
|
467
|
+
// `children` render prop delegates layout to the caller, so we keep
|
|
468
|
+
// messageElements flat for that case.
|
|
469
|
+
const shouldVirtualize =
|
|
470
|
+
!!scrollElement &&
|
|
471
|
+
!children &&
|
|
472
|
+
deduplicatedMessages.length > VIRTUALIZE_THRESHOLD;
|
|
473
|
+
|
|
474
|
+
const virtualizer = useVirtualizer({
|
|
475
|
+
// count=0 disables the virtualizer without changing hook call order.
|
|
476
|
+
count: shouldVirtualize ? deduplicatedMessages.length : 0,
|
|
477
|
+
getScrollElement: () => scrollElement,
|
|
478
|
+
// Conservative height estimate. Items are measured by ResizeObserver after
|
|
479
|
+
// first render so the estimate only affects the initial total height.
|
|
480
|
+
estimateSize: () => 100,
|
|
481
|
+
overscan: 5,
|
|
482
|
+
measureElement: (el: Element) => el?.getBoundingClientRect().height ?? 0,
|
|
483
|
+
// Assume a 600 px viewport before the real element is measured so that
|
|
484
|
+
// the first virtual render shows ~6 items rather than 0.
|
|
485
|
+
initialRect: { width: 0, height: 600 },
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Scroll to the bottom when virtual mode first activates or the thread changes
|
|
489
|
+
// (detected by the first message ID changing). For streaming new messages,
|
|
490
|
+
// use-stick-to-bottom handles auto-scroll via content height growth detection
|
|
491
|
+
// on the virtualizer's total-size div — same as the flat path. Adding
|
|
492
|
+
// deduplicatedMessages.length here would forcibly yank the user to the bottom
|
|
493
|
+
// on every streaming chunk even if they've scrolled up to read history.
|
|
494
|
+
const firstMessageId = deduplicatedMessages[0]?.id;
|
|
495
|
+
useLayoutEffect(() => {
|
|
496
|
+
if (!shouldVirtualize || !deduplicatedMessages.length) return;
|
|
497
|
+
virtualizer.scrollToIndex(deduplicatedMessages.length - 1, {
|
|
498
|
+
align: "end",
|
|
499
|
+
});
|
|
500
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
501
|
+
}, [shouldVirtualize, firstMessageId]);
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Per-message rendering helper (shared by flat and virtual paths)
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
const renderMessageBlock = (message: Message): React.ReactElement[] => {
|
|
507
|
+
const elements: (React.ReactElement | null | undefined)[] = [];
|
|
508
|
+
const stateSnapshot = getStateSnapshotForMessage(message.id);
|
|
509
|
+
|
|
510
|
+
if (renderCustomMessage) {
|
|
511
|
+
elements.push(
|
|
512
|
+
<MemoizedCustomMessage
|
|
513
|
+
key={`${message.id}-custom-before`}
|
|
514
|
+
message={message}
|
|
515
|
+
position="before"
|
|
516
|
+
renderCustomMessage={renderCustomMessage}
|
|
517
|
+
stateSnapshot={stateSnapshot}
|
|
518
|
+
/>,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
491
521
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
522
|
+
if (message.role === "assistant") {
|
|
523
|
+
elements.push(
|
|
524
|
+
<MemoizedAssistantMessage
|
|
525
|
+
key={message.id}
|
|
526
|
+
message={message as AssistantMessage}
|
|
527
|
+
messages={messages}
|
|
528
|
+
isRunning={isRunning}
|
|
529
|
+
AssistantMessageComponent={AssistantComponent}
|
|
530
|
+
slotProps={assistantSlotProps}
|
|
531
|
+
/>,
|
|
532
|
+
);
|
|
533
|
+
} else if (message.role === "user") {
|
|
534
|
+
elements.push(
|
|
535
|
+
<MemoizedUserMessage
|
|
536
|
+
key={message.id}
|
|
537
|
+
message={message as UserMessage}
|
|
538
|
+
UserMessageComponent={UserComponent}
|
|
539
|
+
slotProps={userSlotProps}
|
|
540
|
+
/>,
|
|
541
|
+
);
|
|
542
|
+
} else if (message.role === "activity") {
|
|
543
|
+
elements.push(
|
|
544
|
+
<MemoizedActivityMessage
|
|
545
|
+
key={message.id}
|
|
546
|
+
message={message as ActivityMessage}
|
|
547
|
+
renderActivityMessage={renderActivityMessage}
|
|
548
|
+
/>,
|
|
549
|
+
);
|
|
550
|
+
} else if (message.role === "reasoning") {
|
|
551
|
+
elements.push(
|
|
552
|
+
<MemoizedReasoningMessage
|
|
553
|
+
key={message.id}
|
|
554
|
+
message={message as ReasoningMessage}
|
|
555
|
+
messages={messages}
|
|
556
|
+
isRunning={isRunning}
|
|
557
|
+
ReasoningMessageComponent={ReasoningComponent}
|
|
558
|
+
slotProps={reasoningSlotProps}
|
|
559
|
+
/>,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (renderCustomMessage) {
|
|
564
|
+
elements.push(
|
|
565
|
+
<MemoizedCustomMessage
|
|
566
|
+
key={`${message.id}-custom-after`}
|
|
567
|
+
message={message}
|
|
568
|
+
position="after"
|
|
569
|
+
renderCustomMessage={renderCustomMessage}
|
|
570
|
+
stateSnapshot={stateSnapshot}
|
|
571
|
+
/>,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
504
574
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
575
|
+
return elements.filter(Boolean) as React.ReactElement[];
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// Build the flat element list only when we're not virtualizing (avoids
|
|
579
|
+
// creating 500 React elements that we'd immediately discard).
|
|
580
|
+
const messageElements: React.ReactElement[] = shouldVirtualize
|
|
581
|
+
? []
|
|
582
|
+
: deduplicatedMessages.flatMap(renderMessageBlock);
|
|
508
583
|
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
// children render prop (custom layout, always non-virtual)
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
509
587
|
if (children) {
|
|
510
588
|
return (
|
|
511
589
|
<div data-copilotkit style={{ display: "contents" }}>
|
|
@@ -519,6 +597,9 @@ export function CopilotChatMessageView({
|
|
|
519
597
|
const lastMessage = messages[messages.length - 1];
|
|
520
598
|
const showCursor = isRunning && lastMessage?.role !== "reasoning";
|
|
521
599
|
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Render — shared wrapper, conditional inner content (virtual vs flat)
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
522
603
|
return (
|
|
523
604
|
<div
|
|
524
605
|
data-copilotkit
|
|
@@ -526,7 +607,35 @@ export function CopilotChatMessageView({
|
|
|
526
607
|
className={twMerge("copilotKitMessages cpk:flex cpk:flex-col", className)}
|
|
527
608
|
{...props}
|
|
528
609
|
>
|
|
529
|
-
{
|
|
610
|
+
{shouldVirtualize ? (
|
|
611
|
+
// Virtual path: only visible items are in the DOM; outer div maintains
|
|
612
|
+
// total scroll height so the scrollbar reflects the full list size.
|
|
613
|
+
<div
|
|
614
|
+
style={{ height: virtualizer.getTotalSize(), position: "relative" }}
|
|
615
|
+
>
|
|
616
|
+
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
617
|
+
const message = deduplicatedMessages[virtualItem.index]!;
|
|
618
|
+
return (
|
|
619
|
+
<div
|
|
620
|
+
key={message.id}
|
|
621
|
+
data-index={virtualItem.index}
|
|
622
|
+
ref={virtualizer.measureElement}
|
|
623
|
+
style={{
|
|
624
|
+
position: "absolute",
|
|
625
|
+
top: 0,
|
|
626
|
+
left: 0,
|
|
627
|
+
width: "100%",
|
|
628
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
629
|
+
}}
|
|
630
|
+
>
|
|
631
|
+
{renderMessageBlock(message)}
|
|
632
|
+
</div>
|
|
633
|
+
);
|
|
634
|
+
})}
|
|
635
|
+
</div>
|
|
636
|
+
) : (
|
|
637
|
+
messageElements
|
|
638
|
+
)}
|
|
530
639
|
{interruptElement}
|
|
531
640
|
{showCursor && (
|
|
532
641
|
<div className="cpk:mt-2">
|
|
@@ -72,6 +72,7 @@ export const CopilotChatSuggestionView = React.forwardRef<
|
|
|
72
72
|
CopilotChatSuggestionPillProps
|
|
73
73
|
>(suggestionSlot, CopilotChatSuggestionPill, {
|
|
74
74
|
children: suggestion.title,
|
|
75
|
+
className: suggestion.className,
|
|
75
76
|
isLoading,
|
|
76
77
|
type: "button",
|
|
77
78
|
onClick: () => onSelectSuggestion?.(suggestion, index),
|
|
@@ -13,6 +13,13 @@ import {
|
|
|
13
13
|
TooltipTrigger,
|
|
14
14
|
} from "../../components/ui/tooltip";
|
|
15
15
|
import { renderSlot, WithSlots } from "../../lib/slots";
|
|
16
|
+
import {
|
|
17
|
+
type ImageInputPart,
|
|
18
|
+
type AudioInputPart,
|
|
19
|
+
type VideoInputPart,
|
|
20
|
+
type DocumentInputPart,
|
|
21
|
+
} from "@copilotkit/shared";
|
|
22
|
+
import { CopilotChatAttachmentRenderer } from "./CopilotChatAttachmentRenderer";
|
|
16
23
|
|
|
17
24
|
function flattenUserMessageContent(content?: UserMessage["content"]): string {
|
|
18
25
|
if (!content) {
|
|
@@ -40,6 +47,36 @@ function flattenUserMessageContent(content?: UserMessage["content"]): string {
|
|
|
40
47
|
.join("\n");
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
type MediaPart =
|
|
51
|
+
| ImageInputPart
|
|
52
|
+
| AudioInputPart
|
|
53
|
+
| VideoInputPart
|
|
54
|
+
| DocumentInputPart;
|
|
55
|
+
|
|
56
|
+
function getMediaParts(content: UserMessage["content"]): MediaPart[] {
|
|
57
|
+
if (!content || typeof content === "string") return [];
|
|
58
|
+
return content.filter(
|
|
59
|
+
(part): part is MediaPart =>
|
|
60
|
+
part.type === "image" ||
|
|
61
|
+
part.type === "audio" ||
|
|
62
|
+
part.type === "video" ||
|
|
63
|
+
part.type === "document",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getFilename(part: MediaPart): string | undefined {
|
|
68
|
+
const meta = part.metadata;
|
|
69
|
+
if (
|
|
70
|
+
meta != null &&
|
|
71
|
+
typeof meta === "object" &&
|
|
72
|
+
"filename" in meta &&
|
|
73
|
+
typeof meta.filename === "string"
|
|
74
|
+
) {
|
|
75
|
+
return meta.filename;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
43
80
|
export interface CopilotChatUserMessageOnEditMessageProps {
|
|
44
81
|
message: UserMessage;
|
|
45
82
|
}
|
|
@@ -91,6 +128,11 @@ export function CopilotChatUserMessage({
|
|
|
91
128
|
[message.content],
|
|
92
129
|
);
|
|
93
130
|
|
|
131
|
+
const mediaParts = useMemo(
|
|
132
|
+
() => getMediaParts(message.content),
|
|
133
|
+
[message.content],
|
|
134
|
+
);
|
|
135
|
+
|
|
94
136
|
const BoundMessageRenderer = renderSlot(
|
|
95
137
|
messageRenderer,
|
|
96
138
|
CopilotChatUserMessage.MessageRenderer,
|
|
@@ -178,6 +220,18 @@ export function CopilotChatUserMessage({
|
|
|
178
220
|
{...props}
|
|
179
221
|
>
|
|
180
222
|
{BoundMessageRenderer}
|
|
223
|
+
{mediaParts.length > 0 && (
|
|
224
|
+
<div className="cpk:flex cpk:flex-col cpk:items-end cpk:gap-2 cpk:mt-2">
|
|
225
|
+
{mediaParts.map((part, index) => (
|
|
226
|
+
<CopilotChatAttachmentRenderer
|
|
227
|
+
key={index}
|
|
228
|
+
type={part.type}
|
|
229
|
+
source={part.source}
|
|
230
|
+
filename={getFilename(part)}
|
|
231
|
+
/>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
181
235
|
{BoundToolbar}
|
|
182
236
|
</div>
|
|
183
237
|
);
|