Skip to content

Commit b54cb59

Browse files
authored
fix (ai-sdk/vue): status reactivity (vercel#6234)
## Background Bug with Vue that led to status not updating when tab was changed. ## Summary Changes status from using SWR to using Vue ref.
1 parent 7765a71 commit b54cb59

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

.changeset/heavy-ligers-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/vue': patch
3+
---
4+
5+
fix (ai-sdk/vue): fix status reactivity

packages/vue/src/use-chat.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ export type UseChatHelpers = {
106106
// @ts-expect-error - some issues with the default export of useSWRV
107107
const useSWRV = (swrv.default as (typeof import('swrv'))['default']) || swrv;
108108
const store: Record<string, UIMessage[] | undefined> = {};
109+
const statusStore: Record<
110+
string,
111+
Ref<'submitted' | 'streaming' | 'ready' | 'error'>
112+
> = {};
109113

110114
export function useChat(
111115
{
@@ -164,11 +168,11 @@ export function useChat(
164168
() => store[key] ?? fillMessageParts(initialMessages),
165169
);
166170

167-
const { data: status, mutate: mutateStatus } = useSWRV<
168-
'submitted' | 'streaming' | 'ready' | 'error'
169-
>(`${chatId}-status`, null);
170-
171-
status.value ??= 'ready';
171+
const status =
172+
statusStore[chatId] ??
173+
(statusStore[chatId] = ref<'submitted' | 'streaming' | 'ready' | 'error'>(
174+
'ready',
175+
));
172176

173177
// Force the `data` to be `initialMessages` if it's `undefined`.
174178
messagesData.value ??= fillMessageParts(initialMessages);
@@ -192,7 +196,7 @@ export function useChat(
192196
{ data, headers, body }: ChatRequestOptions = {},
193197
) {
194198
error.value = undefined;
195-
mutateStatus(() => 'submitted');
199+
status.value = 'submitted';
196200

197201
const messageCount = messages.value.length;
198202
const maxStep = extractMaxToolInvocationStep(
@@ -257,7 +261,7 @@ export function useChat(
257261
credentials,
258262
onResponse,
259263
onUpdate({ message, data, replaceLastMessage }) {
260-
mutateStatus(() => 'streaming');
264+
status.value = 'streaming';
261265

262266
mutate([
263267
...(replaceLastMessage
@@ -283,12 +287,12 @@ export function useChat(
283287
lastMessage: recursiveToRaw(chatMessages[chatMessages.length - 1]),
284288
});
285289

286-
mutateStatus(() => 'ready');
290+
status.value = 'ready';
287291
} catch (err) {
288292
// Ignore abort errors as they are expected.
289293
if ((err as any).name === 'AbortError') {
290294
abortController = null;
291-
mutateStatus(() => 'ready');
295+
status.value = 'ready';
292296
return null;
293297
}
294298

@@ -297,7 +301,7 @@ export function useChat(
297301
}
298302

299303
error.value = err as Error;
300-
mutateStatus(() => 'error');
304+
status.value = 'error';
301305
} finally {
302306
abortController = null;
303307
}

packages/vue/src/use-chat.ui.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,47 @@ describe('data protocol stream', () => {
174174
});
175175
});
176176

177+
it('should update status when the tab is hidden', async () => {
178+
const controller = new TestResponseController();
179+
server.urls['/api/chat'].response = {
180+
type: 'controlled-stream',
181+
controller,
182+
};
183+
184+
const originalVisibilityState = document.visibilityState;
185+
186+
try {
187+
await userEvent.click(screen.getByTestId('do-append'));
188+
await waitFor(() =>
189+
expect(screen.getByTestId('status')).toHaveTextContent('submitted'),
190+
);
191+
192+
controller.write('0:"Hello"\n');
193+
await waitFor(() =>
194+
expect(screen.getByTestId('status')).toHaveTextContent('streaming'),
195+
);
196+
197+
Object.defineProperty(document, 'visibilityState', {
198+
configurable: true,
199+
get: () => 'hidden',
200+
});
201+
document.dispatchEvent(new Event('visibilitychange'));
202+
203+
controller.write('0:", world."\n');
204+
controller.close();
205+
206+
await waitFor(() =>
207+
expect(screen.getByTestId('status')).toHaveTextContent('ready'),
208+
);
209+
} finally {
210+
Object.defineProperty(document, 'visibilityState', {
211+
configurable: true,
212+
get: () => originalVisibilityState,
213+
});
214+
document.dispatchEvent(new Event('visibilitychange'));
215+
}
216+
});
217+
177218
it('should set status to error when there is a server error', async () => {
178219
server.urls['/api/chat'].response = {
179220
type: 'error',

0 commit comments

Comments
 (0)