@copilotkit/react-ui 1.68.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-ui",
3
- "version": "1.68.0",
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/runtime-client-gql": "1.68.0",
53
- "@copilotkit/shared": "1.68.0",
54
- "@copilotkit/react-core": "1.68.0"
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
- const handleThumbsUp = (message: Message) => {
898
- if (onThumbsUp) {
899
- onThumbsUp(message);
900
- }
901
-
902
- // Update feedback state
903
- setMessageFeedback((prev) => ({
904
- ...prev,
905
- [message.id]: "thumbsUp",
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
- // Trigger feedback given event
909
- triggerObservabilityHook("onFeedbackGiven", message.id, "thumbsUp");
910
- };
916
+ setMessageFeedback((prev) =>
917
+ applyFeedbackClick(prev, message.id, kind, nextActive),
918
+ );
911
919
 
912
- const handleThumbsDown = (message: Message) => {
913
- if (onThumbsDown) {
914
- onThumbsDown(message);
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
- // Update feedback state
918
- setMessageFeedback((prev) => ({
919
- ...prev,
920
- [message.id]: "thumbsDown",
921
- }));
927
+ const handleThumbsUp = (message: Message, isActive?: boolean) => {
928
+ onThumbsUp?.(message, isActive ?? true);
929
+ recordFeedback(message, "thumbsUp", isActive);
930
+ };
922
931
 
923
- // Trigger feedback given event
924
- triggerObservabilityHook("onFeedbackGiven", message.id, "thumbsDown");
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
+ });
@@ -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
  }