@copilotkit/react-ui 1.56.0 → 1.56.2-canary.pin-to-send

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.56.0",
3
+ "version": "1.56.2-canary.pin-to-send",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -48,9 +48,9 @@
48
48
  "rehype-raw": "^7.0.0",
49
49
  "remark-gfm": "^4.0.1",
50
50
  "remark-math": "^6.0.0",
51
- "@copilotkit/react-core": "1.56.0",
52
- "@copilotkit/runtime-client-gql": "1.56.0",
53
- "@copilotkit/shared": "1.56.0"
51
+ "@copilotkit/react-core": "1.56.2-canary.pin-to-send",
52
+ "@copilotkit/runtime-client-gql": "1.56.2-canary.pin-to-send",
53
+ "@copilotkit/shared": "1.56.2-canary.pin-to-send"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.1.0",
@@ -122,32 +122,42 @@ const defaultComponents: Components = {
122
122
  ),
123
123
  };
124
124
 
125
- const MemoizedReactMarkdown: FC<Options> = memo(
126
- ReactMarkdown,
127
- (prevProps, nextProps) =>
128
- prevProps.children === nextProps.children &&
129
- prevProps.components === nextProps.components,
130
- );
125
+ const MemoizedReactMarkdown: FC<Options> = memo(ReactMarkdown);
131
126
 
