@convex-dev/agent 0.1.4-alpha.0 → 0.1.5-alpha.0

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.
Files changed (165) hide show
  1. package/README.md +24 -35
  2. package/dist/commonjs/client/createTool.d.ts +3 -5
  3. package/dist/commonjs/client/createTool.d.ts.map +1 -1
  4. package/dist/commonjs/client/createTool.js +1 -0
  5. package/dist/commonjs/client/createTool.js.map +1 -1
  6. package/dist/commonjs/client/index.d.ts +6 -6
  7. package/dist/commonjs/client/index.d.ts.map +1 -1
  8. package/dist/commonjs/client/index.js.map +1 -1
  9. package/dist/commonjs/client/streaming.d.ts +5 -5
  10. package/dist/commonjs/client/streaming.d.ts.map +1 -1
  11. package/dist/commonjs/client/streaming.js +3 -2
  12. package/dist/commonjs/client/streaming.js.map +1 -1
  13. package/dist/commonjs/component/files.d.ts +2 -2
  14. package/dist/commonjs/component/files.d.ts.map +1 -1
  15. package/dist/commonjs/component/files.js.map +1 -1
  16. package/dist/commonjs/component/messages.d.ts +24 -24
  17. package/dist/commonjs/component/messages.d.ts.map +1 -1
  18. package/dist/commonjs/component/messages.js +4 -5
  19. package/dist/commonjs/component/messages.js.map +1 -1
  20. package/dist/commonjs/component/schema.d.ts +45 -45
  21. package/dist/commonjs/component/schema.d.ts.map +1 -1
  22. package/dist/commonjs/component/schema.js.map +1 -1
  23. package/dist/commonjs/component/streams.d.ts.map +1 -1
  24. package/dist/commonjs/component/streams.js +4 -7
  25. package/dist/commonjs/component/streams.js.map +1 -1
  26. package/dist/commonjs/component/threads.d.ts +8 -8
  27. package/dist/commonjs/component/threads.js +3 -3
  28. package/dist/commonjs/component/threads.js.map +1 -1
  29. package/dist/commonjs/component/users.js +4 -4
  30. package/dist/commonjs/component/users.js.map +1 -1
  31. package/dist/commonjs/component/vector/index.d.ts +3 -3
  32. package/dist/commonjs/component/vector/index.d.ts.map +1 -1
  33. package/dist/commonjs/component/vector/index.js +4 -5
  34. package/dist/commonjs/component/vector/index.js.map +1 -1
  35. package/dist/commonjs/component/vector/tables.d.ts +5 -7
  36. package/dist/commonjs/component/vector/tables.d.ts.map +1 -1
  37. package/dist/commonjs/mapping.d.ts +2 -2
  38. package/dist/commonjs/mapping.d.ts.map +1 -1
  39. package/dist/commonjs/mapping.js.map +1 -1
  40. package/dist/commonjs/react/deltas.d.ts +29 -0
  41. package/dist/commonjs/react/deltas.d.ts.map +1 -0
  42. package/dist/commonjs/react/deltas.js +267 -0
  43. package/dist/commonjs/react/deltas.js.map +1 -0
  44. package/dist/commonjs/react/index.d.ts +4 -49
  45. package/dist/commonjs/react/index.d.ts.map +1 -1
  46. package/dist/commonjs/react/index.js +15 -446
  47. package/dist/commonjs/react/index.js.map +1 -1
  48. package/dist/commonjs/react/optimisticallySendMessage.d.ts +8 -0
  49. package/dist/commonjs/react/optimisticallySendMessage.d.ts.map +1 -0
  50. package/dist/commonjs/react/optimisticallySendMessage.js +40 -0
  51. package/dist/commonjs/react/optimisticallySendMessage.js.map +1 -0
  52. package/dist/commonjs/react/toUIMessages.d.ts +3 -3
  53. package/dist/commonjs/react/toUIMessages.d.ts.map +1 -1
  54. package/dist/commonjs/react/toUIMessages.js +4 -2
  55. package/dist/commonjs/react/toUIMessages.js.map +1 -1
  56. package/dist/commonjs/react/types.d.ts +26 -0
  57. package/dist/commonjs/react/types.d.ts.map +1 -0
  58. package/dist/commonjs/react/types.js +2 -0
  59. package/dist/commonjs/react/types.js.map +1 -0
  60. package/dist/commonjs/react/useSmoothText.d.ts +20 -0
  61. package/dist/commonjs/react/useSmoothText.d.ts.map +1 -0
  62. package/dist/commonjs/react/useSmoothText.js +50 -0
  63. package/dist/commonjs/react/useSmoothText.js.map +1 -0
  64. package/dist/commonjs/shared.d.ts +2 -2
  65. package/dist/commonjs/shared.d.ts.map +1 -1
  66. package/dist/commonjs/validators.d.ts +1 -1
  67. package/dist/commonjs/validators.d.ts.map +1 -1
  68. package/dist/commonjs/validators.js +2 -2
  69. package/dist/commonjs/validators.js.map +1 -1
  70. package/dist/commonjs.tsbuildinfo +1 -0
  71. package/dist/esm/client/createTool.d.ts +3 -5
  72. package/dist/esm/client/createTool.d.ts.map +1 -1
  73. package/dist/esm/client/createTool.js +1 -0
  74. package/dist/esm/client/createTool.js.map +1 -1
  75. package/dist/esm/client/index.d.ts +6 -6
  76. package/dist/esm/client/index.d.ts.map +1 -1
  77. package/dist/esm/client/index.js.map +1 -1
  78. package/dist/esm/client/streaming.d.ts +5 -5
  79. package/dist/esm/client/streaming.d.ts.map +1 -1
  80. package/dist/esm/client/streaming.js +3 -2
  81. package/dist/esm/client/streaming.js.map +1 -1
  82. package/dist/esm/component/files.d.ts +2 -2
  83. package/dist/esm/component/files.d.ts.map +1 -1
  84. package/dist/esm/component/files.js.map +1 -1
  85. package/dist/esm/component/messages.d.ts +24 -24
  86. package/dist/esm/component/messages.d.ts.map +1 -1
  87. package/dist/esm/component/messages.js +4 -5
  88. package/dist/esm/component/messages.js.map +1 -1
  89. package/dist/esm/component/schema.d.ts +45 -45
  90. package/dist/esm/component/schema.d.ts.map +1 -1
  91. package/dist/esm/component/schema.js.map +1 -1
  92. package/dist/esm/component/streams.d.ts.map +1 -1
  93. package/dist/esm/component/streams.js +4 -7
  94. package/dist/esm/component/streams.js.map +1 -1
  95. package/dist/esm/component/threads.d.ts +8 -8
  96. package/dist/esm/component/threads.js +3 -3
  97. package/dist/esm/component/threads.js.map +1 -1
  98. package/dist/esm/component/users.js +4 -4
  99. package/dist/esm/component/users.js.map +1 -1
  100. package/dist/esm/component/vector/index.d.ts +3 -3
  101. package/dist/esm/component/vector/index.d.ts.map +1 -1
  102. package/dist/esm/component/vector/index.js +4 -5
  103. package/dist/esm/component/vector/index.js.map +1 -1
  104. package/dist/esm/component/vector/tables.d.ts +5 -7
  105. package/dist/esm/component/vector/tables.d.ts.map +1 -1
  106. package/dist/esm/mapping.d.ts +2 -2
  107. package/dist/esm/mapping.d.ts.map +1 -1
  108. package/dist/esm/mapping.js.map +1 -1
  109. package/dist/esm/react/deltas.d.ts +29 -0
  110. package/dist/esm/react/deltas.d.ts.map +1 -0
  111. package/dist/esm/react/deltas.js +267 -0
  112. package/dist/esm/react/deltas.js.map +1 -0
  113. package/dist/esm/react/index.d.ts +4 -49
  114. package/dist/esm/react/index.d.ts.map +1 -1
  115. package/dist/esm/react/index.js +15 -446
  116. package/dist/esm/react/index.js.map +1 -1
  117. package/dist/esm/react/optimisticallySendMessage.d.ts +8 -0
  118. package/dist/esm/react/optimisticallySendMessage.d.ts.map +1 -0
  119. package/dist/esm/react/optimisticallySendMessage.js +40 -0
  120. package/dist/esm/react/optimisticallySendMessage.js.map +1 -0
  121. package/dist/esm/react/toUIMessages.d.ts +3 -3
  122. package/dist/esm/react/toUIMessages.d.ts.map +1 -1
  123. package/dist/esm/react/toUIMessages.js +4 -2
  124. package/dist/esm/react/toUIMessages.js.map +1 -1
  125. package/dist/esm/react/types.d.ts +26 -0
  126. package/dist/esm/react/types.d.ts.map +1 -0
  127. package/dist/esm/react/types.js +2 -0
  128. package/dist/esm/react/types.js.map +1 -0
  129. package/dist/esm/react/useSmoothText.d.ts +20 -0
  130. package/dist/esm/react/useSmoothText.d.ts.map +1 -0
  131. package/dist/esm/react/useSmoothText.js +50 -0
  132. package/dist/esm/react/useSmoothText.js.map +1 -0
  133. package/dist/esm/shared.d.ts +2 -2
  134. package/dist/esm/shared.d.ts.map +1 -1
  135. package/dist/esm/validators.d.ts +1 -1
  136. package/dist/esm/validators.d.ts.map +1 -1
  137. package/dist/esm/validators.js +2 -2
  138. package/dist/esm/validators.js.map +1 -1
  139. package/dist/esm.tsbuildinfo +1 -0
  140. package/package.json +5 -1
  141. package/src/client/createTool.ts +3 -5
  142. package/src/client/index.test.ts +1 -1
  143. package/src/client/index.ts +9 -10
  144. package/src/client/streaming.ts +15 -5
  145. package/src/component/files.ts +2 -2
  146. package/src/component/messages.test.ts +4 -4
  147. package/src/component/messages.ts +15 -15
  148. package/src/component/schema.ts +1 -1
  149. package/src/component/streams.ts +8 -9
  150. package/src/component/threads.ts +6 -6
  151. package/src/component/users.ts +7 -7
  152. package/src/component/vector/index.ts +12 -8
  153. package/src/component/vector/tables.ts +10 -10
  154. package/src/mapping.ts +4 -4
  155. package/src/react/deltas.test.ts +298 -0
  156. package/src/react/deltas.ts +340 -0
  157. package/src/react/index.ts +26 -589
  158. package/src/react/optimisticallySendMessage.ts +46 -0
  159. package/src/react/toUIMessages.test.ts +301 -0
  160. package/src/react/toUIMessages.ts +9 -5
  161. package/src/react/types.ts +52 -0
  162. package/src/react/useSmoothText.ts +76 -0
  163. package/src/shared.ts +2 -2
  164. package/src/validators.test.ts +22 -22
  165. package/src/validators.ts +8 -2
