Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-ligers-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/vue': patch
---

fix (ai-sdk/vue): fix status reactivity
24 changes: 14 additions & 10 deletions packages/vue/src/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, UIMessage[] | undefined> = {};
const statusStore: Record<
string,
Ref<'submitted' | 'streaming' | 'ready' | 'error'>
> = {};

export function useChat(
{
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -233,7 +237,7 @@ export function useChat(
credentials,
onResponse,
onUpdate({ message, data, replaceLastMessage }) {
mutateStatus(() => 'streaming');
status.value = 'streaming';

mutate([
...(replaceLastMessage
Expand All @@ -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;
}

Expand All @@ -270,7 +274,7 @@ export function useChat(
}

error.value = err as Error;
mutateStatus(() => 'error');
status.value = 'error';
} finally {
abortController = null;
}
Expand Down
46 changes: 46 additions & 0 deletions packages/vue/src/use-chat.ui.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,52 @@ 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(
formatDataStreamPart({ type: 'text', value: 'Hello' }),
);

await waitFor(() =>
expect(screen.getByTestId('status')).toHaveTextContent('streaming'),
);

Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: () => 'hidden',
});
document.dispatchEvent(new Event('visibilitychange'));

controller.write(
formatDataStreamPart({ type: 'text', value: ' world.' }),
);
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',
Expand Down
Loading