@copilotkit/react-core 1.56.3 → 1.56.4-canary.1777531098
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-DFaI4j2r.d.mts.map +1 -1
- package/dist/{copilotkit-By2G6-Zx.cjs → copilotkit-DMFu29Kx.cjs} +142 -103
- package/dist/copilotkit-DMFu29Kx.cjs.map +1 -0
- package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -1
- package/dist/{copilotkit-PzJlPKcU.mjs → copilotkit-OmIUrWym.mjs} +142 -103
- package/dist/copilotkit-OmIUrWym.mjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +24 -0
- package/dist/index.umd.js.map +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 +144 -105
- 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 +66 -77
- package/src/v2/components/chat/Lightbox.tsx +103 -0
- package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +183 -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/src/v2/hooks/use-configure-suggestions.tsx +43 -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.1777531098",
|
|
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.1777531098",
|
|
77
|
+
"@copilotkit/web-inspector": "1.56.4-canary.1777531098",
|
|
78
|
+
"@copilotkit/shared": "1.56.4-canary.1777531098",
|
|
79
|
+
"@copilotkit/runtime-client-gql": "1.56.4-canary.1777531098",
|
|
80
|
+
"@copilotkit/core": "1.56.4-canary.1777531098"
|
|
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,11 +255,11 @@ 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);
|
|
@@ -271,6 +268,11 @@ export function CopilotChatView({
|
|
|
271
268
|
// Otherwise, mid-replay (bootstrap stream from /connect) or mid-run, the
|
|
272
269
|
// suggestions would render against a still-assembling message tree and
|
|
273
270
|
// visibly jump as each final text chunk reflows the layout.
|
|
271
|
+
//
|
|
272
|
+
// `available: "always"` controls *eligibility windows* (welcome screen vs
|
|
273
|
+
// after first message), not whether to render through these transitions —
|
|
274
|
+
// we still wait for the connect/run to settle and the end-of-run reload
|
|
275
|
+
// to repopulate against the new context.
|
|
274
276
|
const hasSuggestions =
|
|
275
277
|
!isConnecting &&
|
|
276
278
|
!isRunning &&
|
|
@@ -291,8 +293,9 @@ export function CopilotChatView({
|
|
|
291
293
|
isResizing,
|
|
292
294
|
children: (
|
|
293
295
|
<div
|
|
296
|
+
data-testid="copilot-scroll-content"
|
|
294
297
|
style={{
|
|
295
|
-
paddingBottom: `${hasSuggestions ? 4 : 32}px`,
|
|
298
|
+
paddingBottom: `${inputContainerHeight + (hasSuggestions ? 4 : 32)}px`,
|
|
296
299
|
}}
|
|
297
300
|
>
|
|
298
301
|
<div className="cpk:max-w-3xl cpk:mx-auto">
|
|
@@ -415,17 +418,22 @@ export function CopilotChatView({
|
|
|
415
418
|
{dragOver && <DropOverlay />}
|
|
416
419
|
{BoundScrollView}
|
|
417
420
|
|
|
418
|
-
<div
|
|
421
|
+
<div
|
|
422
|
+
ref={setInputContainerEl}
|
|
423
|
+
data-testid="copilot-input-overlay"
|
|
424
|
+
className="cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-0 cpk:z-20 cpk:pointer-events-none"
|
|
425
|
+
>
|
|
419
426
|
{attachments && attachments.length > 0 && (
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
427
|
+
<div className="cpk:max-w-3xl cpk:mx-auto cpk:w-full cpk:pointer-events-auto">
|
|
428
|
+
<CopilotChatAttachmentQueue
|
|
429
|
+
attachments={attachments}
|
|
430
|
+
onRemoveAttachment={(id) => onRemoveAttachment?.(id)}
|
|
431
|
+
className="cpk:px-4"
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
425
434
|
)}
|
|
435
|
+
{BoundInput}
|
|
426
436
|
</div>
|
|
427
|
-
|
|
428
|
-
{BoundInput}
|
|
429
437
|
</div>
|
|
430
438
|
);
|
|
431
439
|
}
|
|
@@ -476,7 +484,6 @@ export namespace CopilotChatView {
|
|
|
476
484
|
</div>
|
|
477
485
|
</StickToBottom.Content>
|
|
478
486
|
|
|
479
|
-
{/* Feather gradient overlay */}
|
|
480
487
|
{BoundFeather}
|
|
481
488
|
|
|
482
489
|
{/* Scroll to bottom button - hidden during resize */}
|
|
@@ -484,7 +491,7 @@ export namespace CopilotChatView {
|
|
|
484
491
|
<div
|
|
485
492
|
className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
|
|
486
493
|
style={{
|
|
487
|
-
bottom: `${inputContainerHeight +
|
|
494
|
+
bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
|
|
488
495
|
}}
|
|
489
496
|
>
|
|
490
497
|
{renderSlot(
|
|
@@ -541,21 +548,13 @@ export namespace CopilotChatView {
|
|
|
541
548
|
topOffset: 16,
|
|
542
549
|
});
|
|
543
550
|
|
|
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
|
|
551
|
+
// The feather and scroll-to-bottom button live OUTSIDE the scroll
|
|
552
|
+
// container. `position: absolute` children of an `overflow: auto` element
|
|
553
|
+
// are positioned relative to the scroll *content*, which means they
|
|
554
|
+
// scroll away with it. Placing them as siblings of the scroll container
|
|
558
555
|
// (inside a `relative` wrapper) keeps them pinned to the viewport bottom.
|
|
556
|
+
const BoundFeather = renderSlot(feather, CopilotChatView.Feather, {});
|
|
557
|
+
|
|
559
558
|
return (
|
|
560
559
|
<ScrollElementContext.Provider value={nonAutoScrollEl}>
|
|
561
560
|
<div
|
|
@@ -582,14 +581,13 @@ export namespace CopilotChatView {
|
|
|
582
581
|
style={{ height: 0, flex: "0 0 auto" }}
|
|
583
582
|
/>
|
|
584
583
|
</div>
|
|
585
|
-
{/* Soft feather — pinned to wrapper bottom */}
|
|
586
584
|
{BoundFeather}
|
|
587
585
|
{/* Scroll to bottom button */}
|
|
588
586
|
{showScrollButton && !isResizing && (
|
|
589
587
|
<div
|
|
590
588
|
className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
|
|
591
589
|
style={{
|
|
592
|
-
bottom: `${inputContainerHeight +
|
|
590
|
+
bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
|
|
593
591
|
}}
|
|
594
592
|
>
|
|
595
593
|
{renderSlot(
|
|
@@ -722,7 +720,6 @@ export namespace CopilotChatView {
|
|
|
722
720
|
{children}
|
|
723
721
|
</div>
|
|
724
722
|
|
|
725
|
-
{/* Feather gradient overlay */}
|
|
726
723
|
{BoundFeather}
|
|
727
724
|
|
|
728
725
|
{/* Scroll to bottom button for manual mode */}
|
|
@@ -730,7 +727,7 @@ export namespace CopilotChatView {
|
|
|
730
727
|
<div
|
|
731
728
|
className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
|
|
732
729
|
style={{
|
|
733
|
-
bottom: `${inputContainerHeight +
|
|
730
|
+
bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
|
|
734
731
|
}}
|
|
735
732
|
>
|
|
736
733
|
{renderSlot(
|
|
@@ -812,22 +809,14 @@ export namespace CopilotChatView {
|
|
|
812
809
|
</Button>
|
|
813
810
|
);
|
|
814
811
|
|
|
812
|
+
// Default renders an empty div — no visual, but the element is still in the
|
|
813
|
+
// tree so a slot override of the form `scrollView={{ feather: "my-class" }}`
|
|
814
|
+
// can apply classes (and any consumer with a full component override gets
|
|
815
|
+
// the className/style forwarding they expect).
|
|
815
816
|
export const Feather: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
|
816
817
|
className,
|
|
817
|
-
style,
|
|
818
818
|
...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
|
-
);
|
|
819
|
+
}) => <div className={className} {...props} />;
|
|
831
820
|
|
|
832
821
|
export const WelcomeMessage: React.FC<
|
|
833
822
|
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
|
+
}
|