@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
|
@@ -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,6 +17,7 @@ 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";
|
|
@@ -16,6 +26,34 @@ import { getThreadClone } from "../../hooks/use-agent";
|
|
|
16
26
|
import { useCopilotKit } from "../../providers/CopilotKitProvider";
|
|
17
27
|
import { useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider";
|
|
18
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
|
+
|
|
19
57
|
/**
|
|
20
58
|
* Memoized wrapper for assistant messages to prevent re-renders when other messages change.
|
|
21
59
|
*/
|
|
@@ -55,36 +93,32 @@ const MemoizedAssistantMessage = React.memo(
|
|
|
55
93
|
if (prevToolCalls?.length !== nextToolCalls?.length) return false;
|
|
56
94
|
if (prevToolCalls && nextToolCalls) {
|
|
57
95
|
for (let i = 0; i < prevToolCalls.length; i++) {
|
|
58
|
-
const prevTc = prevToolCalls[i]
|
|
59
|
-
const nextTc = nextToolCalls[i]
|
|
60
|
-
if (!prevTc || !nextTc) return false;
|
|
96
|
+
const prevTc = prevToolCalls[i]!;
|
|
97
|
+
const nextTc = nextToolCalls[i]!;
|
|
61
98
|
if (prevTc.id !== nextTc.id) return false;
|
|
62
99
|
if (prevTc.function.arguments !== nextTc.function.arguments)
|
|
63
100
|
return false;
|
|
64
101
|
}
|
|
65
102
|
}
|
|
66
103
|
|
|
67
|
-
// Check if tool results changed for this message's tool calls
|
|
68
|
-
// 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.
|
|
69
106
|
if (prevToolCalls && prevToolCalls.length > 0) {
|
|
70
107
|
const toolCallIds = new Set(prevToolCalls.map((tc) => tc.id));
|
|
71
108
|
|
|
72
109
|
const prevToolResults = prevProps.messages.filter(
|
|
73
|
-
(m)
|
|
110
|
+
(m): m is ToolMessage =>
|
|
111
|
+
m.role === "tool" && toolCallIds.has(m.toolCallId),
|
|
74
112
|
);
|
|
75
113
|
const nextToolResults = nextProps.messages.filter(
|
|
76
|
-
(m)
|
|
114
|
+
(m): m is ToolMessage =>
|
|
115
|
+
m.role === "tool" && toolCallIds.has(m.toolCallId),
|
|
77
116
|
);
|
|
78
117
|
|
|
79
|
-
// If number of tool results changed, re-render
|
|
80
118
|
if (prevToolResults.length !== nextToolResults.length) return false;
|
|
81
119
|
|
|
82
|
-
// If any tool result content changed, re-render
|
|
83
120
|
for (let i = 0; i < prevToolResults.length; i++) {
|
|
84
|
-
if (
|
|
85
|
-
(prevToolResults[i] as any).content !==
|
|
86
|
-
(nextToolResults[i] as any).content
|
|
87
|
-
)
|
|
121
|
+
if (prevToolResults[i]!.content !== nextToolResults[i]!.content)
|
|
88
122
|
return false;
|
|
89
123
|
}
|
|
90
124
|
}
|
|
@@ -294,6 +328,11 @@ export type CopilotChatMessageViewProps = Omit<
|
|
|
294
328
|
}) => React.ReactElement;
|
|
295
329
|
};
|
|
296
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
|
+
|
|
297
336
|
export function CopilotChatMessageView({
|
|
298
337
|
messages = [],
|
|
299
338
|
assistantMessage,
|
|
@@ -363,9 +402,10 @@ export function CopilotChatMessageView({
|
|
|
363
402
|
// Deduplicate messages by id, keeping the last occurrence of each.
|
|
364
403
|
// During streaming, AbstractAgent.addMessage() can push duplicate messages
|
|
365
404
|
// (same id) which causes React "duplicate key" warnings and rendering glitches.
|
|
366
|
-
const deduplicatedMessages =
|
|
367
|
-
...new Map(messages.map((m) => [m.id, m])).values(),
|
|
368
|
-
|
|
405
|
+
const deduplicatedMessages = useMemo(
|
|
406
|
+
() => [...new Map(messages.map((m) => [m.id, m])).values()],
|
|
407
|
+
[messages],
|
|
408
|
+
);
|
|
369
409
|
|
|
370
410
|
if (
|
|
371
411
|
process.env.NODE_ENV === "development" &&
|
|
@@ -376,141 +416,174 @@ export function CopilotChatMessageView({
|
|
|
376
416
|
);
|
|
377
417
|
}
|
|
378
418
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
);
|
|
396
437
|
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
reasoningSlotProps = reasoningMessage as Partial<
|
|
481
|
-
React.ComponentProps<typeof CopilotChatReasoningMessage>
|
|
482
|
-
>;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
elements.push(
|
|
486
|
-
<MemoizedReasoningMessage
|
|
487
|
-
key={message.id}
|
|
488
|
-
message={message as ReasoningMessage}
|
|
489
|
-
messages={messages}
|
|
490
|
-
isRunning={isRunning}
|
|
491
|
-
ReasoningMessageComponent={ReasoningComponent}
|
|
492
|
-
slotProps={reasoningSlotProps}
|
|
493
|
-
/>,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
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
|
+
}
|
|
496
521
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
574
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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);
|
|
513
583
|
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
// children render prop (custom layout, always non-virtual)
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
514
587
|
if (children) {
|
|
515
588
|
return (
|
|
516
589
|
<div data-copilotkit style={{ display: "contents" }}>
|
|
@@ -524,6 +597,9 @@ export function CopilotChatMessageView({
|
|
|
524
597
|
const lastMessage = messages[messages.length - 1];
|
|
525
598
|
const showCursor = isRunning && lastMessage?.role !== "reasoning";
|
|
526
599
|
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Render — shared wrapper, conditional inner content (virtual vs flat)
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
527
603
|
return (
|
|
528
604
|
<div
|
|
529
605
|
data-copilotkit
|
|
@@ -531,7 +607,35 @@ export function CopilotChatMessageView({
|
|
|
531
607
|
className={twMerge("copilotKitMessages cpk:flex cpk:flex-col", className)}
|
|
532
608
|
{...props}
|
|
533
609
|
>
|
|
534
|
-
{
|
|
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
|
+
)}
|
|
535
639
|
{interruptElement}
|
|
536
640
|
{showCursor && (
|
|
537
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
|
);
|