@ai-sdk/vue 0.0.44 → 0.0.45

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.
@@ -1,272 +0,0 @@
1
- import { formatStreamPart } from '@ai-sdk/ui-utils';
2
- import {
3
- mockFetchDataStream,
4
- mockFetchDataStreamWithGenerator,
5
- } from '@ai-sdk/ui-utils/test';
6
- import '@testing-library/jest-dom/vitest';
7
- import { cleanup, findByText, render, screen } from '@testing-library/vue';
8
- import userEvent from '@testing-library/user-event';
9
- import TestChatAssistantStreamComponent from './TestChatAssistantStreamComponent.vue';
10
- import TestChatAssistantThreadChangeComponent from './TestChatAssistantThreadChangeComponent.vue';
11
-
12
- describe('stream data stream', () => {
13
- // Render the TestChatAssistantStreamComponent before each test
14
- beforeEach(() => {
15
- render(TestChatAssistantStreamComponent);
16
- });
17
-
18
- // Cleanup after each test
19
- afterEach(() => {
20
- vi.restoreAllMocks();
21
- cleanup();
22
- });
23
-
24
- it('should show streamed response', async () => {
25
- // Mock the fetch data stream
26
- const { requestBody } = mockFetchDataStream({
27
- url: 'https://example.com/api/assistant',
28
- chunks: [
29
- // Format the stream part
30
- formatStreamPart('assistant_control_data', {
31
- threadId: 't0',
32
- messageId: 'm0',
33
- }),
34
- formatStreamPart('assistant_message', {
35
- id: 'm0',
36
- role: 'assistant',
37
- content: [{ type: 'text', text: { value: '' } }],
38
- }),
39
- // Text parts
40
- '0:"Hello"\n',
41
- '0:", world"\n',
42
- '0:"."\n',
43
- ],
44
- });
45
-
46
- // Click the button
47
- await userEvent.click(screen.getByTestId('do-append'));
48
-
49
- // Find the message-0 element
50
- await screen.findByTestId('message-0');
51
- // Expect the message-0 element to have the text content 'User: hi'
52
- expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
53
-
54
- // Find the message-1 element
55
- await screen.findByTestId('message-1');
56
- // Expect the message-1 element to have the text content 'AI: Hello, world.'
57
- expect(screen.getByTestId('message-1')).toHaveTextContent(
58
- 'AI: Hello, world.',
59
- );
60
-
61
- expect(await requestBody).toStrictEqual(
62
- JSON.stringify({
63
- message: 'hi',
64
- threadId: null,
65
- }),
66
- );
67
- });
68
-
69
- describe('loading state', () => {
70
- it('should show loading state', async () => {
71
- let finishGeneration: ((value?: unknown) => void) | undefined;
72
-
73
- const finishGenerationPromise = new Promise(resolve => {
74
- finishGeneration = resolve;
75
- });
76
-
77
- // Mock the fetch data stream with generator
78
- mockFetchDataStreamWithGenerator({
79
- url: 'https://example.com/api/assistant',
80
- chunkGenerator: (async function* generate() {
81
- const encoder = new TextEncoder();
82
-
83
- yield encoder.encode(
84
- formatStreamPart('assistant_control_data', {
85
- threadId: 't0',
86
- messageId: 'm1',
87
- }),
88
- );
89
-
90
- yield encoder.encode(
91
- formatStreamPart('assistant_message', {
92
- id: 'm1',
93
- role: 'assistant',
94
- content: [{ type: 'text', text: { value: '' } }],
95
- }),
96
- );
97
-
98
- yield encoder.encode('0:"Hello"\n');
99
-
100
- await finishGenerationPromise;
101
- })(),
102
- });
103
-
104
- // Click the button
105
- await userEvent.click(screen.getByTestId('do-append'));
106
-
107
- // Find the loading element and expect it to be in progress
108
- await screen.findByTestId('status');
109
- expect(screen.getByTestId('status')).toHaveTextContent('in_progress');
110
-
111
- // Resolve the finishGenerationPromise
112
- finishGeneration?.();
113
-
114
- // Find the loading element and expect it to be awaiting a message
115
- await findByText(await screen.findByTestId('status'), 'awaiting_message');
116
- expect(screen.getByTestId('status')).toHaveTextContent(
117
- 'awaiting_message',
118
- );
119
- });
120
- });
121
- });
122
-
123
- describe('Thread management', () => {
124
- beforeEach(() => {
125
- render(TestChatAssistantThreadChangeComponent);
126
- });
127
-
128
- afterEach(() => {
129
- vi.restoreAllMocks();
130
- cleanup();
131
- });
132
-
133
- it('create new thread', async () => {
134
- await screen.findByTestId('thread-id');
135
- expect(screen.getByTestId('thread-id')).toHaveTextContent('undefined');
136
- });
137
-
138
- it('should show streamed response', async () => {
139
- const { requestBody } = mockFetchDataStream({
140
- url: 'https://example.com/api/assistant',
141
- chunks: [
142
- formatStreamPart('assistant_control_data', {
143
- threadId: 't0',
144
- messageId: 'm0',
145
- }),
146
- formatStreamPart('assistant_message', {
147
- id: 'm0',
148
- role: 'assistant',
149
- content: [{ type: 'text', text: { value: '' } }],
150
- }),
151
- // text parts:
152
- '0:"Hello"\n',
153
- '0:","\n',
154
- '0:" world"\n',
155
- '0:"."\n',
156
- ],
157
- });
158
-
159
- await userEvent.click(screen.getByTestId('do-append'));
160
-
161
- await screen.findByTestId('message-0');
162
- expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
163
-
164
- expect(screen.getByTestId('thread-id')).toHaveTextContent('t0');
165
-
166
- await screen.findByTestId('message-1');
167
- expect(screen.getByTestId('message-1')).toHaveTextContent(
168
- 'AI: Hello, world.',
169
- );
170
-
171
- expect(await requestBody).toStrictEqual(
172
- JSON.stringify({
173
- message: 'hi',
174
- threadId: null,
175
- }),
176
- );
177
- });
178
-
179
- it('should switch to new thread on setting undefined threadId', async () => {
180
- await userEvent.click(screen.getByTestId('do-new-thread'));
181
-
182
- expect(screen.queryByTestId('message-0')).toBeNull();
183
- expect(screen.queryByTestId('message-1')).toBeNull();
184
-
185
- const { requestBody } = mockFetchDataStream({
186
- url: 'https://example.com/api/assistant',
187
- chunks: [
188
- formatStreamPart('assistant_control_data', {
189
- threadId: 't1',
190
- messageId: 'm0',
191
- }),
192
- formatStreamPart('assistant_message', {
193
- id: 'm0',
194
- role: 'assistant',
195
- content: [{ type: 'text', text: { value: '' } }],
196
- }),
197
- // text parts:
198
- '0:"Hello"\n',
199
- '0:","\n',
200
- '0:" world"\n',
201
- '0:"."\n',
202
- ],
203
- });
204
-
205
- await userEvent.click(screen.getByTestId('do-append'));
206
-
207
- await screen.findByTestId('message-0');
208
- expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
209
-
210
- expect(screen.getByTestId('thread-id')).toHaveTextContent('t1');
211
-
212
- await screen.findByTestId('message-1');
213
- expect(screen.getByTestId('message-1')).toHaveTextContent(
214
- 'AI: Hello, world.',
215
- );
216
-
217
- // check that correct information was sent to the server:
218
- expect(await requestBody).toStrictEqual(
219
- JSON.stringify({
220
- message: 'hi',
221
- threadId: null,
222
- }),
223
- );
224
- });
225
-
226
- it('should switch to thread on setting previously created threadId', async () => {
227
- await userEvent.click(screen.getByTestId('do-thread-3'));
228
-
229
- expect(screen.queryByTestId('message-0')).toBeNull();
230
- expect(screen.queryByTestId('message-1')).toBeNull();
231
-
232
- const { requestBody } = mockFetchDataStream({
233
- url: 'https://example.com/api/assistant',
234
- chunks: [
235
- formatStreamPart('assistant_control_data', {
236
- threadId: 't3',
237
- messageId: 'm0',
238
- }),
239
- formatStreamPart('assistant_message', {
240
- id: 'm0',
241
- role: 'assistant',
242
- content: [{ type: 'text', text: { value: '' } }],
243
- }),
244
- // text parts:
245
- '0:"Hello"\n',
246
- '0:","\n',
247
- '0:" world"\n',
248
- '0:"."\n',
249
- ],
250
- });
251
-
252
- await userEvent.click(screen.getByTestId('do-append'));
253
-
254
- await screen.findByTestId('message-0');
255
- expect(screen.getByTestId('message-0')).toHaveTextContent('User: hi');
256
-
257
- expect(screen.getByTestId('thread-id')).toHaveTextContent('t3');
258
-
259
- await screen.findByTestId('message-1');
260
- expect(screen.getByTestId('message-1')).toHaveTextContent(
261
- 'AI: Hello, world.',
262
- );
263
-
264
- // check that correct information was sent to the server:
265
- expect(await requestBody).toStrictEqual(
266
- JSON.stringify({
267
- message: 'hi',
268
- threadId: 't3',
269
- }),
270
- );
271
- });
272
- });
package/src/use-chat.ts DELETED
@@ -1,321 +0,0 @@
1
- import type {
2
- ChatRequest,
3
- ChatRequestOptions,
4
- CreateMessage,
5
- JSONValue,
6
- Message,
7
- UseChatOptions,
8
- } from '@ai-sdk/ui-utils';
9
- import {
10
- callChatApi,
11
- generateId as generateIdFunc,
12
- processChatStream,
13
- } from '@ai-sdk/ui-utils';
14
- import swrv from 'swrv';
15
- import type { Ref } from 'vue';
16
- import { ref, unref } from 'vue';
17
-
18
- export type { CreateMessage, Message, UseChatOptions };
19
-
20
- export type UseChatHelpers = {
21
- /** Current messages in the chat */
22
- messages: Ref<Message[]>;
23
- /** The error object of the API request */
24
- error: Ref<undefined | Error>;
25
- /**
26
- * Append a user message to the chat list. This triggers the API call to fetch
27
- * the assistant's response.
28
- */
29
- append: (
30
- message: Message | CreateMessage,
31
- chatRequestOptions?: ChatRequestOptions,
32
- ) => Promise<string | null | undefined>;
33
- /**
34
- * Reload the last AI chat response for the given chat history. If the last
35
- * message isn't from the assistant, it will request the API to generate a
36
- * new response.
37
- */
38
- reload: (
39
- chatRequestOptions?: ChatRequestOptions,
40
- ) => Promise<string | null | undefined>;
41
- /**
42
- * Abort the current request immediately, keep the generated tokens if any.
43
- */
44
- stop: () => void;
45
- /**
46
- * Update the `messages` state locally. This is useful when you want to
47
- * edit the messages on the client, and then trigger the `reload` method
48
- * manually to regenerate the AI response.
49
- */
50
- setMessages: (
51
- messages: Message[] | ((messages: Message[]) => Message[]),
52
- ) => void;
53
- /** The current value of the input */
54
- input: Ref<string>;
55
- /** Form submission handler to automatically reset input and append a user message */
56
- handleSubmit: (
57
- event?: { preventDefault?: () => void },
58
- chatRequestOptions?: ChatRequestOptions,
59
- ) => void;
60
- /** Whether the API request is in progress */
61
- isLoading: Ref<boolean | undefined>;
62
-
63
- /** Additional data added on the server via StreamData */
64
- data: Ref<JSONValue[] | undefined>;
65
- };
66
-
67
- let uniqueId = 0;
68
-
69
- // @ts-expect-error - some issues with the default export of useSWRV
70
- const useSWRV = (swrv.default as typeof import('swrv')['default']) || swrv;
71
- const store: Record<string, Message[] | undefined> = {};
72
-
73
- export function useChat({
74
- api = '/api/chat',
75
- id,
76
- initialMessages = [],
77
- initialInput = '',
78
- sendExtraMessageFields,
79
- experimental_onFunctionCall,
80
- streamMode,
81
- streamProtocol,
82
- onResponse,
83
- onFinish,
84
- onError,
85
- credentials,
86
- headers: metadataHeaders,
87
- body: metadataBody,
88
- generateId = generateIdFunc,
89
- fetch,
90
- keepLastMessageOnError = false,
91
- }: UseChatOptions = {}): UseChatHelpers {
92
- // streamMode is deprecated, use streamProtocol instead.
93
- if (streamMode) {
94
- streamProtocol ??= streamMode === 'text' ? 'text' : undefined;
95
- }
96
-
97
- // Generate a unique ID for the chat if not provided.
98
- const chatId = id || `chat-${uniqueId++}`;
99
-
100
- const key = `${api}|${chatId}`;
101
- const { data: messagesData, mutate: originalMutate } = useSWRV<Message[]>(
102
- key,
103
- () => store[key] || initialMessages,
104
- );
105
-
106
- const { data: isLoading, mutate: mutateLoading } = useSWRV<boolean>(
107
- `${chatId}-loading`,
108
- null,
109
- );
110
-
111
- isLoading.value ??= false;
112
-
113
- // Force the `data` to be `initialMessages` if it's `undefined`.
114
- messagesData.value ??= initialMessages;
115
-
116
- const mutate = (data?: Message[]) => {
117
- store[key] = data;
118
- return originalMutate();
119
- };
120
-
121
- // Because of the `initialData` option, the `data` will never be `undefined`.
122
- const messages = messagesData as Ref<Message[]>;
123
-
124
- const error = ref<undefined | Error>(undefined);
125
- // cannot use JSONValue[] in ref because of infinite Typescript recursion:
126
- const streamData = ref<undefined | unknown[]>(undefined);
127
-
128
- let abortController: AbortController | null = null;
129
-
130
- async function triggerRequest(
131
- messagesSnapshot: Message[],
132
- { options, data, headers, body }: ChatRequestOptions = {},
133
- ) {
134
- try {
135
- error.value = undefined;
136
- mutateLoading(() => true);
137
-
138
- abortController = new AbortController();
139
-
140
- // Do an optimistic update to the chat state to show the updated messages
141
- // immediately.
142
- const previousMessages = messagesSnapshot;
143
- mutate(messagesSnapshot);
144
-
145
- const requestOptions = {
146
- headers: headers ?? options?.headers,
147
- body: body ?? options?.body,
148
- };
149
-
150
- let chatRequest: ChatRequest = {
151
- messages: messagesSnapshot,
152
- options: requestOptions,
153
- body: requestOptions.body,
154
- headers: requestOptions.headers,
155
- data,
156
- };
157
-
158
- await processChatStream({
159
- getStreamedResponse: async () => {
160
- const existingData = (streamData.value ?? []) as JSONValue[];
161
-
162
- const constructedMessagesPayload = sendExtraMessageFields
163
- ? chatRequest.messages
164
- : chatRequest.messages.map(
165
- ({
166
- role,
167
- content,
168
- name,
169
- data,
170
- annotations,
171
- function_call,
172
- }) => ({
173
- role,
174
- content,
175
- ...(name !== undefined && { name }),
176
- ...(data !== undefined && { data }),
177
- ...(annotations !== undefined && { annotations }),
178
- // outdated function/tool call handling (TODO deprecate):
179
- ...(function_call !== undefined && { function_call }),
180
- }),
181
- );
182
-
183
- return await callChatApi({
184
- api,
185
- body: {
186
- messages: constructedMessagesPayload,
187
- data: chatRequest.data,
188
- ...unref(metadataBody), // Use unref to unwrap the ref value
189
- ...requestOptions.body,
190
- },
191
- streamProtocol,
192
- headers: {
193
- ...metadataHeaders,
194
- ...requestOptions.headers,
195
- },
196
- abortController: () => abortController,
197
- credentials,
198
- onResponse,
199
- onUpdate(merged, data) {
200
- mutate([...chatRequest.messages, ...merged]);
201
- streamData.value = [...existingData, ...(data ?? [])];
202
- },
203
- onFinish(message, options) {
204
- // workaround: sometimes the last chunk is not shown in the UI.
205
- // push it twice to make sure it's displayed.
206
- mutate([...chatRequest.messages, message]);
207
- onFinish?.(message, options);
208
- },
209
- restoreMessagesOnFailure() {
210
- // Restore the previous messages if the request fails.
211
- if (!keepLastMessageOnError) {
212
- mutate(previousMessages);
213
- }
214
- },
215
- generateId,
216
- onToolCall: undefined, // not implemented yet
217
- fetch,
218
- });
219
- },
220
- experimental_onFunctionCall,
221
- updateChatRequest(newChatRequest) {
222
- chatRequest = newChatRequest;
223
- },
224
- getCurrentMessages: () => messages.value,
225
- });
226
-
227
- abortController = null;
228
- } catch (err) {
229
- // Ignore abort errors as they are expected.
230
- if ((err as any).name === 'AbortError') {
231
- abortController = null;
232
- return null;
233
- }
234
-
235
- if (onError && err instanceof Error) {
236
- onError(err);
237
- }
238
-
239
- error.value = err as Error;
240
- } finally {
241
- mutateLoading(() => false);
242
- }
243
- }
244
-
245
- const append: UseChatHelpers['append'] = async (message, options) => {
246
- if (!message.id) {
247
- message.id = generateId();
248
- }
249
-
250
- return triggerRequest(messages.value.concat(message as Message), options);
251
- };
252
-
253
- const reload: UseChatHelpers['reload'] = async options => {
254
- const messagesSnapshot = messages.value;
255
- if (messagesSnapshot.length === 0) return null;
256
-
257
- const lastMessage = messagesSnapshot[messagesSnapshot.length - 1];
258
- if (lastMessage.role === 'assistant') {
259
- return triggerRequest(messagesSnapshot.slice(0, -1), options);
260
- }
261
-
262
- return triggerRequest(messagesSnapshot, options);
263
- };
264
-
265
- const stop = () => {
266
- if (abortController) {
267
- abortController.abort();
268
- abortController = null;
269
- }
270
- };
271
-
272
- const setMessages = (
273
- messagesArg: Message[] | ((messages: Message[]) => Message[]),
274
- ) => {
275
- if (typeof messagesArg === 'function') {
276
- messagesArg = messagesArg(messages.value);
277
- }
278
-
279
- mutate(messagesArg);
280
- };
281
-
282
- const input = ref(initialInput);
283
-
284
- const handleSubmit = (
285
- event?: { preventDefault?: () => void },
286
- options: ChatRequestOptions = {},
287
- ) => {
288
- event?.preventDefault?.();
289
-
290
- const inputValue = input.value;
291
-
292
- if (!inputValue && !options.allowEmptySubmit) return;
293
-
294
- triggerRequest(
295
- !inputValue && options.allowEmptySubmit
296
- ? messages.value
297
- : messages.value.concat({
298
- id: generateId(),
299
- createdAt: new Date(),
300
- content: inputValue,
301
- role: 'user',
302
- }),
303
- options,
304
- );
305
-
306
- input.value = '';
307
- };
308
-
309
- return {
310
- messages,
311
- append,
312
- error,
313
- reload,
314
- stop,
315
- setMessages,
316
- input,
317
- handleSubmit,
318
- isLoading,
319
- data: streamData as Ref<undefined | JSONValue[]>,
320
- };
321
- }