From 7bc09189d30b1dadfd5fbe61243b20e11fea913f Mon Sep 17 00:00:00 2001 From: nicoalbanese Date: Thu, 8 May 2025 10:54:02 +0100 Subject: [PATCH 1/5] fix (ai-sdk/vue): status reactivity --- packages/vue/src/use-chat.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 764e70ec482f..7743a03fc27d 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -164,11 +164,7 @@ export function useChat( () => store[key] ?? fillMessageParts(initialMessages), ); - const { data: status, mutate: mutateStatus } = useSWRV< - 'submitted' | 'streaming' | 'ready' | 'error' - >(`${chatId}-status`, null); - - status.value ??= 'ready'; + const status = ref<'submitted' | 'streaming' | 'ready' | 'error'>('ready'); // Force the `data` to be `initialMessages` if it's `undefined`. messagesData.value ??= fillMessageParts(initialMessages); @@ -192,7 +188,7 @@ export function useChat( { data, headers, body }: ChatRequestOptions = {}, ) { error.value = undefined; - mutateStatus(() => 'submitted'); + status.value = 'submitted'; const messageCount = messages.value.length; const maxStep = extractMaxToolInvocationStep( @@ -257,7 +253,7 @@ export function useChat( credentials, onResponse, onUpdate({ message, data, replaceLastMessage }) { - mutateStatus(() => 'streaming'); + status.value = 'streaming'; mutate([ ...(replaceLastMessage @@ -283,12 +279,12 @@ export function useChat( lastMessage: recursiveToRaw(chatMessages[chatMessages.length - 1]), }); - 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; } @@ -297,7 +293,7 @@ export function useChat( } error.value = err as Error; - mutateStatus(() => 'error'); + status.value = 'error'; } finally { abortController = null; } From fca7a73ab0c56fc6c6a814031cb6a7dd6b3b2319 Mon Sep 17 00:00:00 2001 From: nicoalbanese Date: Thu, 8 May 2025 10:56:59 +0100 Subject: [PATCH 2/5] add changeset --- .changeset/heavy-ligers-lay.md | 5 +++++ 1 file changed, 5 insertions(+) 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 From f3e710e59538d66ff358f08876735ea6f6d1d9cd Mon Sep 17 00:00:00 2001 From: nicoalbanese Date: Thu, 8 May 2025 13:13:22 +0100 Subject: [PATCH 3/5] add status store for different chatIds --- packages/vue/src/use-chat.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/vue/src/use-chat.ts b/packages/vue/src/use-chat.ts index 7743a03fc27d..7cff1dd8a607 100644 --- a/packages/vue/src/use-chat.ts +++ b/packages/vue/src/use-chat.ts @@ -106,6 +106,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( { @@ -164,7 +168,11 @@ export function useChat( () => store[key] ?? fillMessageParts(initialMessages), ); - const status = ref<'submitted' | 'streaming' | 'ready' | 'error'>('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 ??= fillMessageParts(initialMessages); From 4cafd7576abf00560bd72bafc2a576e968c94ffd Mon Sep 17 00:00:00 2001 From: nicoalbanese Date: Fri, 9 May 2025 11:48:17 +0100 Subject: [PATCH 4/5] add test --- packages/vue/src/use-chat.ui.test.tsx | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/vue/src/use-chat.ui.test.tsx b/packages/vue/src/use-chat.ui.test.tsx index d31896adf26f..e07e783b3444 100644 --- a/packages/vue/src/use-chat.ui.test.tsx +++ b/packages/vue/src/use-chat.ui.test.tsx @@ -174,6 +174,47 @@ describe('data protocol stream', () => { }); }); + it('gets stuck when the stream finishes while 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 8ac86c8bc35333247f3124d9e58935fefd4d4190 Mon Sep 17 00:00:00 2001 From: nicoalbanese Date: Fri, 9 May 2025 12:06:41 +0100 Subject: [PATCH 5/5] rename test --- packages/vue/src/use-chat.ui.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue/src/use-chat.ui.test.tsx b/packages/vue/src/use-chat.ui.test.tsx index e07e783b3444..99a8422621d1 100644 --- a/packages/vue/src/use-chat.ui.test.tsx +++ b/packages/vue/src/use-chat.ui.test.tsx @@ -174,7 +174,7 @@ describe('data protocol stream', () => { }); }); - it('gets stuck when the stream finishes while the tab is hidden', async () => { + it('should update status when the tab is hidden', async () => { const controller = new TestResponseController(); server.urls['/api/chat'].response = { type: 'controlled-stream',