@ai-sdk/vue 3.0.44 → 3.0.46

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/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './use-completion';
2
+ export { Chat } from './chat.vue';
3
+ export * from './use-object';
@@ -0,0 +1,10 @@
1
+ {
2
+ "main": "./dist/index.js",
3
+ "module": "./dist/index.mjs",
4
+ "types": "./dist/index.d.ts",
5
+ "exports": "./dist/index.mjs",
6
+ "private": true,
7
+ "peerDependencies": {
8
+ "vue": "*"
9
+ }
10
+ }
@@ -0,0 +1,22 @@
1
+ import { cleanup, render } from '@testing-library/vue';
2
+ import { beforeEach, afterEach, vi } from 'vitest';
3
+
4
+ export const setupTestComponent = (
5
+ TestComponent: any,
6
+ {
7
+ init,
8
+ }: {
9
+ init?: (TestComponent: any) => any;
10
+ } = {},
11
+ ) => {
12
+ beforeEach(() => {
13
+ render(init?.(TestComponent) ?? TestComponent);
14
+ });
15
+
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ cleanup();
19
+ });
20
+
21
+ return TestComponent;
22
+ };
@@ -0,0 +1,5 @@
1
+ // required for vue testing library
2
+ declare module '*.vue' {
3
+ import Vue from 'vue';
4
+ export default Vue;
5
+ }
@@ -0,0 +1,162 @@
1
+ import type { CompletionRequestOptions, UseCompletionOptions } from 'ai';
2
+ import { callCompletionApi } from 'ai';
3
+ import swrv from 'swrv';
4
+ import type { Ref } from 'vue';
5
+ import { ref, unref } from 'vue';
6
+
7
+ export type { UseCompletionOptions };
8
+
9
+ export type UseCompletionHelpers = {
10
+ /** The current completion result */
11
+ completion: Ref<string>;
12
+ /** The error object of the API request */
13
+ error: Ref<undefined | Error>;
14
+ /**
15
+ * Send a new prompt to the API endpoint and update the completion state.
16
+ */
17
+ complete: (
18
+ prompt: string,
19
+ options?: CompletionRequestOptions,
20
+ ) => Promise<string | null | undefined>;
21
+ /**
22
+ * Abort the current API request but keep the generated tokens.
23
+ */
24
+ stop: () => void;
25
+ /**
26
+ * Update the `completion` state locally.
27
+ */
28
+ setCompletion: (completion: string) => void;
29
+ /** The current value of the input */
30
+ input: Ref<string>;
31
+ /**
32
+ * Form submission handler to automatically reset input and append a user message
33
+ * @example
34
+ * ```jsx
35
+ * <form @submit="handleSubmit">
36
+ * <input @change="handleInputChange" v-model="input" />
37
+ * </form>
38
+ * ```
39
+ */
40
+ handleSubmit: (event?: { preventDefault?: () => void }) => void;
41
+ /** Whether the API request is in progress */
42
+ isLoading: Ref<boolean | undefined>;
43
+ };
44
+
45
+ let uniqueId = 0;
46
+
47
+ // @ts-expect-error - some issues with the default export of useSWRV
48
+ const useSWRV = (swrv.default as (typeof import('swrv'))['default']) || swrv;
49
+ const store: Record<string, any> = {};
50
+
51
+ export function useCompletion({
52
+ api = '/api/completion',
53
+ id,
54
+ initialCompletion = '',
55
+ initialInput = '',
56
+ credentials,
57
+ headers,
58
+ body,
59
+ streamProtocol,
60
+ onFinish,
61
+ onError,
62
+ fetch,
63
+ }: UseCompletionOptions = {}): UseCompletionHelpers {
64
+ // Generate an unique id for the completion if not provided.
65
+ const completionId = id || `completion-${uniqueId++}`;
66
+
67
+ const key = `${api}|${completionId}`;
68
+ const { data, mutate: originalMutate } = useSWRV<string>(
69
+ key,
70
+ () => store[key] || initialCompletion,
71
+ );
72
+
73
+ const { data: isLoading, mutate: mutateLoading } = useSWRV<boolean>(
74
+ `${completionId}-loading`,
75
+ null,
76
+ );
77
+
78
+ isLoading.value ??= false;
79
+
80
+ // Force the `data` to be `initialCompletion` if it's `undefined`.
81
+ data.value ||= initialCompletion;
82
+
83
+ const mutate = (data: string) => {
84
+ store[key] = data;
85
+ return originalMutate();
86
+ };
87
+
88
+ // Because of the `initialData` option, the `data` will never be `undefined`.
89
+ const completion = data as Ref<string>;
90
+
91
+ const error = ref<undefined | Error>(undefined);
92
+
93
+ let abortController: AbortController | null = null;
94
+
95
+ async function triggerRequest(
96
+ prompt: string,
97
+ options?: CompletionRequestOptions,
98
+ ) {
99
+ return callCompletionApi({
100
+ api,
101
+ prompt,
102
+ credentials,
103
+ headers: {
104
+ ...headers,
105
+ ...options?.headers,
106
+ },
107
+ body: {
108
+ ...unref(body),
109
+ ...options?.body,
110
+ },
111
+ streamProtocol,
112
+ setCompletion: mutate,
113
+ setLoading: loading => mutateLoading(() => loading),
114
+ setError: err => {
115
+ error.value = err;
116
+ },
117
+ setAbortController: controller => {
118
+ abortController = controller;
119
+ },
120
+ onFinish,
121
+ onError,
122
+ fetch,
123
+ });
124
+ }
125
+
126
+ const complete: UseCompletionHelpers['complete'] = async (
127
+ prompt,
128
+ options,
129
+ ) => {
130
+ return triggerRequest(prompt, options);
131
+ };
132
+
133
+ const stop = () => {
134
+ if (abortController) {
135
+ abortController.abort();
136
+ abortController = null;
137
+ }
138
+ };
139
+
140
+ const setCompletion = (completion: string) => {
141
+ mutate(completion);
142
+ };
143
+
144
+ const input = ref(initialInput);
145
+
146
+ const handleSubmit = (event?: { preventDefault?: () => void }) => {
147
+ event?.preventDefault?.();
148
+ const inputValue = input.value;
149
+ return inputValue ? complete(inputValue) : undefined;
150
+ };
151
+
152
+ return {
153
+ completion,
154
+ complete,
155
+ error,
156
+ stop,
157
+ setCompletion,
158
+ input,
159
+ handleSubmit,
160
+ isLoading,
161
+ };
162
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ createTestServer,
3
+ TestResponseController,
4
+ } from '@ai-sdk/test-server/with-vitest';
5
+ import '@testing-library/jest-dom/vitest';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { findByText, screen } from '@testing-library/vue';
8
+ import { UIMessageChunk } from 'ai';
9
+ import TestCompletionComponent from './TestCompletionComponent.vue';
10
+ import TestCompletionTextStreamComponent from './TestCompletionTextStreamComponent.vue';
11
+ import { setupTestComponent } from './setup-test-component';
12
+ import { describe, it, expect } from 'vitest';
13
+
14
+ function formatChunk(part: UIMessageChunk) {
15
+ return `data: ${JSON.stringify(part)}\n\n`;
16
+ }
17
+
18
+ const server = createTestServer({
19
+ '/api/completion': {},
20
+ });
21
+
22
+ describe('stream data stream', () => {
23
+ setupTestComponent(TestCompletionComponent);
24
+
25
+ it('should show streamed response', async () => {
26
+ server.urls['/api/completion'].response = {
27
+ type: 'stream-chunks',
28
+ chunks: [
29
+ formatChunk({ type: 'text-start', id: '0' }),
30
+ formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
31
+ formatChunk({ type: 'text-delta', id: '0', delta: ',' }),
32
+ formatChunk({ type: 'text-delta', id: '0', delta: ' world' }),
33
+ formatChunk({ type: 'text-delta', id: '0', delta: '.' }),
34
+ formatChunk({ type: 'text-end', id: '0' }),
35
+ ],
36
+ };
37
+
38
+ await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
39
+
40
+ await screen.findByTestId('completion');
41
+ expect(screen.getByTestId('completion')).toHaveTextContent('Hello, world.');
42
+ });
43
+
44
+ describe('loading state', () => {
45
+ it('should show loading state', async () => {
46
+ const controller = new TestResponseController();
47
+ server.urls['/api/completion'].response = {
48
+ type: 'controlled-stream',
49
+ controller,
50
+ };
51
+
52
+ await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
53
+
54
+ await screen.findByTestId('loading');
55
+ expect(screen.getByTestId('loading')).toHaveTextContent('true');
56
+
57
+ controller.write(formatChunk({ type: 'text-start', id: '0' }));
58
+ controller.write(
59
+ formatChunk({ type: 'text-delta', id: '0', delta: 'Hello' }),
60
+ );
61
+ controller.write(formatChunk({ type: 'text-end', id: '0' }));
62
+ controller.close();
63
+
64
+ await findByText(await screen.findByTestId('loading'), 'false');
65
+ expect(screen.getByTestId('loading')).toHaveTextContent('false');
66
+ });
67
+
68
+ it('should reset loading state on error', async () => {
69
+ server.urls['/api/completion'].response = {
70
+ type: 'error',
71
+ status: 404,
72
+ body: 'Not found',
73
+ };
74
+
75
+ await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
76
+
77
+ await screen.findByTestId('loading');
78
+ expect(screen.getByTestId('loading')).toHaveTextContent('false');
79
+ });
80
+ });
81
+ });
82
+
83
+ describe('stream data stream', () => {
84
+ setupTestComponent(TestCompletionTextStreamComponent);
85
+
86
+ it('should show streamed response', async () => {
87
+ server.urls['/api/completion'].response = {
88
+ type: 'stream-chunks',
89
+ chunks: ['Hello', ',', ' world', '.'],
90
+ };
91
+
92
+ await userEvent.type(screen.getByTestId('input'), 'hi{enter}');
93
+
94
+ await screen.findByTestId('completion');
95
+ expect(screen.getByTestId('completion')).toHaveTextContent('Hello, world.');
96
+ });
97
+ });
@@ -0,0 +1,232 @@
1
+ import {
2
+ isAbortError,
3
+ safeValidateTypes,
4
+ type FetchFunction,
5
+ } from '@ai-sdk/provider-utils';
6
+ import {
7
+ asSchema,
8
+ isDeepEqualData,
9
+ parsePartialJson,
10
+ type DeepPartial,
11
+ type FlexibleSchema,
12
+ type InferSchema,
13
+ } from 'ai';
14
+ import swrv from 'swrv';
15
+ import { ref, type Ref } from 'vue';
16
+
17
+ // use function to allow for mocking in tests
18
+ const getOriginalFetch = () => fetch;
19
+
20
+ export type Experimental_UseObjectOptions<
21
+ SCHEMA extends FlexibleSchema,
22
+ RESULT,
23
+ > = {
24
+ /** API endpoint that streams JSON chunks matching the schema */
25
+ api: string;
26
+
27
+ /** Schema that defines the final object shape */
28
+ schema: SCHEMA;
29
+
30
+ /** Shared state key. If omitted a random one is generated */
31
+ id?: string;
32
+
33
+ /** Initial partial value */
34
+ initialValue?: DeepPartial<RESULT>;
35
+
36
+ /** Optional custom fetch implementation */
37
+ fetch?: FetchFunction;
38
+
39
+ /** Called when stream ends */
40
+ onFinish?: (event: {
41
+ object: RESULT | undefined;
42
+ error: Error | undefined;
43
+ }) => Promise<void> | void;
44
+
45
+ /** Called on error */
46
+ onError?: (error: Error) => void;
47
+
48
+ /** Extra request headers */
49
+ headers?: Record<string, string> | Headers;
50
+
51
+ /** Request credentials mode. Defaults to 'same-origin' if omitted */
52
+ credentials?: RequestCredentials;
53
+ };
54
+
55
+ export type Experimental_UseObjectHelpers<RESULT, INPUT> = {
56
+ /** POST the input and start streaming */
57
+ submit: (input: INPUT) => void;
58
+
59
+ /** Current partial object, updated as chunks arrive */
60
+ object: Ref<DeepPartial<RESULT> | undefined>;
61
+
62
+ /** Latest error if any */
63
+ error: Ref<Error | undefined>;
64
+
65
+ /** Loading flag for the in-flight request */
66
+ isLoading: Ref<boolean | undefined>;
67
+
68
+ /** Abort the current request. Keeps current partial object. */
69
+ stop: () => void;
70
+
71
+ /** Abort and clear all state */
72
+ clear: () => void;
73
+ };
74
+
75
+ let uniqueId = 0;
76
+
77
+ // @ts-expect-error - some issues with the default export of useSWRV
78
+ const useSWRV = (swrv.default as (typeof import('swrv'))['default']) || swrv;
79
+ const store: Record<string, any> = {};
80
+
81
+ export const experimental_useObject = function useObject<
82
+ SCHEMA extends FlexibleSchema,
83
+ RESULT = InferSchema<SCHEMA>,
84
+ INPUT = any,
85
+ >({
86
+ api,
87
+ id,
88
+ schema,
89
+ initialValue,
90
+ fetch,
91
+ onError,
92
+ onFinish,
93
+ headers,
94
+ credentials,
95
+ }: Experimental_UseObjectOptions<
96
+ SCHEMA,
97
+ RESULT
98
+ >): Experimental_UseObjectHelpers<RESULT, INPUT> {
99
+ // Generate an unique id for the object if not provided.
100
+ const completionId = id || `completion-${uniqueId++}`;
101
+
102
+ const key = `${api}|${completionId}`;
103
+ const { data, mutate: originalMutate } = useSWRV<
104
+ DeepPartial<RESULT> | undefined
105
+ >(key, () => (key in store ? store[key] : initialValue));
106
+
107
+ const { data: isLoading, mutate: mutateLoading } = useSWRV<boolean>(
108
+ `${completionId}-loading`,
109
+ null,
110
+ );
111
+
112
+ isLoading.value ??= false;
113
+ data.value ||= initialValue as DeepPartial<RESULT> | undefined;
114
+
115
+ const mutateObject = (value: DeepPartial<RESULT> | undefined) => {
116
+ store[key] = value;
117
+ return originalMutate();
118
+ };
119
+
120
+ const error = ref<Error | undefined>(undefined);
121
+ let abortController: AbortController | null = null;
122
+
123
+ const stop = async () => {
124
+ if (abortController) {
125
+ try {
126
+ abortController.abort();
127
+ } catch {
128
+ // ignore
129
+ } finally {
130
+ abortController = null;
131
+ }
132
+ }
133
+ await mutateLoading(() => false);
134
+ };
135
+
136
+ const clearObject = async () => {
137
+ error.value = undefined;
138
+ await mutateLoading(() => false);
139
+ await mutateObject(undefined);
140
+ // Need to explicitly set the value to undefined to trigger a re-render
141
+ data.value = undefined;
142
+ };
143
+
144
+ const clear = async () => {
145
+ await stop();
146
+ await clearObject();
147
+ };
148
+
149
+ const submit = async (input: INPUT) => {
150
+ try {
151
+ await clearObject();
152
+ await mutateLoading(() => true);
153
+
154
+ abortController = new AbortController();
155
+
156
+ const actualFetch = fetch ?? getOriginalFetch();
157
+ const response = await actualFetch(api, {
158
+ method: 'POST',
159
+ headers: {
160
+ 'Content-Type': 'application/json',
161
+ ...(headers as any),
162
+ },
163
+ credentials: credentials ?? 'same-origin',
164
+ signal: abortController.signal,
165
+ body: JSON.stringify(input),
166
+ });
167
+
168
+ if (!response.ok) {
169
+ throw new Error(
170
+ (await response.text()) || 'Failed to fetch the response.',
171
+ );
172
+ }
173
+
174
+ if (!response.body) {
175
+ throw new Error('The response body is empty.');
176
+ }
177
+
178
+ let accumulatedText = '';
179
+ let latestObject: DeepPartial<RESULT> | undefined = undefined;
180
+
181
+ await response.body.pipeThrough(new TextDecoderStream()).pipeTo(
182
+ new WritableStream<string>({
183
+ async write(chunk) {
184
+ accumulatedText += chunk;
185
+ const { value } = await parsePartialJson(accumulatedText);
186
+ const currentObject = value as DeepPartial<RESULT>;
187
+ if (!isDeepEqualData(latestObject, currentObject)) {
188
+ latestObject = currentObject;
189
+ await mutateObject(currentObject);
190
+ }
191
+ },
192
+ async close() {
193
+ await mutateLoading(() => false);
194
+ abortController = null;
195
+
196
+ if (onFinish) {
197
+ const validationResult = await safeValidateTypes({
198
+ value: latestObject,
199
+ schema: asSchema(schema),
200
+ });
201
+
202
+ onFinish(
203
+ validationResult.success
204
+ ? {
205
+ object: validationResult.value as RESULT,
206
+ error: undefined,
207
+ }
208
+ : { object: undefined, error: validationResult.error },
209
+ );
210
+ }
211
+ },
212
+ }),
213
+ );
214
+ } catch (err: unknown) {
215
+ if (isAbortError(err)) return;
216
+
217
+ if (onError && err instanceof Error) onError(err);
218
+
219
+ await mutateLoading(() => false);
220
+ error.value = err instanceof Error ? err : new Error(String(err));
221
+ }
222
+ };
223
+
224
+ return {
225
+ submit,
226
+ object: data,
227
+ error,
228
+ isLoading,
229
+ stop,
230
+ clear,
231
+ };
232
+ };