From 4a0ca192e157c683773499b964d135f5e3b786d9 Mon Sep 17 00:00:00 2001 From: Nico Albanese <49612682+nicoalbanese@users.noreply.github.com> Date: Fri, 9 May 2025 13:24:24 +0100 Subject: [PATCH 1/2] fix (ai-sdk/vue): status reactivity (#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. --- .changeset/heavy-ligers-lay.md | 5 ++++ packages/vue/src/use-chat.ts | 24 +++++++++------- packages/vue/src/use-chat.ui.test.tsx | 41 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 .changeset/heavy-ligers-lay.md diff --git a/.changeset/heavy-ligers-lay.md b/.changeset/heavy-ligers-lay.md new file mode 100644 index 000000000000..b87f18bc7826 --- /dev/null +++ b/.changeset/heavy-ligers-lay.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/vue': patch +--- + +fix (ai-sdk/vue): fix status reactivity diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 875c63615b64..269de7b32ee2 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -107,6 +107,10 @@ export type UseChatHelpers = { // @ts-expect-error - some issues with the default export of useSWRV const useSWRV = (swrv.default as (typeof import('swrv'))['default']) || swrv; const store: Record = {}; +const statusStore: Record< + string, + Ref<'submitted' | 'streaming' | 'ready' | 'error'> +> = {}; export function useChat( { @@ -165,11 +169,11 @@ export function useChat( () => store[key] ?? initialMessages, ); - const { data: status, mutate: mutateStatus } = useSWRV< - 'submitted' | 'streaming' | 'ready' | 'error' - >(`${chatId}-status`, null); - - status.value ??= 'ready'; + const status = + statusStore[chatId] ?? + (statusStore[chatId] = ref<'submitted' | 'streaming' | 'ready' | 'error'>( + 'ready', + )); // Force the `data` to be `initialMessages` if it's `undefined`. messagesData.value ??= initialMessages; @@ -193,7 +197,7 @@ export function useChat( { data, headers, body }: ChatRequestOptions = {}, ) { error.value = undefined; - mutateStatus(() => 'submitted'); + status.value = 'submitted'; const messageCount = messages.value.length; const lastMessage = messages.value.at(-1); @@ -233,7 +237,7 @@ export function useChat( credentials, onResponse, onUpdate({ message, data, replaceLastMessage }) { - mutateStatus(() => 'streaming'); + status.value = 'streaming'; mutate([ ...(replaceLastMessage @@ -256,12 +260,12 @@ export function useChat( getCurrentDate, }); - mutateStatus(() => 'ready'); + status.value = 'ready'; } catch (err) { // Ignore abort errors as they are expected. if ((err as any).name === 'AbortError') { abortController = null; - mutateStatus(() => 'ready'); + status.value = 'ready'; return null; } @@ -270,7 +274,7 @@ export function useChat( } error.value = err as Error; - mutateStatus(() => 'error'); + status.value = 'error'; } finally { abortController = null; } diff --git a/packages/vue/src/use-chat.ui.test.tsx b/packages/vue/src/use-chat.ui.test.tsx index 1816d4f006ce..068d291cffe1 100644 --- a/packages/vue/src/use-chat.ui.test.tsx +++ b/packages/vue/src/use-chat.ui.test.tsx @@ -193,6 +193,47 @@ describe('data protocol stream', () => { }); }); + it('should update status when the tab is hidden', async () => { + const controller = new TestResponseController(); + server.urls['/api/chat'].response = { + type: 'controlled-stream', + controller, + }; + + const originalVisibilityState = document.visibilityState; + + try { + await userEvent.click(screen.getByTestId('do-append')); + await waitFor(() => + expect(screen.getByTestId('status')).toHaveTextContent('submitted'), + ); + + controller.write('0:"Hello"\n'); + await waitFor(() => + expect(screen.getByTestId('status')).toHaveTextContent('streaming'), + ); + + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: () => 'hidden', + }); + document.dispatchEvent(new Event('visibilitychange')); + + controller.write('0:", world."\n'); + controller.close(); + + await waitFor(() => + expect(screen.getByTestId('status')).toHaveTextContent('ready'), + ); + } finally { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: () => originalVisibilityState, + }); + document.dispatchEvent(new Event('visibilitychange')); + } + }); + it('should set status to error when there is a server error', async () => { server.urls['/api/chat'].response = { type: 'error', From 0e77dbfeb11a23df22a5c532d54c3081047bfec0 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Sat, 10 May 2025 08:54:44 +0200 Subject: [PATCH 2/2] fix test --- packages/vue/src/use-chat.ui.test.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/vue/src/use-chat.ui.test.tsx b/packages/vue/src/use-chat.ui.test.tsx index 068d291cffe1..71121ebd04ae 100644 --- a/packages/vue/src/use-chat.ui.test.tsx +++ b/packages/vue/src/use-chat.ui.test.tsx @@ -208,7 +208,10 @@ describe('data protocol stream', () => { expect(screen.getByTestId('status')).toHaveTextContent('submitted'), ); - controller.write('0:"Hello"\n'); + controller.write( + formatDataStreamPart({ type: 'text', value: 'Hello' }), + ); + await waitFor(() => expect(screen.getByTestId('status')).toHaveTextContent('streaming'), ); @@ -219,7 +222,9 @@ describe('data protocol stream', () => { }); document.dispatchEvent(new Event('visibilitychange')); - controller.write('0:", world."\n'); + controller.write( + formatDataStreamPart({ type: 'text', value: ' world.' }), + ); controller.close(); await waitFor(() =>