@@ -0,0 +1,46 @@
1
+ import { insertAtTop } from "convex/react";
2
+ import type { MessageDoc } from "../client";
3
+ import type { OptimisticLocalStore } from "convex/browser";
4
+ import type { ThreadQuery } from "./types";
5
+
6
+ export function optimisticallySendMessage(
7
+ query: ThreadQuery<unknown, MessageDoc>
8
+ ): (
9
+ store: OptimisticLocalStore,
10
+ args: { threadId: string; prompt: string }
11
+ ) => void {
12
+ return (store, args) => {
13
+ const queries = store.getAllQueries(query);
14
+ let maxOrder = 0;
15
+ let maxStepOrder = 0;
16
+ for (const q of queries) {
17
+ if (q.args?.threadId !== args.threadId) continue;
18
+ if (q.args.streamArgs) continue;
19
+ for (const m of q.value?.page ?? []) {
20
+ maxOrder = Math.max(maxOrder, m.order);
21
+ maxStepOrder = Math.max(maxStepOrder, m.stepOrder);
22
+ }
23
+ }
24
+ const order = maxOrder + 1;
25
+ const stepOrder = 0;
26
+ insertAtTop({
27
+ paginatedQuery: query,
28
+ argsToMatch: { threadId: args.threadId, streamArgs: undefined },
29
+ item: {
30
+ _creationTime: Date.now(),
31
+ _id: crypto.randomUUID(),
32
+ order,
33
+ stepOrder,
34
+ status: "pending",
35
+ threadId: args.threadId,
36
+ tool: false,
37
+ message: {
38
+ role: "user",
39
+ content: args.prompt,
40
+ },
41
+ text: args.prompt,
42
+ },
43
+ localQueryStore: store,
44
+ });
45
+ };
46
+ }
@@ -0,0 +1,301 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { toUIMessages } from "./toUIMessages";
3
+ import type { MessageDoc } from "../client";
4
+
5
+ // Helper to create a base message doc
6
+ function baseMessageDoc(overrides: Partial<MessageDoc> = {}): MessageDoc {
7
+ return {
8
+ _id: "msg1",
9
+ _creationTime: Date.now(),
10
+ order: 1,
11
+ stepOrder: 0,
12
+ status: "success",
13
+ threadId: "thread1",
14
+ tool: false,
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ describe("toUIMessages", () => {
20
+ it("handles user message", () => {
21
+ const messages = [
22
+ baseMessageDoc({
23
+ message: {
24
+ role: "user",
25
+ content: "Hello!",
26
+ },
27
+ text: "Hello!",
28
+ }),
29
+ ];
30
+ const uiMessages = toUIMessages(messages);
31
+ expect(uiMessages).toHaveLength(1);
32
+ expect(uiMessages[0].role).toBe("user");
33
+ expect(uiMessages[0].content).toBe("Hello!");
34
+ expect(uiMessages[0].parts[0]).toEqual({ type: "text", text: "Hello!" });
35
+ });
36
+
37
+ it("handles assistant message", () => {
38
+ const messages = [
39
+ baseMessageDoc({
40
+ message: {
41
+ role: "assistant",
42
+ content: "Hi, how can I help?",
43
+ },
44
+ text: "Hi, how can I help?",
45
+ }),
46
+ ];
47
+ const uiMessages = toUIMessages(messages);
48
+ expect(uiMessages).toHaveLength(1);
49
+ expect(uiMessages[0].role).toBe("assistant");
50
+ expect(uiMessages[0].content).toBe("Hi, how can I help?");
51
+ expect(uiMessages[0].parts[0]).toEqual({
52
+ type: "text",
53
+ text: "Hi, how can I help?",
54
+ });
55
+ });
56
+
57
+ it("handles multiple messages", () => {
58
+ const messages = [
59
+ baseMessageDoc({
60
+ message: {
61
+ role: "user",
62
+ content: "Hello!",
63
+ },
64
+ text: "Hello!",
65
+ }),
66
+ baseMessageDoc({
67
+ message: {
68
+ role: "assistant",
69
+ content: [
70
+ {
71
+ type: "reasoning",
72
+ text: "I'm thinking...",
73
+ },
74
+ {
75
+ type: "redacted-reasoning",
76
+ data: "asdfasdfasdf",
77
+ },
78
+ {
79
+ type: "text",
80
+ text: "I'm thinking...",
81
+ },
82
+ {
83
+ type: "file",
84
+ mimeType: "text/plain",
85
+ data: "asdfasdfasdf",
86
+ },
87
+ {
88
+ type: "tool-call",
89
+ toolName: "myTool",
90
+ toolCallId: "call1",
91
+ args: "",
92
+ },
93
+ ],
94
+ },
95
+ tool: true,
96
+ files: [
97
+ {
98
+ mimeType: "text/plain",
99
+ data: "asdfasdfasdf",
100
+ },
101
+ ],
102
+ reasoning: "I'm thinking...",
103
+ text: "I'm thinking...",
104
+ }),
105
+ baseMessageDoc({
106
+ message: {
107
+ role: "tool",
108
+ content: [
109
+ {
110
+ type: "tool-result",
111
+ toolCallId: "call1",
112
+ toolName: "myTool",
113
+ result: "42",
114
+ },
115
+ ],
116
+ },
117
+ text: "42",
118
+ tool: true,
119
+ }),
120
+ ];
121
+ const uiMessages = toUIMessages(messages);
122
+ expect(uiMessages).toHaveLength(2);
123
+ expect(uiMessages[0].role).toBe("user");
124
+ expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength(
125
+ 1
126
+ );
127
+ expect(uiMessages[1].role).toBe("assistant");
128
+ expect(
129
+ uiMessages[1].parts.filter((p) => p.type === "tool-invocation")
130
+ ).toHaveLength(1);
131
+ expect(
132
+ uiMessages[1].parts.filter((p) => p.type === "tool-invocation")[0]
133
+ .toolInvocation
134
+ ).toEqual({
135
+ toolName: "myTool",
136
+ toolCallId: "call1",
137
+ args: "",
138
+ state: "result",
139
+ result: "42",
140
+ step: 0,
141
+ });
142
+ });
143
+
144
+ it("handles multiple text and reasoning parts", () => {
145
+ const messages = [
146
+ baseMessageDoc({
147
+ message: {
148
+ role: "assistant",
149
+ content: [
150
+ {
151
+ type: "reasoning",
152
+ text: "I'm thinking...",
153
+ },
154
+ {
155
+ type: "text",
156
+ text: "Here's one idea.",
157
+ },
158
+ {
159
+ type: "reasoning",
160
+ text: "I'm thinking...",
161
+ },
162
+ {
163
+ type: "text",
164
+ text: "Here's another idea.",
165
+ },
166
+ ],
167
+ },
168
+ reasoning: "I'm thinking...I'm thinking...",
169
+ text: "Here's one idea. Here's another idea.",
170
+ }),
171
+ ];
172
+ const uiMessages = toUIMessages(messages);
173
+ expect(uiMessages).toHaveLength(1);
174
+ expect(uiMessages[0].role).toBe("assistant");
175
+ expect(uiMessages[0].content).toBe("Here's one idea. Here's another idea.");
176
+ expect(
177
+ uiMessages[0].parts.filter((p) => p.type === "reasoning")
178
+ ).toHaveLength(1);
179
+ expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength(
180
+ 1
181
+ );
182
+ expect(uiMessages[0].parts.filter((p) => p.type === "text")[0].text).toBe(
183
+ "Here's one idea. Here's another idea."
184
+ );
185
+ });
186
+
187
+ it("handles system message", () => {
188
+ const messages = [
189
+ baseMessageDoc({
190
+ message: {
191
+ role: "system",
192
+ content: "System message here",
193
+ },
194
+ text: "System message here",
195
+ }),
196
+ ];
197
+ const uiMessages = toUIMessages(messages);
198
+ expect(uiMessages).toHaveLength(1);
199
+ expect(uiMessages[0].role).toBe("system");
200
+ expect(uiMessages[0].content).toBe("System message here");
201
+ expect(uiMessages[0].parts[0]).toEqual({
202
+ type: "text",
203
+ text: "System message here",
204
+ });
205
+ });
206
+
207
+ it("handles tool call", () => {
208
+ const messages = [
209
+ baseMessageDoc({
210
+ message: {
211
+ role: "assistant",
212
+ content: [
213
+ {
214
+ type: "tool-call",
215
+ toolName: "myTool",
216
+ toolCallId: "call1",
217
+ args: "",
218
+ },
219
+ ],
220
+ },
221
+ text: "",
222
+ }),
223
+ ];
224
+ const uiMessages = toUIMessages(messages);
225
+ expect(uiMessages).toHaveLength(1);
226
+ expect(uiMessages[0].role).toBe("assistant");
227
+ expect(
228
+ uiMessages[0].parts.filter((p) => p.type === "tool-invocation")
229
+ ).toHaveLength(1);
230
+ expect(
231
+ uiMessages[0].parts.filter((p) => p.type === "tool-invocation")[0]
232
+ .toolInvocation
233
+ ).toEqual({
234
+ toolName: "myTool",
235
+ toolCallId: "call1",
236
+ args: "",
237
+ state: "call",
238
+ step: 0,
239
+ });
240
+ });
241
+
242
+ it("handles tool result", () => {
243
+ const messages = [
244
+ baseMessageDoc({
245
+ tool: true,
246
+ message: {
247
+ role: "assistant",
248
+ content: [
249
+ {
250
+ type: "tool-call",
251
+ toolName: "myTool",
252
+ toolCallId: "call1",
253
+ args: "",
254
+ },
255
+ ],
256
+ },
257
+ text: "",
258
+ }),
259
+ baseMessageDoc({
260
+ message: {
261
+ role: "tool",
262
+ content: [
263
+ {
264
+ type: "tool-result",
265
+ toolCallId: "call1",
266
+ toolName: "myTool",
267
+ result: "42",
268
+ },
269
+ ],
270
+ },
271
+ text: "",
272
+ }),
273
+ ];
274
+ const uiMessages = toUIMessages(messages);
275
+ expect(uiMessages).toHaveLength(1);
276
+ expect(uiMessages[0].role).toBe("assistant");
277
+ // Should have a tool-invocation part
278
+ expect(uiMessages[0].parts.some((p) => p.type === "tool-invocation")).toBe(
279
+ true
280
+ );
281
+ });
282
+
283
+ it("does not duplicate text content", () => {
284
+ const messages = [
285
+ baseMessageDoc({
286
+ message: {
287
+ role: "assistant",
288
+ content: "Hello!",
289
+ },
290
+ text: "Hello!",
291
+ }),
292
+ ];
293
+ const uiMessages = toUIMessages(messages);
294
+ // There should only be one text part
295
+ const textParts = uiMessages[0].parts.filter((p) => p.type === "text");
296
+ expect(textParts).toHaveLength(1);
297
+ expect(textParts[0].text).toBe("Hello!");
298
+ });
299
+
300
+ // Add more tests for array content, tool calls, etc. as needed
301
+ });
@@ -1,8 +1,8 @@
1
- import { UIMessage as AIUIMessage } from "ai";
2
1
  import type { ToolInvocationUIPart } from "@ai-sdk/ui-utils";
3
- import { MessageDoc } from "../client";
2
+ import type { UIMessage as AIUIMessage } from "ai";
3
+ import type { MessageDoc } from "../client";
4
4
  import { deserializeMessage, toUIFilePart } from "../mapping";
5
- import { MessageStatus } from "../validators";
5
+ import type { MessageStatus } from "../validators";
6
6
 
7
7
  export type UIMessage = AIUIMessage & {
8
8
  key: string;
@@ -64,7 +64,7 @@ export function toUIMessages(
64
64
  assistantMessage = {
65
65
  ...common,
66
66
  role: "assistant",
67
- content: message.text ?? "",
67
+ content: "",
68
68
  parts: [],
69
69
  };
70
70
  uiMessages.push(assistantMessage);
@@ -152,7 +152,11 @@ export function toUIMessages(
152
152
  }
153
153
  }
154
154
  }
155
- if (!message.tool) {
155
+ if (
156
+ !message.tool &&
157
+ assistantMessage &&
158
+ assistantMessage.parts.length > 0
159
+ ) {
156
160
  // Reset it so the next set of tool calls will create a new assistant message
157
161
  assistantMessage = undefined;
158
162
  }
@@ -0,0 +1,52 @@
1
+ import type { BetterOmit, Expand } from "convex-helpers";
2
+ import type {
3
+ FunctionArgs,
4
+ FunctionReference,
5
+ PaginationOptions,
6
+ PaginationResult,
7
+ } from "convex/server";
8
+ import type { MessageDoc } from "../client";
9
+ import type { SyncStreamsReturnValue } from "../client/types";
10
+ import type { StreamArgs } from "../validators";
11
+
12
+ export type ThreadQuery<
13
+ Args = unknown,
14
+ M extends MessageDoc = MessageDoc,
15
+ > = FunctionReference<
16
+ "query",
17
+ "public",
18
+ {
19
+ threadId: string;
20
+ paginationOpts: PaginationOptions;
21
+ // TODO: will this allow passing a function that doesn't have this param?
22
+ /**
23
+ * If { stream: true } is passed, it will also query for stream deltas.
24
+ * In order for this to work, the query must take as an argument streamArgs.
25
+ */
26
+ streamArgs?: StreamArgs;
27
+ } & Args,
28
+ PaginationResult<M> & { streams?: SyncStreamsReturnValue }
29
+ >;
30
+
31
+ export type ThreadStreamQuery<
32
+ Args = Record<string, unknown>,
33
+ M extends MessageDoc = MessageDoc,
34
+ > = FunctionReference<
35
+ "query",
36
+ "public",
37
+ {
38
+ threadId: string;
39
+ paginationOpts: PaginationOptions;
40
+ streamArgs?: StreamArgs; // required for stream query
41
+ } & Args,
42
+ PaginationResult<M> & { streams: SyncStreamsReturnValue }
43
+ >;
44
+
45
+ export type ThreadMessagesArgs<Query extends ThreadQuery<unknown, MessageDoc>> =
46
+ Query extends ThreadQuery<unknown, MessageDoc>
47
+ ? Expand<BetterOmit<FunctionArgs<Query>, "paginationOpts" | "streamArgs">>
48
+ : never;
49
+
50
+ export type ThreadMessagesResult<
51
+ Query extends ThreadQuery<unknown, MessageDoc>,
52
+ > = Query extends ThreadQuery<unknown, infer M> ? M : never;
@@ -0,0 +1,76 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ const FPS = 20;
4
+ const MS_PER_FRAME = 1000 / FPS;
5
+ const MAX_TIME_JUMP_MS = 250;
6
+ /**
7
+ * A hook that smoothly displays text as it is streamed.
8
+ *
9
+ * @param text The text to display. Pass in the full text each time.
10
+ * @param charsPerSec The number of characters to display per second.
11
+ * @returns A tuple of the visible text and the state of the smooth text,
12
+ * including the current cursor position and whether it's still streaming.
13
+ * This allows you to decide if it's too far behind and you want to adjust
14
+ * the charsPerSec or just prefer the full text.
15
+ */
16
+ export function useSmoothText(
17
+ text: string,
18
+ {
19
+ charsPerSec = 256,
20
+ }: {
21
+ /**
22
+ * The number of characters to display per second.
23
+ */
24
+ charsPerSec?: number;
25
+ } = {}
26
+ ): [string, { cursor: number; isStreaming: boolean }] {
27
+ const [visibleText, setVisibleText] = useState(text);
28
+ const smoothState = useRef({
29
+ tick: Date.now() + (text.length * 1000) / charsPerSec,
30
+ cursor: text.length,
31
+ start: Date.now(),
32
+ initialLength: text.length,
33
+ charsPerMs: charsPerSec / 1000,
34
+ });
35
+
36
+ const isStreaming = smoothState.current.cursor < text.length;
37
+
38
+ useEffect(() => {
39
+ if (!isStreaming) {
40
+ return;
41
+ }
42
+ const latestCharsPerMs =
43
+ (text.length - smoothState.current.initialLength) /
44
+ (Date.now() - smoothState.current.start);
45
+ // Smooth out the charsPerSec by averaging it with the previous value.
46
+ smoothState.current.charsPerMs = Math.min(
47
+ (2 * latestCharsPerMs + smoothState.current.charsPerMs) / 3,
48
+ smoothState.current.charsPerMs * 2
49
+ );
50
+
51
+ function update() {
52
+ if (smoothState.current.cursor >= text.length) {
53
+ return;
54
+ }
55
+ const now = Date.now();
56
+ const timeSinceLastUpdate = Math.min(
57
+ MAX_TIME_JUMP_MS,
58
+ now - smoothState.current.tick
59
+ );
60
+ const chars = Math.floor(
61
+ timeSinceLastUpdate * smoothState.current.charsPerMs
62
+ );
63
+ smoothState.current.cursor = Math.min(
64
+ smoothState.current.cursor + chars,
65
+ text.length
66
+ );
67
+ smoothState.current.tick = now;
68
+ setVisibleText(text.slice(0, smoothState.current.cursor));
69
+ }
70
+ update();
71
+ const interval = setInterval(update, MS_PER_FRAME);
72
+ return () => clearInterval(interval);
73
+ }, [text, isStreaming, charsPerSec]);
74
+
75
+ return [visibleText, { cursor: smoothState.current.cursor, isStreaming }];
76
+ }
package/src/shared.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { CoreMessage } from "ai";
2
- import { Message } from "./validators";
1
+ import type { CoreMessage } from "ai";
2
+ import type { Message } from "./validators";
3
3
 
4
4
  export const DEFAULT_RECENT_MESSAGES = 100;
5
5
 
@@ -1,5 +1,24 @@
1
- import { Infer } from "convex/values";
1
+ import type {
2
+ TextStreamPart as AITextStreamPart,
3
+ AssistantContent,
4
+ CoreAssistantMessage,
5
+ CoreMessage,
6
+ CoreSystemMessage,
7
+ CoreToolMessage,
8
+ CoreUserMessage,
9
+ FilePart,
10
+ ImagePart,
11
+ TextPart,
12
+ ToolCallPart,
13
+ ToolContent,
14
+ ToolSet,
15
+ UserContent,
16
+ } from "ai";
17
+ import type { Infer } from "convex/values";
2
18
  import { expectTypeOf, test } from "vitest";
19
+ import type { ContextOptions, StorageOptions } from "./client";
20
+ import type { SerializeUrlsAndUint8Arrays } from "./mapping";
21
+ import type { TextStreamPart } from "./validators";
3
22
  import {
4
23
  vAssistantContent,
5
24
  vAssistantMessage,
@@ -15,28 +34,9 @@ import {
15
34
  vToolCallPart,
16
35
  vToolContent,
17
36
  vToolMessage,
37
+ vUserContent,
18
38
  vUserMessage,
19
39
  } from "./validators";
20
- import { vUserContent } from "./validators";
21
- import type { TextStreamPart } from "./validators";
22
- import {
23
- AssistantContent,
24
- CoreAssistantMessage,
25
- CoreMessage,
26
- CoreSystemMessage,
27
- CoreToolMessage,
28
- CoreUserMessage,
29
- FilePart,
30
- ImagePart,
31
- TextPart,
32
- TextStreamPart as AITextStreamPart,
33
- ToolCallPart,
34
- ToolContent,
35
- ToolSet,
36
- UserContent,
37
- } from "ai";
38
- import { SerializeUrlsAndUint8Arrays } from "./mapping";
39
- import { ContextOptions, StorageOptions } from "./client";
40
40
 
41
41
  // type assertion
42
42
  type OurUserContent = SerializeUrlsAndUint8Arrays<UserContent>;
@@ -125,4 +125,4 @@ type StreamPart = Extract<
125
125
  >;
126
126
  expectTypeOf<StreamPart>().toExtend<TextStreamPart>();
127
127
 
128
- test("noop", () => { });
128
+ test("noop", () => {});
package/src/validators.ts CHANGED
@@ -1,5 +1,11 @@
1
- import { Infer, ObjectType, v, Validator, Value } from "convex/values";
2
- import { vVectorDimension } from "./component/vector/tables";
1
+ import {
2
+ v,
3
+ type Infer,
4
+ type ObjectType,
5
+ type Validator,
6
+ type Value,
7
+ } from "convex/values";
8
+ import { vVectorDimension } from "./component/vector/tables.js";
3
9
 
4
10
  // const deprecated = v.optional(v.any()) as unknown as VNull<unknown, "optional">;
5
11