@ai-sdk/react 0.0.20 → 0.0.22
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/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-clean.log +1 -1
- package/CHANGELOG.md +16 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +51 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/use-chat.ts +56 -4
- package/src/use-chat.ui.test.tsx +298 -5
package/src/use-chat.ui.test.tsx
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
/* eslint-disable @next/next/no-img-element */
|
|
1
2
|
import {
|
|
2
3
|
mockFetchDataStream,
|
|
3
4
|
mockFetchDataStreamWithGenerator,
|
|
4
5
|
mockFetchError,
|
|
5
6
|
} from '@ai-sdk/ui-utils/test';
|
|
6
7
|
import '@testing-library/jest-dom/vitest';
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
act,
|
|
10
|
+
cleanup,
|
|
11
|
+
findByText,
|
|
12
|
+
render,
|
|
13
|
+
screen,
|
|
14
|
+
} from '@testing-library/react';
|
|
8
15
|
import userEvent from '@testing-library/user-event';
|
|
9
|
-
import React from 'react';
|
|
16
|
+
import React, { useRef, useState } from 'react';
|
|
10
17
|
import { useChat } from './use-chat';
|
|
11
|
-
import { formatStreamPart } from '@ai-sdk/ui-utils';
|
|
18
|
+
import { formatStreamPart, getTextFromDataUrl } from '@ai-sdk/ui-utils';
|
|
12
19
|
|
|
13
20
|
describe('stream data stream', () => {
|
|
14
21
|
const TestComponent = () => {
|
|
@@ -227,12 +234,11 @@ describe('form actions', () => {
|
|
|
227
234
|
</div>
|
|
228
235
|
))}
|
|
229
236
|
|
|
230
|
-
<form onSubmit={handleSubmit}
|
|
237
|
+
<form onSubmit={handleSubmit}>
|
|
231
238
|
<input
|
|
232
239
|
value={input}
|
|
233
240
|
placeholder="Send message..."
|
|
234
241
|
onChange={handleInputChange}
|
|
235
|
-
className="w-full p-2 bg-zinc-100"
|
|
236
242
|
disabled={isLoading}
|
|
237
243
|
data-testid="do-input"
|
|
238
244
|
/>
|
|
@@ -556,3 +562,290 @@ describe('maxToolRoundtrips', () => {
|
|
|
556
562
|
});
|
|
557
563
|
});
|
|
558
564
|
});
|
|
565
|
+
|
|
566
|
+
describe('file attachments with data url', () => {
|
|
567
|
+
const TestComponent = () => {
|
|
568
|
+
const { messages, handleSubmit, handleInputChange, isLoading, input } =
|
|
569
|
+
useChat({
|
|
570
|
+
api: '/api/stream-chat',
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const [attachments, setAttachments] = useState<FileList | undefined>(
|
|
574
|
+
undefined,
|
|
575
|
+
);
|
|
576
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<div>
|
|
580
|
+
{messages.map((m, idx) => (
|
|
581
|
+
<div data-testid={`message-${idx}`} key={m.id}>
|
|
582
|
+
{m.role === 'user' ? 'User: ' : 'AI: '}
|
|
583
|
+
{m.content}
|
|
584
|
+
{m.experimental_attachments?.map(attachment => {
|
|
585
|
+
if (attachment.contentType?.startsWith('image/')) {
|
|
586
|
+
return (
|
|
587
|
+
<img
|
|
588
|
+
key={attachment.name}
|
|
589
|
+
src={attachment.url}
|
|
590
|
+
alt={attachment.name}
|
|
591
|
+
data-testid={`attachment-${idx}`}
|
|
592
|
+
/>
|
|
593
|
+
);
|
|
594
|
+
} else if (attachment.contentType?.startsWith('text/')) {
|
|
595
|
+
return (
|
|
596
|
+
<div key={attachment.name} data-testid={`attachment-${idx}`}>
|
|
597
|
+
{getTextFromDataUrl(attachment.url)}
|
|
598
|
+
</div>
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
})}
|
|
602
|
+
</div>
|
|
603
|
+
))}
|
|
604
|
+
|
|
605
|
+
<form
|
|
606
|
+
onSubmit={event => {
|
|
607
|
+
handleSubmit(event, {
|
|
608
|
+
experimental_attachments: attachments,
|
|
609
|
+
});
|
|
610
|
+
setAttachments(undefined);
|
|
611
|
+
if (fileInputRef.current) {
|
|
612
|
+
fileInputRef.current.value = '';
|
|
613
|
+
}
|
|
614
|
+
}}
|
|
615
|
+
data-testid="chat-form"
|
|
616
|
+
>
|
|
617
|
+
<input
|
|
618
|
+
type="file"
|
|
619
|
+
onChange={event => {
|
|
620
|
+
if (event.target.files) {
|
|
621
|
+
setAttachments(event.target.files);
|
|
622
|
+
}
|
|
623
|
+
}}
|
|
624
|
+
multiple
|
|
625
|
+
ref={fileInputRef}
|
|
626
|
+
data-testid="file-input"
|
|
627
|
+
/>
|
|
628
|
+
<input
|
|
629
|
+
value={input}
|
|
630
|
+
onChange={handleInputChange}
|
|
631
|
+
disabled={isLoading}
|
|
632
|
+
data-testid="message-input"
|
|
633
|
+
/>
|
|
634
|
+
<button type="submit" data-testid="submit-button">
|
|
635
|
+
Send
|
|
636
|
+
</button>
|
|
637
|
+
</form>
|
|
638
|
+
</div>
|
|
639
|
+
);
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
beforeEach(() => {
|
|
643
|
+
render(<TestComponent />);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
afterEach(() => {
|
|
647
|
+
vi.restoreAllMocks();
|
|
648
|
+
cleanup();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('should handle text file attachment and submission', async () => {
|
|
652
|
+
const file = new File(['test file content'], 'test.txt', {
|
|
653
|
+
type: 'text/plain',
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const { requestBody } = mockFetchDataStream({
|
|
657
|
+
url: '/api/stream-chat',
|
|
658
|
+
chunks: ['0:"Response to message with text attachment"\n'],
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const fileInput = screen.getByTestId('file-input');
|
|
662
|
+
await userEvent.upload(fileInput, file);
|
|
663
|
+
|
|
664
|
+
const messageInput = screen.getByTestId('message-input');
|
|
665
|
+
await userEvent.type(messageInput, 'Message with text attachment');
|
|
666
|
+
|
|
667
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
668
|
+
await userEvent.click(submitButton);
|
|
669
|
+
|
|
670
|
+
const sentBody = JSON.parse((await requestBody) as string);
|
|
671
|
+
expect(sentBody.messages[0].content).toBe('Message with text attachment');
|
|
672
|
+
expect(sentBody.messages[0].experimental_attachments).toBeDefined();
|
|
673
|
+
expect(sentBody.messages[0].experimental_attachments.length).toBe(1);
|
|
674
|
+
expect(sentBody.messages[0].experimental_attachments[0].name).toBe(
|
|
675
|
+
'test.txt',
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
await screen.findByTestId('message-0');
|
|
679
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent(
|
|
680
|
+
'User: Message with text attachment',
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
await screen.findByTestId('attachment-0');
|
|
684
|
+
expect(screen.getByTestId('attachment-0')).toHaveTextContent(
|
|
685
|
+
'test file content',
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
await screen.findByTestId('message-1');
|
|
689
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
690
|
+
'AI: Response to message with text attachment',
|
|
691
|
+
);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// image file
|
|
695
|
+
|
|
696
|
+
it('should handle image file attachment and submission', async () => {
|
|
697
|
+
const file = new File(['test image content'], 'test.png', {
|
|
698
|
+
type: 'image/png',
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const { requestBody } = mockFetchDataStream({
|
|
702
|
+
url: '/api/stream-chat',
|
|
703
|
+
chunks: ['0:"Response to message with image attachment"\n'],
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const fileInput = screen.getByTestId('file-input');
|
|
707
|
+
await userEvent.upload(fileInput, file);
|
|
708
|
+
|
|
709
|
+
const messageInput = screen.getByTestId('message-input');
|
|
710
|
+
await userEvent.type(messageInput, 'Message with image attachment');
|
|
711
|
+
|
|
712
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
713
|
+
await userEvent.click(submitButton);
|
|
714
|
+
|
|
715
|
+
const sentBody = JSON.parse((await requestBody) as string);
|
|
716
|
+
expect(sentBody.messages[0].content).toBe('Message with image attachment');
|
|
717
|
+
expect(sentBody.messages[0].experimental_attachments).toBeDefined();
|
|
718
|
+
expect(sentBody.messages[0].experimental_attachments.length).toBe(1);
|
|
719
|
+
expect(sentBody.messages[0].experimental_attachments[0].name).toBe(
|
|
720
|
+
'test.png',
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
await screen.findByTestId('message-0');
|
|
724
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent(
|
|
725
|
+
'User: Message with image attachment',
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
await screen.findByTestId('attachment-0');
|
|
729
|
+
expect(screen.getByTestId('attachment-0')).toHaveAttribute(
|
|
730
|
+
'src',
|
|
731
|
+
expect.stringContaining('data:image/png;base64'),
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
await screen.findByTestId('message-1');
|
|
735
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
736
|
+
'AI: Response to message with image attachment',
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
describe('file attachments with url', () => {
|
|
742
|
+
const TestComponent = () => {
|
|
743
|
+
const { messages, handleSubmit, handleInputChange, isLoading, input } =
|
|
744
|
+
useChat({
|
|
745
|
+
api: '/api/stream-chat',
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<div>
|
|
750
|
+
{messages.map((m, idx) => (
|
|
751
|
+
<div data-testid={`message-${idx}`} key={m.id}>
|
|
752
|
+
{m.role === 'user' ? 'User: ' : 'AI: '}
|
|
753
|
+
{m.content}
|
|
754
|
+
{m.experimental_attachments?.map(attachment => {
|
|
755
|
+
if (attachment.contentType?.startsWith('image/')) {
|
|
756
|
+
return (
|
|
757
|
+
<img
|
|
758
|
+
key={attachment.name}
|
|
759
|
+
src={attachment.url}
|
|
760
|
+
alt={attachment.name}
|
|
761
|
+
data-testid={`attachment-${idx}`}
|
|
762
|
+
/>
|
|
763
|
+
);
|
|
764
|
+
} else if (attachment.contentType?.startsWith('text/')) {
|
|
765
|
+
return (
|
|
766
|
+
<div key={attachment.name} data-testid={`attachment-${idx}`}>
|
|
767
|
+
{Buffer.from(
|
|
768
|
+
attachment.url.split(',')[1],
|
|
769
|
+
'base64',
|
|
770
|
+
).toString('utf-8')}
|
|
771
|
+
</div>
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
})}
|
|
775
|
+
</div>
|
|
776
|
+
))}
|
|
777
|
+
|
|
778
|
+
<form
|
|
779
|
+
onSubmit={event => {
|
|
780
|
+
handleSubmit(event, {
|
|
781
|
+
experimental_attachments: [
|
|
782
|
+
{
|
|
783
|
+
name: 'test.png',
|
|
784
|
+
contentType: 'image/png',
|
|
785
|
+
url: 'https://example.com/image.png',
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
});
|
|
789
|
+
}}
|
|
790
|
+
data-testid="chat-form"
|
|
791
|
+
>
|
|
792
|
+
<input
|
|
793
|
+
value={input}
|
|
794
|
+
onChange={handleInputChange}
|
|
795
|
+
disabled={isLoading}
|
|
796
|
+
data-testid="message-input"
|
|
797
|
+
/>
|
|
798
|
+
<button type="submit" data-testid="submit-button">
|
|
799
|
+
Send
|
|
800
|
+
</button>
|
|
801
|
+
</form>
|
|
802
|
+
</div>
|
|
803
|
+
);
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
beforeEach(() => {
|
|
807
|
+
render(<TestComponent />);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
afterEach(() => {
|
|
811
|
+
vi.restoreAllMocks();
|
|
812
|
+
cleanup();
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('should handle image file attachment and submission', async () => {
|
|
816
|
+
const { requestBody } = mockFetchDataStream({
|
|
817
|
+
url: '/api/stream-chat',
|
|
818
|
+
chunks: ['0:"Response to message with image attachment"\n'],
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const messageInput = screen.getByTestId('message-input');
|
|
822
|
+
await userEvent.type(messageInput, 'Message with image attachment');
|
|
823
|
+
|
|
824
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
825
|
+
await userEvent.click(submitButton);
|
|
826
|
+
|
|
827
|
+
const sentBody = JSON.parse((await requestBody) as string);
|
|
828
|
+
expect(sentBody.messages[0].content).toBe('Message with image attachment');
|
|
829
|
+
expect(sentBody.messages[0].experimental_attachments).toBeDefined();
|
|
830
|
+
expect(sentBody.messages[0].experimental_attachments.length).toBe(1);
|
|
831
|
+
expect(sentBody.messages[0].experimental_attachments[0].name).toBe(
|
|
832
|
+
'test.png',
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
await screen.findByTestId('message-0');
|
|
836
|
+
expect(screen.getByTestId('message-0')).toHaveTextContent(
|
|
837
|
+
'User: Message with image attachment',
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
await screen.findByTestId('attachment-0');
|
|
841
|
+
expect(screen.getByTestId('attachment-0')).toHaveAttribute(
|
|
842
|
+
'src',
|
|
843
|
+
expect.stringContaining('https://example.com/image.png'),
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
await screen.findByTestId('message-1');
|
|
847
|
+
expect(screen.getByTestId('message-1')).toHaveTextContent(
|
|
848
|
+
'AI: Response to message with image attachment',
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
});
|