@copilotkit/react-ui 1.68.1 → 1.68.2
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/index.cjs +43 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -2
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +40 -16
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +40 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +43 -16
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +43 -16
- package/dist/index.umd.js.map +1 -1
- package/package.json +4 -4
- package/src/components/chat/Chat.tsx +37 -27
- package/src/components/chat/feedback.test.ts +74 -0
- package/src/components/chat/feedback.ts +38 -0
- package/src/components/chat/messages/AssistantMessage.tsx +5 -2
- package/src/components/chat/props.ts +30 -12
- package/src/css/header-padding.test.ts +40 -0
- package/src/css/header.css +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/react-ui",
|
|
3
|
-
"version": "1.68.
|
|
3
|
+
"version": "1.68.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"rehype-sanitize": "^6.0.0",
|
|
50
50
|
"remark-gfm": "^4.0.1",
|
|
51
51
|
"remark-math": "^6.0.0",
|
|
52
|
-
"@copilotkit/
|
|
53
|
-
"@copilotkit/
|
|
54
|
-
"@copilotkit/shared": "1.68.
|
|
52
|
+
"@copilotkit/react-core": "1.68.2",
|
|
53
|
+
"@copilotkit/runtime-client-gql": "1.68.2",
|
|
54
|
+
"@copilotkit/shared": "1.68.2"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "^19.1.0",
|
|
@@ -102,6 +102,8 @@ import type {
|
|
|
102
102
|
UserMessageProps,
|
|
103
103
|
} from "./props";
|
|
104
104
|
|
|
105
|
+
import type { FeedbackKind } from "./feedback";
|
|
106
|
+
import { applyFeedbackClick } from "./feedback";
|
|
105
107
|
import { AttachmentQueue } from "./AttachmentQueue";
|
|
106
108
|
import type { Attachment, AttachmentsConfig } from "./props";
|
|
107
109
|
import {
|
|
@@ -185,14 +187,20 @@ export interface CopilotChatProps {
|
|
|
185
187
|
onCopy?: (message: string) => void;
|
|
186
188
|
|
|
187
189
|
/**
|
|
188
|
-
* A callback function for thumbs up feedback
|
|
190
|
+
* A callback function for thumbs up feedback.
|
|
191
|
+
*
|
|
192
|
+
* `isActive` reports the feedback state the click is transitioning to:
|
|
193
|
+
* `true` when thumbs up is being applied, `false` when it is being
|
|
194
|
+
* retracted. It is optional so existing one-argument handlers keep working.
|
|
189
195
|
*/
|
|
190
|
-
onThumbsUp?: (message: Message) => void;
|
|
196
|
+
onThumbsUp?: (message: Message, isActive?: boolean) => void;
|
|
191
197
|
|
|
192
198
|
/**
|
|
193
|
-
* A callback function for thumbs down feedback
|
|
199
|
+
* A callback function for thumbs down feedback.
|
|
200
|
+
*
|
|
201
|
+
* See `onThumbsUp` for the meaning of `isActive`.
|
|
194
202
|
*/
|
|
195
|
-
onThumbsDown?: (message: Message) => void;
|
|
203
|
+
onThumbsDown?: (message: Message, isActive?: boolean) => void;
|
|
196
204
|
|
|
197
205
|
/**
|
|
198
206
|
* A list of markdown components to render in assistant message.
|
|
@@ -894,34 +902,36 @@ export function CopilotChat({
|
|
|
894
902
|
}
|
|
895
903
|
};
|
|
896
904
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
905
|
+
// `isActive` is the state the click transitions to. The built-in
|
|
906
|
+
// AssistantMessage derives it from the message's current feedback so a second
|
|
907
|
+
// click on an active button retracts it; a custom AssistantMessage may pass
|
|
908
|
+
// its own value. When omitted, treat the click as applying feedback.
|
|
909
|
+
const recordFeedback = (
|
|
910
|
+
message: Message,
|
|
911
|
+
kind: FeedbackKind,
|
|
912
|
+
isActive?: boolean,
|
|
913
|
+
) => {
|
|
914
|
+
const nextActive = isActive ?? true;
|
|
907
915
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
916
|
+
setMessageFeedback((prev) =>
|
|
917
|
+
applyFeedbackClick(prev, message.id, kind, nextActive),
|
|
918
|
+
);
|
|
911
919
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
920
|
+
// `onFeedbackGiven` has no way to express a retraction, so only report the
|
|
921
|
+
// click that applies feedback.
|
|
922
|
+
if (nextActive) {
|
|
923
|
+
triggerObservabilityHook("onFeedbackGiven", message.id, kind);
|
|
915
924
|
}
|
|
925
|
+
};
|
|
916
926
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}));
|
|
927
|
+
const handleThumbsUp = (message: Message, isActive?: boolean) => {
|
|
928
|
+
onThumbsUp?.(message, isActive ?? true);
|
|
929
|
+
recordFeedback(message, "thumbsUp", isActive);
|
|
930
|
+
};
|
|
922
931
|
|
|
923
|
-
|
|
924
|
-
|
|
932
|
+
const handleThumbsDown = (message: Message, isActive?: boolean) => {
|
|
933
|
+
onThumbsDown?.(message, isActive ?? true);
|
|
934
|
+
recordFeedback(message, "thumbsDown", isActive);
|
|
925
935
|
};
|
|
926
936
|
|
|
927
937
|
return (
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyFeedbackClick,
|
|
4
|
+
isActivatingClick,
|
|
5
|
+
type MessageFeedbackMap,
|
|
6
|
+
} from "./feedback";
|
|
7
|
+
|
|
8
|
+
describe("isActivatingClick (#2615)", () => {
|
|
9
|
+
it("activates when no feedback has been given", () => {
|
|
10
|
+
expect(isActivatingClick(null, "thumbsUp")).toBe(true);
|
|
11
|
+
expect(isActivatingClick(undefined, "thumbsDown")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("deactivates when clicking the already-active button", () => {
|
|
15
|
+
expect(isActivatingClick("thumbsUp", "thumbsUp")).toBe(false);
|
|
16
|
+
expect(isActivatingClick("thumbsDown", "thumbsDown")).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("activates when switching to the opposite button", () => {
|
|
20
|
+
expect(isActivatingClick("thumbsDown", "thumbsUp")).toBe(true);
|
|
21
|
+
expect(isActivatingClick("thumbsUp", "thumbsDown")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("applyFeedbackClick (#2615)", () => {
|
|
26
|
+
it("records feedback for a message that had none", () => {
|
|
27
|
+
expect(applyFeedbackClick({}, "m1", "thumbsUp", true)).toEqual({
|
|
28
|
+
m1: "thumbsUp",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("replaces the opposite feedback rather than keeping both", () => {
|
|
33
|
+
const previous: MessageFeedbackMap = { m1: "thumbsDown" };
|
|
34
|
+
expect(applyFeedbackClick(previous, "m1", "thumbsUp", true)).toEqual({
|
|
35
|
+
m1: "thumbsUp",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("removes the entry when the click retracts feedback", () => {
|
|
40
|
+
const previous: MessageFeedbackMap = { m1: "thumbsUp", m2: "thumbsDown" };
|
|
41
|
+
const next = applyFeedbackClick(previous, "m1", "thumbsUp", false);
|
|
42
|
+
|
|
43
|
+
expect(next).toEqual({ m2: "thumbsDown" });
|
|
44
|
+
expect("m1" in next).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("leaves other messages untouched", () => {
|
|
48
|
+
const previous: MessageFeedbackMap = { m1: "thumbsUp" };
|
|
49
|
+
expect(applyFeedbackClick(previous, "m2", "thumbsDown", true)).toEqual({
|
|
50
|
+
m1: "thumbsUp",
|
|
51
|
+
m2: "thumbsDown",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not mutate the map it is given", () => {
|
|
56
|
+
const previous: MessageFeedbackMap = { m1: "thumbsUp" };
|
|
57
|
+
applyFeedbackClick(previous, "m1", "thumbsUp", false);
|
|
58
|
+
expect(previous).toEqual({ m1: "thumbsUp" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns the same reference when nothing changes", () => {
|
|
62
|
+
const previous: MessageFeedbackMap = { m1: "thumbsUp" };
|
|
63
|
+
expect(applyFeedbackClick(previous, "m1", "thumbsUp", true)).toBe(previous);
|
|
64
|
+
expect(applyFeedbackClick(previous, "m2", "thumbsUp", false)).toBe(
|
|
65
|
+
previous,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("round-trips a toggle back to the starting state", () => {
|
|
70
|
+
const applied = applyFeedbackClick({}, "m1", "thumbsUp", true);
|
|
71
|
+
const retracted = applyFeedbackClick(applied, "m1", "thumbsUp", false);
|
|
72
|
+
expect(retracted).toEqual({});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type FeedbackKind = "thumbsUp" | "thumbsDown";
|
|
2
|
+
|
|
3
|
+
export type MessageFeedbackMap = Record<string, FeedbackKind>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The feedback state a click on `kind` transitions to.
|
|
7
|
+
*
|
|
8
|
+
* Clicking the button that is already active retracts the feedback, so the
|
|
9
|
+
* click deactivates it; every other click applies feedback.
|
|
10
|
+
*/
|
|
11
|
+
export function isActivatingClick(
|
|
12
|
+
current: FeedbackKind | null | undefined,
|
|
13
|
+
kind: FeedbackKind,
|
|
14
|
+
): boolean {
|
|
15
|
+
return current !== kind;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Applies a feedback click to the message-id keyed map.
|
|
20
|
+
*
|
|
21
|
+
* A deactivating click removes the entry entirely rather than storing a falsy
|
|
22
|
+
* value, so `messageFeedback[id]` stays absent for "no feedback given".
|
|
23
|
+
*/
|
|
24
|
+
export function applyFeedbackClick(
|
|
25
|
+
previous: MessageFeedbackMap,
|
|
26
|
+
messageId: string,
|
|
27
|
+
kind: FeedbackKind,
|
|
28
|
+
isActive: boolean,
|
|
29
|
+
): MessageFeedbackMap {
|
|
30
|
+
if (!isActive) {
|
|
31
|
+
if (!(messageId in previous)) return previous;
|
|
32
|
+
const { [messageId]: _retracted, ...rest } = previous;
|
|
33
|
+
return rest;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (previous[messageId] === kind) return previous;
|
|
37
|
+
return { ...previous, [messageId]: kind };
|
|
38
|
+
}
|
|
@@ -4,6 +4,7 @@ import { Markdown } from "../Markdown";
|
|
|
4
4
|
import { useState } from "react";
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { copyToClipboard } from "@copilotkit/shared";
|
|
7
|
+
import { isActivatingClick } from "../feedback";
|
|
7
8
|
|
|
8
9
|
export const AssistantMessage = (props: AssistantMessageProps) => {
|
|
9
10
|
const { icons, labels } = useChatContext();
|
|
@@ -36,15 +37,17 @@ export const AssistantMessage = (props: AssistantMessageProps) => {
|
|
|
36
37
|
if (onRegenerate) onRegenerate();
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
// Clicking the already-active button retracts the feedback, so report the
|
|
41
|
+
// state the click transitions to rather than an unconditional `true`.
|
|
39
42
|
const handleThumbsUp = () => {
|
|
40
43
|
if (onThumbsUp && message) {
|
|
41
|
-
onThumbsUp(message);
|
|
44
|
+
onThumbsUp(message, isActivatingClick(feedback, "thumbsUp"));
|
|
42
45
|
}
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
const handleThumbsDown = () => {
|
|
46
49
|
if (onThumbsDown && message) {
|
|
47
|
-
onThumbsDown(message);
|
|
50
|
+
onThumbsDown(message, isActivatingClick(feedback, "thumbsDown"));
|
|
48
51
|
}
|
|
49
52
|
};
|
|
50
53
|
|
|
@@ -118,14 +118,20 @@ export interface MessagesProps {
|
|
|
118
118
|
onCopy?: (message: string) => void;
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* Callback function for thumbs up feedback
|
|
121
|
+
* Callback function for thumbs up feedback.
|
|
122
|
+
*
|
|
123
|
+
* `isActive` reports the feedback state the click is transitioning to:
|
|
124
|
+
* `true` when thumbs up is being applied, `false` when it is being
|
|
125
|
+
* retracted. It is optional so existing one-argument handlers keep working.
|
|
122
126
|
*/
|
|
123
|
-
onThumbsUp?: (message: Message) => void;
|
|
127
|
+
onThumbsUp?: (message: Message, isActive?: boolean) => void;
|
|
124
128
|
|
|
125
129
|
/**
|
|
126
|
-
* Callback function for thumbs down feedback
|
|
130
|
+
* Callback function for thumbs down feedback.
|
|
131
|
+
*
|
|
132
|
+
* See `onThumbsUp` for the meaning of `isActive`.
|
|
127
133
|
*/
|
|
128
|
-
onThumbsDown?: (message: Message) => void;
|
|
134
|
+
onThumbsDown?: (message: Message, isActive?: boolean) => void;
|
|
129
135
|
|
|
130
136
|
/**
|
|
131
137
|
* Map of message IDs to their feedback state
|
|
@@ -214,14 +220,20 @@ export interface AssistantMessageProps {
|
|
|
214
220
|
onCopy?: (message: string) => void;
|
|
215
221
|
|
|
216
222
|
/**
|
|
217
|
-
* Callback function for thumbs up feedback
|
|
223
|
+
* Callback function for thumbs up feedback.
|
|
224
|
+
*
|
|
225
|
+
* `isActive` reports the feedback state the click is transitioning to:
|
|
226
|
+
* `true` when thumbs up is being applied, `false` when it is being
|
|
227
|
+
* retracted. It is optional so existing one-argument handlers keep working.
|
|
218
228
|
*/
|
|
219
|
-
onThumbsUp?: (message: Message) => void;
|
|
229
|
+
onThumbsUp?: (message: Message, isActive?: boolean) => void;
|
|
220
230
|
|
|
221
231
|
/**
|
|
222
|
-
* Callback function for thumbs down feedback
|
|
232
|
+
* Callback function for thumbs down feedback.
|
|
233
|
+
*
|
|
234
|
+
* See `onThumbsUp` for the meaning of `isActive`.
|
|
223
235
|
*/
|
|
224
|
-
onThumbsDown?: (message: Message) => void;
|
|
236
|
+
onThumbsDown?: (message: Message, isActive?: boolean) => void;
|
|
225
237
|
|
|
226
238
|
/**
|
|
227
239
|
* The feedback state for this message ("thumbsUp" or "thumbsDown")
|
|
@@ -310,14 +322,20 @@ export interface RenderMessageProps {
|
|
|
310
322
|
onCopy?: (message: string) => void;
|
|
311
323
|
|
|
312
324
|
/**
|
|
313
|
-
* Callback function for thumbs up feedback
|
|
325
|
+
* Callback function for thumbs up feedback.
|
|
326
|
+
*
|
|
327
|
+
* `isActive` reports the feedback state the click is transitioning to:
|
|
328
|
+
* `true` when thumbs up is being applied, `false` when it is being
|
|
329
|
+
* retracted. It is optional so existing one-argument handlers keep working.
|
|
314
330
|
*/
|
|
315
|
-
onThumbsUp?: (message: Message) => void;
|
|
331
|
+
onThumbsUp?: (message: Message, isActive?: boolean) => void;
|
|
316
332
|
|
|
317
333
|
/**
|
|
318
|
-
* Callback function for thumbs down feedback
|
|
334
|
+
* Callback function for thumbs down feedback.
|
|
335
|
+
*
|
|
336
|
+
* See `onThumbsUp` for the meaning of `isActive`.
|
|
319
337
|
*/
|
|
320
|
-
onThumbsDown?: (message: Message) => void;
|
|
338
|
+
onThumbsDown?: (message: Message, isActive?: boolean) => void;
|
|
321
339
|
|
|
322
340
|
/**
|
|
323
341
|
* Map of message IDs to their feedback state
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const headerCss = fs.readFileSync(
|
|
6
|
+
path.join(path.resolve(__dirname), "header.css"),
|
|
7
|
+
"utf-8",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the declarations of a rule as it applies with no media query in
|
|
12
|
+
* effect — i.e. the narrow-viewport (mobile) case.
|
|
13
|
+
*/
|
|
14
|
+
function baseDeclarationsFor(css: string, selector: string): string {
|
|
15
|
+
// Drop every @media block so only unconditional rules remain.
|
|
16
|
+
const withoutMediaBlocks = css.replace(
|
|
17
|
+
/@media[^{]*\{(?:[^{}]*\{[^{}]*\}\s*)*\}/g,
|
|
18
|
+
"",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const rule = new RegExp(
|
|
22
|
+
`(?:^|\\})\\s*${selector.replace(/\./g, "\\.")}\\s*\\{([^}]*)\\}`,
|
|
23
|
+
).exec(withoutMediaBlocks);
|
|
24
|
+
|
|
25
|
+
expect(rule, `expected an unconditional \`${selector}\` rule`).not.toBeNull();
|
|
26
|
+
return rule![1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("header horizontal padding (#2493)", () => {
|
|
30
|
+
it("reserves horizontal padding on mobile, not only at the sm breakpoint", () => {
|
|
31
|
+
const base = baseDeclarationsFor(headerCss, ".copilotKitHeader");
|
|
32
|
+
|
|
33
|
+
// The header lays out `justify-content: space-between`, so the title sits
|
|
34
|
+
// against the left edge and the close button against the right edge. With
|
|
35
|
+
// padding declared only inside `@media (min-width: 640px)` both ran flush
|
|
36
|
+
// to the viewport edge below 640px.
|
|
37
|
+
expect(base).toMatch(/padding-left:/);
|
|
38
|
+
expect(base).toMatch(/padding-right:/);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/css/header.css
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
border-top-right-radius: 0;
|
|
10
10
|
border-bottom: 1px solid var(--copilot-kit-separator-color);
|
|
11
11
|
padding-left: 1.5rem;
|
|
12
|
+
padding-right: 1.5rem;
|
|
12
13
|
background-color: var(--copilot-kit-contrast-color);
|
|
13
14
|
justify-content: space-between;
|
|
14
15
|
z-index: 2;
|
|
@@ -29,8 +30,6 @@
|
|
|
29
30
|
|
|
30
31
|
@media (min-width: 640px) {
|
|
31
32
|
.copilotKitHeader {
|
|
32
|
-
padding-left: 1.5rem;
|
|
33
|
-
padding-right: 24px;
|
|
34
33
|
border-top-left-radius: 8px;
|
|
35
34
|
border-top-right-radius: 8px;
|
|
36
35
|
}
|