@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.
@@ -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 { cleanup, findByText, render, screen } from '@testing-library/react';
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} className="fixed bottom-0 w-full p-2">
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
+ });