132
- type MarkdownProps = {
127
+ type MarkdownProps = Omit<Options, "children"> & {
133
128
  content: string;
134
- components?: Components;
135
129
  };
136
130
 
137
- export const Markdown = ({ content, components }: MarkdownProps) => {
131
+ export const Markdown = ({
132
+ content,
133
+ components,
134
+ remarkPlugins,
135
+ rehypePlugins,
136
+ ...rest
137
+ }: MarkdownProps) => {
138
138
  const mergedComponents = useMemo(
139
139
  () => ({ ...defaultComponents, ...components }),
140
140
  [components],
141
141
  );
142
+ const mergedRemarkPlugins = useMemo<Options["remarkPlugins"]>(
143
+ () => [
144
+ remarkGfm,
145
+ [remarkMath, { singleDollarTextMath: false }],
146
+ ...(remarkPlugins ?? []),
147
+ ],
148
+ [remarkPlugins],
149
+ );
150
+ const mergedRehypePlugins = useMemo<Options["rehypePlugins"]>(
151
+ () => [rehypeRaw, ...(rehypePlugins ?? [])],
152
+ [rehypePlugins],
153
+ );
142
154
  return (
143
155
  <div className="copilotKitMarkdown">
144
156
  <MemoizedReactMarkdown
157
+ {...rest}
145
158
  components={mergedComponents}
146
- remarkPlugins={[
147
- remarkGfm,
148
- [remarkMath, { singleDollarTextMath: false }],
149
- ]}
150
- rehypePlugins={[rehypeRaw]}
159
+ remarkPlugins={mergedRemarkPlugins}
160
+ rehypePlugins={mergedRehypePlugins}
151
161
  >
152
162
  {content}
153
163
  </MemoizedReactMarkdown>
@@ -112,9 +112,9 @@ export const Messages = ({
112
112
  />
113
113
  );
114
114
  })}
115
- {messages[messages.length - 1]?.role === "user" && inProgress && (
116
- <LoadingIcon />
117
- )}
115
+ {inProgress &&
116
+ (messages[messages.length - 1]?.role === "user" ||
117
+ messages[messages.length - 1]?.role === "tool") && <LoadingIcon />}
118
118
  {interrupt}
119
119
  {chatError && ErrorMessage && (
120
120
  <ErrorMessage error={chatError} isCurrentMessage />
@@ -2,6 +2,8 @@ export * from "./props";
2
2
  export { CopilotPopup } from "./Popup";
3
3
  export { CopilotSidebar } from "./Sidebar";
4
4
  export { CopilotChat } from "./Chat";
5
+ export { CopilotModal } from "./Modal";
6
+ export type { CopilotModalProps } from "./Modal";
5
7
  export { Markdown } from "./Markdown";
6
8
  export { AssistantMessage } from "./messages/AssistantMessage";
7
9
  export { UserMessage } from "./messages/UserMessage";
@@ -84,13 +84,16 @@ export function CopilotDevConsole() {
84
84
  };
85
85
 
86
86
  useEffect(() => {
87
+ if (!showDevConsole) {
88
+ return;
89
+ }
87
90
  if (dontRunTwiceInDevMode.current === true) {
88
91
  return;
89
92
  }
90
93
  dontRunTwiceInDevMode.current = true;
91
94
 
92
95
  checkForUpdates();
93
- }, []);
96
+ }, [showDevConsole]);
94
97
 
95
98
  if (!showDevConsole) {
96
99
  return null;
@@ -21,7 +21,8 @@
21
21
  margin: 8px auto 0 auto;
22
22
  justify-content: flex-start;
23
23
  flex-direction: column;
24
- width: 97%;
24
+ width: 100%;
25
+ box-sizing: border-box;
25
26
  }
26
27
 
27
28
  .copilotKitMessages::-webkit-scrollbar {
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ /**
4
+ * Tests for the sendFunction handling in usePushToTalk.
5
+ *
6
+ * Issue #3011: sendFunction (wrapping sendMessage) returns Promise<void>,
7
+ * but the code did `const message = await sendFunction(transcription);`
8
+ * then `message.id` — causing a TypeError on undefined.
9
+ *
10
+ * Fix: Guard .id access with `if (message)` check, and update SendFunction
11
+ * type to accept `Promise<Message | void>`.
12
+ */
13
+
14
+ describe("usePushToTalk sendFunction handling", () => {
15
+ it("should handle sendFunction returning void without crashing", async () => {
16
+ // Simulates what sendMessage actually returns: Promise<void>
17
+ const sendFunction = vi.fn().mockResolvedValue(undefined);
18
+ let startReadingFromMessageId: string | null = null;
19
+
20
+ const transcription = "Hello world";
21
+ const message = await sendFunction(transcription);
22
+
23
+ // Apply the same guard as the fix
24
+ if (message) {
25
+ startReadingFromMessageId = message.id;
26
+ }
27
+
28
+ // Should not have set the message id (because message is void)
29
+ expect(startReadingFromMessageId).toBeNull();
30
+ expect(sendFunction).toHaveBeenCalledWith(transcription);
31
+ });
32
+
33
+ it("should use message.id when sendFunction returns a message", async () => {
34
+ const sendFunction = vi.fn().mockResolvedValue({
35
+ id: "msg-123",
36
+ content: "test",
37
+ role: "user",
38
+ });
39
+ let startReadingFromMessageId: string | null = null;
40
+
41
+ const message = await sendFunction("Hello");
42
+
43
+ if (message) {
44
+ startReadingFromMessageId = message.id;
45
+ }
46
+
47
+ expect(startReadingFromMessageId).toBe("msg-123");
48
+ });
49
+ });
@@ -58,6 +58,7 @@ const startRecording = async (
58
58
 
59
59
  const stopRecording = (
60
60
  mediaRecorderRef: MutableRefObject<MediaRecorder | null>,
61
+ mediaStreamRef?: MutableRefObject<MediaStream | null>,
61
62
  ) => {
62
63
  if (
63
64
  mediaRecorderRef.current &&
@@ -65,15 +66,22 @@ const stopRecording = (
65
66
  ) {
66
67
  mediaRecorderRef.current.stop();
67
68
  }
69
+ // Release microphone tracks to free the device
70
+ if (mediaStreamRef?.current) {
71
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
72
+ mediaStreamRef.current = null;
73
+ }
68
74
  };
69
75
 
70
76
  const transcribeAudio = async (
71
77
  recordedChunks: Blob[],
72
78
  transcribeAudioUrl: string,
79
+ mediaType: string = "audio/mp4",
73
80
  ) => {
74
- const completeBlob = new Blob(recordedChunks, { type: "audio/mp4" });
81
+ const extension = mediaType.split("/")[1] || "mp4";
82
+ const completeBlob = new Blob(recordedChunks, { type: mediaType });
75
83
  const formData = new FormData();
76
- formData.append("file", completeBlob, "recording.mp4");
84
+ formData.append("file", completeBlob, `recording.${extension}`);
77
85
 
78
86
  const response = await fetch(transcribeAudioUrl, {
79
87
  method: "POST",
@@ -112,14 +120,16 @@ const playAudioResponse = (
112
120
 
113
121
  export type PushToTalkState = "idle" | "recording" | "transcribing";
114
122
 
115
- export type SendFunction = (text: string) => Promise<Message>;
123
+ export type SendFunction = (text: string) => Promise<Message | void>;
116
124
 
117
125
  export const usePushToTalk = ({
118
126
  sendFunction,
119
127
  inProgress,
128
+ mediaType = "audio/mp4",
120
129
  }: {
121
130
  sendFunction: SendFunction;
122
131
  inProgress: boolean;
132
+ mediaType?: string;
123
133
  }) => {
124
134
  const [pushToTalkState, setPushToTalkState] =
125
135
  useState<PushToTalkState>("idle");
@@ -146,22 +156,25 @@ export const usePushToTalk = ({
146
156
  },
147
157
  );
148
158
  } else {
149
- stopRecording(mediaRecorderRef);
159
+ stopRecording(mediaRecorderRef, mediaStreamRef);
150
160
  if (pushToTalkState === "transcribing") {
151
161
  transcribeAudio(
152
162
  recordedChunks.current,
153
163
  context.copilotApiConfig.transcribeAudioUrl!,
164
+ mediaType,
154
165
  ).then(async (transcription) => {
155
166
  recordedChunks.current = [];
156
167
  setPushToTalkState("idle");
157
168
  const message = await sendFunction(transcription);
158
- setStartReadingFromMessageId(message.id);
169
+ if (message) {
170
+ setStartReadingFromMessageId(message.id);
171
+ }
159
172
  });
160
173
  }
161
174
  }
162
175
 
163
176
  return () => {
164
- stopRecording(mediaRecorderRef);
177
+ stopRecording(mediaRecorderRef, mediaStreamRef);
165
178
  };
166
179
  }, [pushToTalkState]);
167
180