@copilotkit/react-core 1.56.3 → 1.56.4-canary.1777529757
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/dist/{copilotkit-By2G6-Zx.cjs → copilotkit-BAkj3zUc.cjs} +119 -104
- package/dist/copilotkit-BAkj3zUc.cjs.map +1 -0
- package/dist/{copilotkit-PzJlPKcU.mjs → copilotkit-DAatqMh2.mjs} +119 -104
- package/dist/copilotkit-DAatqMh2.mjs.map +1 -0
- package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -1
- package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +121 -106
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +8 -8
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +7 -114
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +26 -6
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +2 -2
- package/src/v2/components/chat/CopilotChatView.tsx +68 -85
- package/src/v2/components/chat/Lightbox.tsx +103 -0
- package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +189 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +193 -57
- package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +264 -0
- package/dist/copilotkit-By2G6-Zx.cjs.map +0 -1
- package/dist/copilotkit-PzJlPKcU.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/react-core",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.4-canary.1777529757",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@ag-ui/client": "0.0.
|
|
56
|
-
"@ag-ui/core": "0.0.
|
|
55
|
+
"@ag-ui/client": "0.0.53",
|
|
56
|
+
"@ag-ui/core": "0.0.53",
|
|
57
57
|
"@jetbrains/websandbox": "^1.1.3",
|
|
58
58
|
"@lit-labs/react": "^2.0.2",
|
|
59
59
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
"untruncate-json": "^0.0.1",
|
|
74
74
|
"use-stick-to-bottom": "^1.1.1",
|
|
75
75
|
"zod-to-json-schema": "^3.24.5",
|
|
76
|
-
"@copilotkit/
|
|
77
|
-
"@copilotkit/
|
|
78
|
-
"@copilotkit/
|
|
79
|
-
"@copilotkit/
|
|
80
|
-
"@copilotkit/
|
|
76
|
+
"@copilotkit/a2ui-renderer": "1.56.4-canary.1777529757",
|
|
77
|
+
"@copilotkit/shared": "1.56.4-canary.1777529757",
|
|
78
|
+
"@copilotkit/web-inspector": "1.56.4-canary.1777529757",
|
|
79
|
+
"@copilotkit/runtime-client-gql": "1.56.4-canary.1777529757",
|
|
80
|
+
"@copilotkit/core": "1.56.4-canary.1777529757"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@tailwindcss/cli": "^4.1.11",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { createPortal, flushSync } from "react-dom";
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
3
2
|
import type { Attachment } from "@copilotkit/shared";
|
|
4
3
|
import {
|
|
5
4
|
formatFileSize,
|
|
6
5
|
getSourceUrl,
|
|
7
6
|
getDocumentIcon,
|
|
8
7
|
} from "@copilotkit/shared";
|
|
9
|
-
import { Play
|
|
8
|
+
import { Play } from "lucide-react";
|
|
10
9
|
import { cn } from "../../lib/utils";
|
|
10
|
+
import { Lightbox, useLightbox } from "./Lightbox";
|
|
11
11
|
|
|
12
12
|
interface CopilotChatAttachmentQueueProps {
|
|
13
13
|
attachments: Attachment[];
|
|
@@ -21,7 +21,10 @@ export const CopilotChatAttachmentQueue: React.FC<
|
|
|
21
21
|
if (attachments.length === 0) return null;
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<div
|
|
24
|
+
<div
|
|
25
|
+
data-testid="copilot-attachment-queue"
|
|
26
|
+
className={cn("cpk:flex cpk:flex-wrap cpk:gap-2 cpk:p-2", className)}
|
|
27
|
+
>
|
|
25
28
|
{attachments.map((attachment) => {
|
|
26
29
|
const isMedia =
|
|
27
30
|
attachment.type === "image" || attachment.type === "video";
|
|
@@ -85,116 +88,6 @@ function AttachmentPreview({ attachment }: { attachment: Attachment }) {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// Lightbox – fullscreen overlay for images and videos (portal to body)
|
|
90
|
-
// Uses the View Transition API when available for a smooth thumbnail-to-
|
|
91
|
-
// fullscreen morph; falls back to a simple opacity fade.
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
interface LightboxProps {
|
|
95
|
-
onClose: () => void;
|
|
96
|
-
children: React.ReactNode;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function Lightbox({ onClose, children }: LightboxProps) {
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
const handleKey = (e: KeyboardEvent) => {
|
|
102
|
-
if (e.key === "Escape") onClose();
|
|
103
|
-
};
|
|
104
|
-
document.addEventListener("keydown", handleKey);
|
|
105
|
-
return () => document.removeEventListener("keydown", handleKey);
|
|
106
|
-
}, [onClose]);
|
|
107
|
-
|
|
108
|
-
if (typeof document === "undefined") return null;
|
|
109
|
-
|
|
110
|
-
return createPortal(
|
|
111
|
-
<div
|
|
112
|
-
className="cpk:fixed cpk:inset-0 cpk:z-[9999] cpk:flex cpk:items-center cpk:justify-center cpk:bg-black/80 cpk:animate-fade-in"
|
|
113
|
-
onClick={onClose}
|
|
114
|
-
>
|
|
115
|
-
<button
|
|
116
|
-
onClick={onClose}
|
|
117
|
-
className="cpk:absolute cpk:top-4 cpk:right-4 cpk:text-white cpk:bg-white/10 cpk:hover:bg-white/20 cpk:rounded-full cpk:w-10 cpk:h-10 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:border-none cpk:z-10"
|
|
118
|
-
aria-label="Close preview"
|
|
119
|
-
>
|
|
120
|
-
<X className="cpk:w-5 cpk:h-5" />
|
|
121
|
-
</button>
|
|
122
|
-
|
|
123
|
-
<div onClick={(e) => e.stopPropagation()}>{children}</div>
|
|
124
|
-
</div>,
|
|
125
|
-
document.body,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
type DocWithVT = Document & {
|
|
130
|
-
startViewTransition?: (cb: () => void) => { finished: Promise<void> };
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Hook that manages lightbox open/close and uses the View Transition API to
|
|
135
|
-
* morph the thumbnail into fullscreen content.
|
|
136
|
-
*
|
|
137
|
-
* The trick: `view-transition-name` must live on exactly ONE element at a time.
|
|
138
|
-
* - Old state (thumbnail visible): name is on the thumbnail.
|
|
139
|
-
* - New state (lightbox visible): name moves to the lightbox content.
|
|
140
|
-
* `flushSync` ensures React commits the DOM change synchronously inside the
|
|
141
|
-
* `startViewTransition` callback so the API can snapshot old → new correctly.
|
|
142
|
-
*/
|
|
143
|
-
function useLightbox() {
|
|
144
|
-
const thumbnailRef = useRef<HTMLElement>(null);
|
|
145
|
-
const [open, setOpen] = useState(false);
|
|
146
|
-
const vtName = useId();
|
|
147
|
-
|
|
148
|
-
const openLightbox = useCallback(() => {
|
|
149
|
-
const thumb = thumbnailRef.current;
|
|
150
|
-
const doc = document as DocWithVT;
|
|
151
|
-
|
|
152
|
-
if (doc.startViewTransition && thumb) {
|
|
153
|
-
// Old snapshot: name on the thumbnail
|
|
154
|
-
thumb.style.viewTransitionName = vtName;
|
|
155
|
-
|
|
156
|
-
doc.startViewTransition(() => {
|
|
157
|
-
// New snapshot: remove from thumb (lightbox content will have it)
|
|
158
|
-
thumb.style.viewTransitionName = "";
|
|
159
|
-
flushSync(() => setOpen(true));
|
|
160
|
-
});
|
|
161
|
-
} else {
|
|
162
|
-
setOpen(true);
|
|
163
|
-
}
|
|
164
|
-
}, []);
|
|
165
|
-
|
|
166
|
-
const closeLightbox = useCallback(() => {
|
|
167
|
-
const thumb = thumbnailRef.current;
|
|
168
|
-
const doc = document as DocWithVT;
|
|
169
|
-
|
|
170
|
-
if (doc.startViewTransition && thumb) {
|
|
171
|
-
const transition = doc.startViewTransition(() => {
|
|
172
|
-
// New snapshot: name back on thumbnail
|
|
173
|
-
flushSync(() => setOpen(false));
|
|
174
|
-
thumb.style.viewTransitionName = vtName;
|
|
175
|
-
});
|
|
176
|
-
// Clean up the name after animation finishes (or fails)
|
|
177
|
-
transition.finished
|
|
178
|
-
.then(() => {
|
|
179
|
-
thumb.style.viewTransitionName = "";
|
|
180
|
-
})
|
|
181
|
-
.catch(() => {
|
|
182
|
-
thumb.style.viewTransitionName = "";
|
|
183
|
-
});
|
|
184
|
-
} else {
|
|
185
|
-
setOpen(false);
|
|
186
|
-
}
|
|
187
|
-
}, []);
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
thumbnailRef,
|
|
191
|
-
vtName,
|
|
192
|
-
open,
|
|
193
|
-
openLightbox,
|
|
194
|
-
closeLightbox,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
91
|
// ---------------------------------------------------------------------------
|
|
199
92
|
// Image
|
|
200
93
|
// ---------------------------------------------------------------------------
|
|
@@ -2,6 +2,7 @@ import React, { memo, useState } from "react";
|
|
|
2
2
|
import type { InputContentSource } from "@copilotkit/shared";
|
|
3
3
|
import { getSourceUrl, getDocumentIcon } from "@copilotkit/shared";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
|
+
import { Lightbox, useLightbox } from "./Lightbox";
|
|
5
6
|
|
|
6
7
|
interface CopilotChatAttachmentRendererProps {
|
|
7
8
|
type: "image" | "audio" | "video" | "document";
|
|
@@ -18,6 +19,8 @@ const ImageAttachment = memo(function ImageAttachment({
|
|
|
18
19
|
className?: string;
|
|
19
20
|
}) {
|
|
20
21
|
const [error, setError] = useState(false);
|
|
22
|
+
const { thumbnailRef, vtName, open, openLightbox, closeLightbox } =
|
|
23
|
+
useLightbox();
|
|
21
24
|
|
|
22
25
|
if (error) {
|
|
23
26
|
return (
|
|
@@ -33,12 +36,29 @@ const ImageAttachment = memo(function ImageAttachment({
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
<>
|
|
40
|
+
<img
|
|
41
|
+
ref={thumbnailRef as React.Ref<HTMLImageElement>}
|
|
42
|
+
src={src}
|
|
43
|
+
alt="Image attachment"
|
|
44
|
+
className={cn(
|
|
45
|
+
"cpk:max-w-[80px] cpk:max-h-[80px] cpk:w-auto cpk:h-auto cpk:rounded-xl cpk:object-cover cpk:cursor-pointer cpk:bg-muted",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
onClick={openLightbox}
|
|
49
|
+
onError={() => setError(true)}
|
|
50
|
+
/>
|
|
51
|
+
{open && (
|
|
52
|
+
<Lightbox onClose={closeLightbox}>
|
|
53
|
+
<img
|
|
54
|
+
style={{ viewTransitionName: vtName }}
|
|
55
|
+
src={src}
|
|
56
|
+
alt="Image attachment"
|
|
57
|
+
className="cpk:max-w-[90vw] cpk:max-h-[90vh] cpk:object-contain cpk:rounded-lg"
|
|
58
|
+
/>
|
|
59
|
+
</Lightbox>
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
42
62
|
);
|
|
43
63
|
});
|
|
44
64
|
|
|
@@ -217,9 +217,8 @@ export function CopilotChatUserMessage({
|
|
|
217
217
|
data-message-id={message.id}
|
|
218
218
|
{...props}
|
|
219
219
|
>
|
|
220
|
-
{BoundMessageRenderer}
|
|
221
220
|
{mediaParts.length > 0 && (
|
|
222
|
-
<div className="cpk:flex cpk:flex-
|
|
221
|
+
<div className="cpk:flex cpk:flex-row cpk:flex-wrap cpk:justify-end cpk:gap-2 cpk:mb-2">
|
|
223
222
|
{mediaParts.map((part, index) => (
|
|
224
223
|
<CopilotChatAttachmentRenderer
|
|
225
224
|
key={index}
|
|
@@ -230,6 +229,7 @@ export function CopilotChatUserMessage({
|
|
|
230
229
|
))}
|
|
231
230
|
</div>
|
|
232
231
|
)}
|
|
232
|
+
{BoundMessageRenderer}
|
|
233
233
|
{BoundToolbar}
|
|
234
234
|
</div>
|
|
235
235
|
);
|
|
@@ -6,17 +6,19 @@ import React, {
|
|
|
6
6
|
useLayoutEffect,
|
|
7
7
|
} from "react";
|
|
8
8
|
import { ScrollElementContext } from "./scroll-element-context";
|
|
9
|
-
import { WithSlots, SlotValue
|
|
9
|
+
import type { WithSlots, SlotValue } from "../../lib/slots";
|
|
10
|
+
import { renderSlot } from "../../lib/slots";
|
|
10
11
|
import CopilotChatMessageView from "./CopilotChatMessageView";
|
|
11
|
-
import
|
|
12
|
+
import type {
|
|
12
13
|
CopilotChatInputProps,
|
|
13
14
|
CopilotChatInputMode,
|
|
14
15
|
} from "./CopilotChatInput";
|
|
16
|
+
import CopilotChatInput from "./CopilotChatInput";
|
|
15
17
|
import CopilotChatSuggestionView, {
|
|
16
18
|
CopilotChatSuggestionViewProps,
|
|
17
19
|
} from "./CopilotChatSuggestionView";
|
|
18
|
-
import { Suggestion } from "@copilotkit/core";
|
|
19
|
-
import { Message } from "@ag-ui/core";
|
|
20
|
+
import type { Suggestion } from "@copilotkit/core";
|
|
21
|
+
import type { Message } from "@ag-ui/core";
|
|
20
22
|
import type { Attachment } from "@copilotkit/shared";
|
|
21
23
|
import { CopilotChatAttachmentQueue } from "./CopilotChatAttachmentQueue";
|
|
22
24
|
import { twMerge } from "tailwind-merge";
|
|
@@ -37,29 +39,8 @@ import { normalizeAutoScroll } from "./normalize-auto-scroll";
|
|
|
37
39
|
import type { AutoScrollMode } from "./normalize-auto-scroll";
|
|
38
40
|
import { usePinToSend } from "../../hooks/use-pin-to-send";
|
|
39
41
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
// Pin-to-send uses a softer, shorter feather than pin-to-bottom so readable
|
|
44
|
-
// content isn't obscured (h-12 = 3rem = 48px).
|
|
45
|
-
const PIN_TO_SEND_FEATHER_HEIGHT = 48;
|
|
46
|
-
|
|
47
|
-
const PinToSendSoftFeather: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
|
48
|
-
className,
|
|
49
|
-
style,
|
|
50
|
-
...props
|
|
51
|
-
}) => (
|
|
52
|
-
<div
|
|
53
|
-
className={cn(
|
|
54
|
-
"cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-4 cpk:h-12 cpk:pointer-events-none cpk:z-10 cpk:bg-gradient-to-t",
|
|
55
|
-
"cpk:from-white cpk:to-transparent",
|
|
56
|
-
"cpk:dark:from-[rgb(33,33,33)]",
|
|
57
|
-
className,
|
|
58
|
-
)}
|
|
59
|
-
style={style}
|
|
60
|
-
{...props}
|
|
61
|
-
/>
|
|
62
|
-
);
|
|
42
|
+
// Vertical gap between the scroll-to-bottom button and the input container.
|
|
43
|
+
const SCROLL_BUTTON_OFFSET = 16;
|
|
63
44
|
|
|
64
45
|
// Forward declaration for WelcomeScreen component type
|
|
65
46
|
export type WelcomeScreenProps = WithSlots<
|
|
@@ -186,7 +167,17 @@ export function CopilotChatView({
|
|
|
186
167
|
className,
|
|
187
168
|
...props
|
|
188
169
|
}: CopilotChatViewProps) {
|
|
189
|
-
|
|
170
|
+
// Element-as-state via callback ref. The overlay wrapper only renders on the
|
|
171
|
+
// chat-view branch (the welcome-screen branch omits it), so a plain
|
|
172
|
+
// useRef + `[]` useEffect would observe `null` on mount whenever the chat
|
|
173
|
+
// starts on the welcome screen and never re-attach after the user sends
|
|
174
|
+
// their first message — leaving inputContainerHeight at 0 and the scroll
|
|
175
|
+
// content's reserved bottom padding at 32px instead of ~input height. The
|
|
176
|
+
// result is the last messages scrolling underneath the absolute-positioned
|
|
177
|
+
// input pill. Subscribing to element state lets the observer attach (and
|
|
178
|
+
// detach) reactively as the overlay mounts/unmounts.
|
|
179
|
+
const [inputContainerEl, setInputContainerEl] =
|
|
180
|
+
useState<HTMLDivElement | null>(null);
|
|
190
181
|
const [inputContainerHeight, setInputContainerHeight] = useState(0);
|
|
191
182
|
const [isResizing, setIsResizing] = useState(false);
|
|
192
183
|
const resizeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
@@ -197,8 +188,14 @@ export function CopilotChatView({
|
|
|
197
188
|
|
|
198
189
|
// Track input container height changes
|
|
199
190
|
useEffect(() => {
|
|
200
|
-
const element =
|
|
201
|
-
if (!element)
|
|
191
|
+
const element = inputContainerEl;
|
|
192
|
+
if (!element) {
|
|
193
|
+
// Reset measured height so the scroll content's paddingBottom doesn't
|
|
194
|
+
// hold a stale value if the overlay unmounts (e.g. messages cleared
|
|
195
|
+
// and the welcome screen returns).
|
|
196
|
+
setInputContainerHeight(0);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
202
199
|
|
|
203
200
|
const resizeObserver = new ResizeObserver((entries) => {
|
|
204
201
|
for (const entry of entries) {
|
|
@@ -237,7 +234,7 @@ export function CopilotChatView({
|
|
|
237
234
|
clearTimeout(resizeTimeoutRef.current);
|
|
238
235
|
}
|
|
239
236
|
};
|
|
240
|
-
}, []);
|
|
237
|
+
}, [inputContainerEl]);
|
|
241
238
|
|
|
242
239
|
const BoundMessageView = renderSlot(messageView, CopilotChatMessageView, {
|
|
243
240
|
messages,
|
|
@@ -258,24 +255,23 @@ export function CopilotChatView({
|
|
|
258
255
|
onAddFile,
|
|
259
256
|
positioning: "static",
|
|
260
257
|
keyboardHeight: isKeyboardOpen ? keyboardHeight : 0,
|
|
261
|
-
containerRef: inputContainerRef,
|
|
262
258
|
showDisclaimer: true,
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// input (below) intentionally
|
|
259
|
+
// The parent overlay wrapper handles absolute bottom-0 positioning.
|
|
260
|
+
// `bottomAnchored` still triggers the license-banner offset padding
|
|
261
|
+
// inside CopilotChatInput. The welcome-screen input (below) intentionally
|
|
262
|
+
// omits this flag.
|
|
266
263
|
bottomAnchored: true,
|
|
267
264
|
...(disclaimer !== undefined ? { disclaimer } : {}),
|
|
268
265
|
} as CopilotChatInputProps);
|
|
269
266
|
|
|
270
|
-
// Hide suggestions while a thread is connecting
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
267
|
+
// Hide suggestions while a thread is connecting (mid-replay would render
|
|
268
|
+
// against a still-assembling message tree and visibly jump as each final
|
|
269
|
+
// text chunk reflows the layout). Run-in-flight is handled by the
|
|
270
|
+
// SuggestionEngine: at run start, non-"always" suggestions are cleared and
|
|
271
|
+
// only "always" ones are restored — so a non-empty `suggestions` array
|
|
272
|
+
// here already means "this is something the user opted to keep visible."
|
|
274
273
|
const hasSuggestions =
|
|
275
|
-
!isConnecting &&
|
|
276
|
-
!isRunning &&
|
|
277
|
-
Array.isArray(suggestions) &&
|
|
278
|
-
suggestions.length > 0;
|
|
274
|
+
!isConnecting && Array.isArray(suggestions) && suggestions.length > 0;
|
|
279
275
|
const BoundSuggestionView = hasSuggestions
|
|
280
276
|
? renderSlot(suggestionView, CopilotChatSuggestionView, {
|
|
281
277
|
suggestions,
|
|
@@ -291,8 +287,9 @@ export function CopilotChatView({
|
|
|
291
287
|
isResizing,
|
|
292
288
|
children: (
|
|
293
289
|
<div
|
|
290
|
+
data-testid="copilot-scroll-content"
|
|
294
291
|
style={{
|
|
295
|
-
paddingBottom: `${hasSuggestions ? 4 : 32}px`,
|
|
292
|
+
paddingBottom: `${inputContainerHeight + (hasSuggestions ? 4 : 32)}px`,
|
|
296
293
|
}}
|
|
297
294
|
>
|
|
298
295
|
<div className="cpk:max-w-3xl cpk:mx-auto">
|
|
@@ -415,17 +412,22 @@ export function CopilotChatView({
|
|
|
415
412
|
{dragOver && <DropOverlay />}
|
|
416
413
|
{BoundScrollView}
|
|
417
414
|
|
|
418
|
-
<div
|
|
415
|
+
<div
|
|
416
|
+
ref={setInputContainerEl}
|
|
417
|
+
data-testid="copilot-input-overlay"
|
|
418
|
+
className="cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-0 cpk:z-20 cpk:pointer-events-none"
|
|
419
|
+
>
|
|
419
420
|
{attachments && attachments.length > 0 && (
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
421
|
+
<div className="cpk:max-w-3xl cpk:mx-auto cpk:w-full cpk:pointer-events-auto">
|
|
422
|
+
<CopilotChatAttachmentQueue
|
|
423
|
+
attachments={attachments}
|
|
424
|
+
onRemoveAttachment={(id) => onRemoveAttachment?.(id)}
|
|
425
|
+
className="cpk:px-4"
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
425
428
|
)}
|
|
429
|
+
{BoundInput}
|
|
426
430
|
</div>
|
|
427
|
-
|
|
428
|
-
{BoundInput}
|
|
429
431
|
</div>
|
|
430
432
|
);
|
|
431
433
|
}
|
|
@@ -476,7 +478,6 @@ export namespace CopilotChatView {
|
|
|
476
478
|
</div>
|
|
477
479
|
</StickToBottom.Content>
|
|
478
480
|
|
|
479
|
-
{/* Feather gradient overlay */}
|
|
480
481
|
{BoundFeather}
|
|
481
482
|
|
|
482
483
|
{/* Scroll to bottom button - hidden during resize */}
|
|
@@ -484,7 +485,7 @@ export namespace CopilotChatView {
|
|
|
484
485
|
<div
|
|
485
486
|
className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
|
|
486
487
|
style={{
|
|
487
|
-
bottom: `${inputContainerHeight +
|
|
488
|
+
bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
|
|
488
489
|
}}
|
|
489
490
|
>
|
|
490
491
|
{renderSlot(
|
|
@@ -541,21 +542,13 @@ export namespace CopilotChatView {
|
|
|
541
542
|
topOffset: 16,
|
|
542
543
|
});
|
|
543
544
|
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
// - pin-to-send: h-12 + from-white to-transparent (gradual fade,
|
|
549
|
-
// no opaque midline). Gives a visual soft edge above the input
|
|
550
|
-
// without obscuring otherwise-readable content.
|
|
551
|
-
// Consumers can still override with the `feather` slot.
|
|
552
|
-
const BoundFeather = renderSlot(feather, PinToSendSoftFeather, {});
|
|
553
|
-
|
|
554
|
-
// Feather and scroll-to-bottom button live OUTSIDE the scroll container.
|
|
555
|
-
// `position: absolute` children of an `overflow: auto` element are
|
|
556
|
-
// positioned relative to the scroll *content*, which means they scroll
|
|
557
|
-
// away with it. Placing them as siblings of the scroll container
|
|
545
|
+
// The feather and scroll-to-bottom button live OUTSIDE the scroll
|
|
546
|
+
// container. `position: absolute` children of an `overflow: auto` element
|
|
547
|
+
// are positioned relative to the scroll *content*, which means they
|
|
548
|
+
// scroll away with it. Placing them as siblings of the scroll container
|
|
558
549
|
// (inside a `relative` wrapper) keeps them pinned to the viewport bottom.
|
|
550
|
+
const BoundFeather = renderSlot(feather, CopilotChatView.Feather, {});
|
|
551
|
+
|
|
559
552
|
return (
|
|
560
553
|
<ScrollElementContext.Provider value={nonAutoScrollEl}>
|
|
561
554
|
<div
|
|
@@ -582,14 +575,13 @@ export namespace CopilotChatView {
|
|
|
582
575
|
style={{ height: 0, flex: "0 0 auto" }}
|
|
583
576
|
/>
|
|
584
577
|
</div>
|
|
585
|
-
{/* Soft feather — pinned to wrapper bottom */}
|
|
586
578
|
{BoundFeather}
|
|
587
579
|
{/* Scroll to bottom button */}
|
|
588
580
|
{showScrollButton && !isResizing && (
|
|
589
581
|
<div
|
|
590
582
|
className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
|
|
591
583
|
style={{
|
|
592
|
-
bottom: `${inputContainerHeight +
|
|
584
|
+
bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
|
|
593
585
|
}}
|
|
594
586
|
>
|
|
595
587
|
{renderSlot(
|
|
@@ -722,7 +714,6 @@ export namespace CopilotChatView {
|
|
|
722
714
|
{children}
|
|
723
715
|
</div>
|
|
724
716
|
|
|
725
|
-
{/* Feather gradient overlay */}
|
|
726
717
|
{BoundFeather}
|
|
727
718
|
|
|
728
719
|
{/* Scroll to bottom button for manual mode */}
|
|
@@ -730,7 +721,7 @@ export namespace CopilotChatView {
|
|
|
730
721
|
<div
|
|
731
722
|
className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
|
|
732
723
|
style={{
|
|
733
|
-
bottom: `${inputContainerHeight +
|
|
724
|
+
bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
|
|
734
725
|
}}
|
|
735
726
|
>
|
|
736
727
|
{renderSlot(
|
|
@@ -812,22 +803,14 @@ export namespace CopilotChatView {
|
|
|
812
803
|
</Button>
|
|
813
804
|
);
|
|
814
805
|
|
|
806
|
+
// Default renders an empty div — no visual, but the element is still in the
|
|
807
|
+
// tree so a slot override of the form `scrollView={{ feather: "my-class" }}`
|
|
808
|
+
// can apply classes (and any consumer with a full component override gets
|
|
809
|
+
// the className/style forwarding they expect).
|
|
815
810
|
export const Feather: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
|
816
811
|
className,
|
|
817
|
-
style,
|
|
818
812
|
...props
|
|
819
|
-
}) =>
|
|
820
|
-
<div
|
|
821
|
-
className={cn(
|
|
822
|
-
"cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-4 cpk:h-24 cpk:pointer-events-none cpk:z-10 cpk:bg-gradient-to-t",
|
|
823
|
-
"cpk:from-white cpk:via-white cpk:to-transparent",
|
|
824
|
-
"cpk:dark:from-[rgb(33,33,33)] cpk:dark:via-[rgb(33,33,33)]",
|
|
825
|
-
className,
|
|
826
|
-
)}
|
|
827
|
-
style={style}
|
|
828
|
-
{...props}
|
|
829
|
-
/>
|
|
830
|
-
);
|
|
813
|
+
}) => <div className={className} {...props} />;
|
|
831
814
|
|
|
832
815
|
export const WelcomeMessage: React.FC<
|
|
833
816
|
React.HTMLAttributes<HTMLDivElement>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
2
|
+
import { createPortal, flushSync } from "react-dom";
|
|
3
|
+
import { X } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface LightboxProps {
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Lightbox({ onClose, children }: LightboxProps) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
13
|
+
if (e.key === "Escape") onClose();
|
|
14
|
+
};
|
|
15
|
+
document.addEventListener("keydown", handleKey);
|
|
16
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
17
|
+
}, [onClose]);
|
|
18
|
+
|
|
19
|
+
if (typeof document === "undefined") return null;
|
|
20
|
+
|
|
21
|
+
return createPortal(
|
|
22
|
+
<div
|
|
23
|
+
className="cpk:fixed cpk:inset-0 cpk:z-[9999] cpk:flex cpk:items-center cpk:justify-center cpk:bg-black/80 cpk:animate-fade-in"
|
|
24
|
+
onClick={onClose}
|
|
25
|
+
>
|
|
26
|
+
<button
|
|
27
|
+
onClick={onClose}
|
|
28
|
+
className="cpk:absolute cpk:top-4 cpk:right-4 cpk:text-white cpk:bg-white/10 cpk:hover:bg-white/20 cpk:rounded-full cpk:w-10 cpk:h-10 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:border-none cpk:z-10"
|
|
29
|
+
aria-label="Close preview"
|
|
30
|
+
>
|
|
31
|
+
<X className="cpk:w-5 cpk:h-5" />
|
|
32
|
+
</button>
|
|
33
|
+
|
|
34
|
+
<div onClick={(e) => e.stopPropagation()}>{children}</div>
|
|
35
|
+
</div>,
|
|
36
|
+
document.body,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type DocWithVT = Document & {
|
|
41
|
+
startViewTransition?: (cb: () => void) => { finished: Promise<void> };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook that manages lightbox open/close and uses the View Transition API to
|
|
46
|
+
* morph the thumbnail into fullscreen content.
|
|
47
|
+
*
|
|
48
|
+
* The trick: `view-transition-name` must live on exactly ONE element at a time.
|
|
49
|
+
* - Old state (thumbnail visible): name is on the thumbnail.
|
|
50
|
+
* - New state (lightbox visible): name moves to the lightbox content.
|
|
51
|
+
* `flushSync` ensures React commits the DOM change synchronously inside the
|
|
52
|
+
* `startViewTransition` callback so the API can snapshot old → new correctly.
|
|
53
|
+
*/
|
|
54
|
+
export function useLightbox() {
|
|
55
|
+
const thumbnailRef = useRef<HTMLElement>(null);
|
|
56
|
+
const [open, setOpen] = useState(false);
|
|
57
|
+
const vtName = useId();
|
|
58
|
+
|
|
59
|
+
const openLightbox = useCallback(() => {
|
|
60
|
+
const thumb = thumbnailRef.current;
|
|
61
|
+
const doc = document as DocWithVT;
|
|
62
|
+
|
|
63
|
+
if (doc.startViewTransition && thumb) {
|
|
64
|
+
thumb.style.viewTransitionName = vtName;
|
|
65
|
+
|
|
66
|
+
doc.startViewTransition(() => {
|
|
67
|
+
thumb.style.viewTransitionName = "";
|
|
68
|
+
flushSync(() => setOpen(true));
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
setOpen(true);
|
|
72
|
+
}
|
|
73
|
+
}, [vtName]);
|
|
74
|
+
|
|
75
|
+
const closeLightbox = useCallback(() => {
|
|
76
|
+
const thumb = thumbnailRef.current;
|
|
77
|
+
const doc = document as DocWithVT;
|
|
78
|
+
|
|
79
|
+
if (doc.startViewTransition && thumb) {
|
|
80
|
+
const transition = doc.startViewTransition(() => {
|
|
81
|
+
flushSync(() => setOpen(false));
|
|
82
|
+
thumb.style.viewTransitionName = vtName;
|
|
83
|
+
});
|
|
84
|
+
transition.finished
|
|
85
|
+
.then(() => {
|
|
86
|
+
thumb.style.viewTransitionName = "";
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {
|
|
89
|
+
thumb.style.viewTransitionName = "";
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
setOpen(false);
|
|
93
|
+
}
|
|
94
|
+
}, [vtName]);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
thumbnailRef,
|
|
98
|
+
vtName,
|
|
99
|
+
open,
|
|
100
|
+
openLightbox,
|
|
101
|
+
closeLightbox,
|
|
102
|
+
};
|
|
103
|
+
}
|