MingruiZhang commited on
Commit
a8e1cb0
1 Parent(s): 3c9f03f

multi image

Browse files
app/api/chat/route.ts CHANGED
@@ -4,11 +4,11 @@ import OpenAI from 'openai';
4
  import { auth } from '@/auth';
5
  import { nanoid } from '@/lib/utils';
6
  import {
 
7
  ChatCompletionContentPart,
8
  ChatCompletionContentPartImage,
9
- ChatCompletionMessageParam,
10
  } from 'openai/resources';
11
- import { DatasetImageEntity } from '../../../lib/types';
12
 
13
  export const runtime = 'edge';
14
 
@@ -18,9 +18,8 @@ const openai = new OpenAI({
18
 
19
  export async function POST(req: Request) {
20
  const json = await req.json();
21
- const { messages, dataset } = json as {
22
- messages: ChatCompletionMessageParam[];
23
- dataset: DatasetImageEntity[];
24
  };
25
 
26
  const userId = (await auth())?.user.id;
@@ -31,15 +30,16 @@ export async function POST(req: Request) {
31
  });
32
  }
33
 
34
- const messagesWithImage: ChatCompletionMessageParam[] = messages.map(
35
  message => {
36
- if (message.role !== 'user') return message;
 
37
  const contentWithImage: ChatCompletionContentPart[] = [
38
  {
39
  type: 'text',
40
  text: message.content as string,
41
  },
42
- ...dataset.map(
43
  entity =>
44
  ({
45
  type: 'image_url',
@@ -56,32 +56,13 @@ export async function POST(req: Request) {
56
 
57
  const res = await openai.chat.completions.create({
58
  model: 'gpt-4-vision-preview',
59
- messages: messagesWithImage,
60
- temperature: 0.7,
61
  stream: true,
62
  max_tokens: 300,
63
  });
64
 
65
- const stream = OpenAIStream(res, {
66
- async onCompletion(completion) {
67
- const title = json.messages[0].content.substring(0, 100);
68
- const id = json.id ?? nanoid();
69
- const createdAt = Date.now();
70
- const payload = {
71
- id,
72
- title,
73
- userId,
74
- createdAt,
75
- messages: [
76
- ...messages,
77
- {
78
- content: completion,
79
- role: 'assistant',
80
- },
81
- ],
82
- };
83
- },
84
- });
85
 
86
  return new StreamingTextResponse(stream);
87
  }
 
4
  import { auth } from '@/auth';
5
  import { nanoid } from '@/lib/utils';
6
  import {
7
+ ChatCompletionMessageParam,
8
  ChatCompletionContentPart,
9
  ChatCompletionContentPartImage,
 
10
  } from 'openai/resources';
11
+ import { MessageWithSelectedDataset } from '../../../lib/types';
12
 
13
  export const runtime = 'edge';
14
 
 
18
 
19
  export async function POST(req: Request) {
20
  const json = await req.json();
21
+ const { messages } = json as {
22
+ messages: MessageWithSelectedDataset[];
 
23
  };
24
 
25
  const userId = (await auth())?.user.id;
 
30
  });
31
  }
32
 
33
+ const formattedMessage: ChatCompletionMessageParam[] = messages.map(
34
  message => {
35
+ const { dataset, ...rest } = message;
36
+
37
  const contentWithImage: ChatCompletionContentPart[] = [
38
  {
39
  type: 'text',
40
  text: message.content as string,
41
  },
42
+ ...(dataset ?? []).map(
43
  entity =>
44
  ({
45
  type: 'image_url',
 
56
 
57
  const res = await openai.chat.completions.create({
58
  model: 'gpt-4-vision-preview',
59
+ messages: formattedMessage,
60
+ temperature: 0.3,
61
  stream: true,
62
  max_tokens: 300,
63
  });
64
 
65
+ const stream = OpenAIStream(res);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  return new StreamingTextResponse(stream);
68
  }
app/globals.css CHANGED
@@ -87,8 +87,8 @@
87
  height: 50px;
88
  background: linear-gradient(
89
  to bottom,
90
- rgba(255, 255, 255, 0),
91
- rgba(255, 255, 255, 1)
92
  );
93
  pointer-events: none;
94
  }
 
87
  height: 50px;
88
  background: linear-gradient(
89
  to bottom,
90
+ rgba(255, 255, 255, 1),
91
+ rgba(255, 255, 255, 0)
92
  );
93
  pointer-events: none;
94
  }
components/chat-message-actions.tsx CHANGED
@@ -1,40 +1,41 @@
1
- 'use client'
2
 
3
- import { type Message } from 'ai'
4
 
5
- import { Button } from '@/components/ui/button'
6
- import { IconCheck, IconCopy } from '@/components/ui/icons'
7
- import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
8
- import { cn } from '@/lib/utils'
 
9
 
10
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
11
- message: Message
12
  }
13
 
14
  export function ChatMessageActions({
15
- message,
16
- className,
17
- ...props
18
  }: ChatMessageActionsProps) {
19
- const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
20
 
21
- const onCopy = () => {
22
- if (isCopied) return
23
- copyToClipboard(message.content)
24
- }
25
 
26
- return (
27
- <div
28
- className={cn(
29
- 'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
30
- className
31
- )}
32
- {...props}
33
- >
34
- <Button variant="ghost" size="icon" onClick={onCopy}>
35
- {isCopied ? <IconCheck /> : <IconCopy />}
36
- <span className="sr-only">Copy message</span>
37
- </Button>
38
- </div>
39
- )
40
  }
 
1
+ 'use client';
2
 
3
+ import { type Message } from 'ai';
4
 
5
+ import { Button } from '@/components/ui/button';
6
+ import { IconCheck, IconCopy } from '@/components/ui/icons';
7
+ import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard';
8
+ import { cn } from '@/lib/utils';
9
+ import { MessageWithSelectedDataset } from '../lib/types';
10
 
11
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
+ message: MessageWithSelectedDataset;
13
  }
14
 
15
  export function ChatMessageActions({
16
+ message,
17
+ className,
18
+ ...props
19
  }: ChatMessageActionsProps) {
20
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
21
 
22
+ const onCopy = () => {
23
+ if (isCopied) return;
24
+ copyToClipboard(message.content);
25
+ };
26
 
27
+ return (
28
+ <div
29
+ className={cn(
30
+ 'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
31
+ className,
32
+ )}
33
+ {...props}
34
+ >
35
+ <Button variant="ghost" size="icon" onClick={onCopy}>
36
+ {isCopied ? <IconCheck /> : <IconCopy />}
37
+ <span className="sr-only">Copy message</span>
38
+ </Button>
39
+ </div>
40
+ );
41
  }
components/chat-message.tsx CHANGED
@@ -1,7 +1,6 @@
1
  // Inspired by Chatbot-UI and modified to fit the needs of this project
2
  // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3
 
4
- import { Message } from 'ai';
5
  import remarkGfm from 'remark-gfm';
6
  import remarkMath from 'remark-math';
7
 
@@ -10,9 +9,10 @@ import { CodeBlock } from '@/components/ui/codeblock';
10
  import { MemoizedReactMarkdown } from '@/components/markdown';
11
  import { IconOpenAI, IconUser } from '@/components/ui/icons';
12
  import { ChatMessageActions } from '@/components/chat-message-actions';
 
13
 
14
  export interface ChatMessageProps {
15
- message: Message;
16
  }
17
 
18
  export function ChatMessage({ message, ...props }: ChatMessageProps) {
 
1
  // Inspired by Chatbot-UI and modified to fit the needs of this project
2
  // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3
 
 
4
  import remarkGfm from 'remark-gfm';
5
  import remarkMath from 'remark-math';
6
 
 
9
  import { MemoizedReactMarkdown } from '@/components/markdown';
10
  import { IconOpenAI, IconUser } from '@/components/ui/icons';
11
  import { ChatMessageActions } from '@/components/chat-message-actions';
12
+ import { MessageWithSelectedDataset } from '../lib/types';
13
 
14
  export interface ChatMessageProps {
15
+ message: MessageWithSelectedDataset;
16
  }
17
 
18
  export function ChatMessage({ message, ...props }: ChatMessageProps) {
components/chat-panel.tsx CHANGED
@@ -7,20 +7,16 @@ import { PromptForm } from '@/components/prompt-form';
7
  import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom';
8
  import { IconRefresh, IconShare, IconStop } from '@/components/ui/icons';
9
  import { ChatShareDialog } from '@/components/chat-share-dialog';
 
10
 
11
  export interface ChatPanelProps
12
  extends Pick<
13
  UseChatHelpers,
14
- | 'append'
15
- | 'isLoading'
16
- | 'reload'
17
- | 'messages'
18
- | 'stop'
19
- | 'input'
20
- | 'setInput'
21
  > {
22
  id?: string;
23
  title?: string;
 
24
  }
25
 
26
  export function ChatPanel({
@@ -37,7 +33,7 @@ export function ChatPanel({
37
  const [shareDialogOpen, setShareDialogOpen] = React.useState(false);
38
 
39
  return (
40
- <div className="fixed inset-x-0 bottom-0 w-full bg-gradient-to-b from-muted/30 from-0% to-muted/30 to-50% animate-in duration-300 ease-in-out dark:from-background/10 dark:from-10% dark:to-background/80 peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]">
41
  <ButtonScrollToBottom />
42
  <div className="mx-auto sm:max-w-3xl sm:px-4">
43
  <div className="flex items-center justify-center h-12">
@@ -57,7 +53,7 @@ export function ChatPanel({
57
  <IconRefresh className="mr-2" />
58
  Regenerate response
59
  </Button>
60
- {id && title ? (
61
  <>
62
  <Button
63
  variant="outline"
@@ -78,7 +74,7 @@ export function ChatPanel({
78
  }}
79
  />
80
  </>
81
- ) : null}
82
  </div>
83
  )
84
  )}
 
7
  import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom';
8
  import { IconRefresh, IconShare, IconStop } from '@/components/ui/icons';
9
  import { ChatShareDialog } from '@/components/chat-share-dialog';
10
+ import { MessageWithSelectedDataset } from '../lib/types';
11
 
12
  export interface ChatPanelProps
13
  extends Pick<
14
  UseChatHelpers,
15
+ 'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput'
 
 
 
 
 
 
16
  > {
17
  id?: string;
18
  title?: string;
19
+ messages: MessageWithSelectedDataset[];
20
  }
21
 
22
  export function ChatPanel({
 
33
  const [shareDialogOpen, setShareDialogOpen] = React.useState(false);
34
 
35
  return (
36
+ <div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]">
37
  <ButtonScrollToBottom />
38
  <div className="mx-auto sm:max-w-3xl sm:px-4">
39
  <div className="flex items-center justify-center h-12">
 
53
  <IconRefresh className="mr-2" />
54
  Regenerate response
55
  </Button>
56
+ {/* {id && title ? (
57
  <>
58
  <Button
59
  variant="outline"
 
74
  }}
75
  />
76
  </>
77
+ ) : null} */}
78
  </div>
79
  )
80
  )}
components/chat-share-dialog.tsx CHANGED
@@ -1,106 +1,106 @@
1
- 'use client'
2
 
3
- import * as React from 'react'
4
- import { type DialogProps } from '@radix-ui/react-dialog'
5
- import { toast } from 'react-hot-toast'
6
 
7
- import { ServerActionResult, type Chat } from '@/lib/types'
8
- import { Button } from '@/components/ui/button'
9
  import {
10
- Dialog,
11
- DialogContent,
12
- DialogDescription,
13
- DialogFooter,
14
- DialogHeader,
15
- DialogTitle
16
- } from '@/components/ui/dialog'
17
- import { IconSpinner } from '@/components/ui/icons'
18
- import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
19
 
20
  interface ChatShareDialogProps extends DialogProps {
21
- chat: Pick<Chat, 'id' | 'title' | 'messages'>
22
- shareChat: (id: string) => ServerActionResult<Chat>
23
- onCopy: () => void
24
  }
25
 
26
  export function ChatShareDialog({
27
- chat,
28
- shareChat,
29
- onCopy,
30
- ...props
31
  }: ChatShareDialogProps) {
32
- const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 })
33
- const [isSharePending, startShareTransition] = React.useTransition()
34
 
35
- const copyShareLink = React.useCallback(
36
- async (chat: Chat) => {
37
- if (!chat.sharePath) {
38
- return toast.error('Could not copy share link to clipboard')
39
- }
40
 
41
- const url = new URL(window.location.href)
42
- url.pathname = chat.sharePath
43
- copyToClipboard(url.toString())
44
- onCopy()
45
- toast.success('Share link copied to clipboard', {
46
- style: {
47
- borderRadius: '10px',
48
- background: '#333',
49
- color: '#fff',
50
- fontSize: '14px'
51
- },
52
- iconTheme: {
53
- primary: 'white',
54
- secondary: 'black'
55
- }
56
- })
57
- },
58
- [copyToClipboard, onCopy]
59
- )
60
 
61
- return (
62
- <Dialog {...props}>
63
- <DialogContent>
64
- <DialogHeader>
65
- <DialogTitle>Share link to chat</DialogTitle>
66
- <DialogDescription>
67
- Anyone with the URL will be able to view the shared chat.
68
- </DialogDescription>
69
- </DialogHeader>
70
- <div className="p-4 space-y-1 text-sm border rounded-md">
71
- <div className="font-medium">{chat.title}</div>
72
- <div className="text-muted-foreground">
73
- {chat.messages.length} messages
74
- </div>
75
- </div>
76
- <DialogFooter className="items-center">
77
- <Button
78
- disabled={isSharePending}
79
- onClick={() => {
80
- // @ts-ignore
81
- startShareTransition(async () => {
82
- const result = await shareChat(chat.id)
83
 
84
- if (result && 'error' in result) {
85
- toast.error(result.error)
86
- return
87
- }
88
 
89
- copyShareLink(result)
90
- })
91
- }}
92
- >
93
- {isSharePending ? (
94
- <>
95
- <IconSpinner className="mr-2 animate-spin" />
96
- Copying...
97
- </>
98
- ) : (
99
- <>Copy link</>
100
- )}
101
- </Button>
102
- </DialogFooter>
103
- </DialogContent>
104
- </Dialog>
105
- )
106
  }
 
1
+ 'use client';
2
 
3
+ import * as React from 'react';
4
+ import { type DialogProps } from '@radix-ui/react-dialog';
5
+ import { toast } from 'react-hot-toast';
6
 
7
+ import { ServerActionResult, type Chat } from '@/lib/types';
8
+ import { Button } from '@/components/ui/button';
9
  import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from '@/components/ui/dialog';
17
+ import { IconSpinner } from '@/components/ui/icons';
18
+ import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard';
19
 
20
  interface ChatShareDialogProps extends DialogProps {
21
+ chat: Pick<Chat, 'id' | 'title' | 'messages'>;
22
+ shareChat: (id: string) => ServerActionResult<Chat>;
23
+ onCopy: () => void;
24
  }
25
 
26
  export function ChatShareDialog({
27
+ chat,
28
+ shareChat,
29
+ onCopy,
30
+ ...props
31
  }: ChatShareDialogProps) {
32
+ const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
33
+ const [isSharePending, startShareTransition] = React.useTransition();
34
 
35
+ const copyShareLink = React.useCallback(
36
+ async (chat: Chat) => {
37
+ if (!chat.sharePath) {
38
+ return toast.error('Could not copy share link to clipboard');
39
+ }
40
 
41
+ const url = new URL(window.location.href);
42
+ url.pathname = chat.sharePath;
43
+ copyToClipboard(url.toString());
44
+ onCopy();
45
+ toast.success('Share link copied to clipboard', {
46
+ style: {
47
+ borderRadius: '10px',
48
+ background: '#333',
49
+ color: '#fff',
50
+ fontSize: '14px',
51
+ },
52
+ iconTheme: {
53
+ primary: 'white',
54
+ secondary: 'black',
55
+ },
56
+ });
57
+ },
58
+ [copyToClipboard, onCopy],
59
+ );
60
 
61
+ return (
62
+ <Dialog {...props}>
63
+ <DialogContent>
64
+ <DialogHeader>
65
+ <DialogTitle>Share link to chat</DialogTitle>
66
+ <DialogDescription>
67
+ Anyone with the URL will be able to view the shared chat.
68
+ </DialogDescription>
69
+ </DialogHeader>
70
+ <div className="p-4 space-y-1 text-sm border rounded-md">
71
+ <div className="font-medium">{chat.title}</div>
72
+ <div className="text-muted-foreground">
73
+ {chat.messages.length} messages
74
+ </div>
75
+ </div>
76
+ <DialogFooter className="items-center">
77
+ <Button
78
+ disabled={isSharePending}
79
+ onClick={() => {
80
+ // @ts-ignore
81
+ startShareTransition(async () => {
82
+ const result = await shareChat(chat.id);
83
 
84
+ if (result && 'error' in result) {
85
+ toast.error(result.error);
86
+ return;
87
+ }
88
 
89
+ copyShareLink(result);
90
+ });
91
+ }}
92
+ >
93
+ {isSharePending ? (
94
+ <>
95
+ <IconSpinner className="mr-2 animate-spin" />
96
+ Copying...
97
+ </>
98
+ ) : (
99
+ <>Copy link</>
100
+ )}
101
+ </Button>
102
+ </DialogFooter>
103
+ </DialogContent>
104
+ </Dialog>
105
+ );
106
  }
components/chat/ChatList.tsx CHANGED
@@ -1,71 +1,26 @@
1
  'use client';
2
 
3
- import { type Message } from 'ai';
4
- import { useEffect, useState } from 'react';
5
-
6
  import { Separator } from '@/components/ui/separator';
7
  import { ChatMessage } from '@/components/chat-message';
 
8
 
9
  export interface ChatList {
10
- messages: Message[];
11
- isLoading: boolean;
12
  }
13
 
14
- export function ChatList({ messages, isLoading }: ChatList) {
15
- const [loadingDots, setLoadingDots] = useState('');
16
-
17
- useEffect(() => {
18
- let loadingInterval: NodeJS.Timeout;
19
-
20
- if (isLoading) {
21
- loadingInterval = setInterval(() => {
22
- setLoadingDots(prevMessage => {
23
- switch (prevMessage) {
24
- case '':
25
- return '.';
26
- case '.':
27
- return '..';
28
- case '..':
29
- return '...';
30
- case '...':
31
- return '';
32
- default:
33
- return '';
34
- }
35
- });
36
- }, 500);
37
- }
38
-
39
- return () => {
40
- clearInterval(loadingInterval);
41
- };
42
- }, [isLoading]);
43
-
44
- if (!messages.length) {
45
- return null;
46
- }
47
-
48
- const assistantLoadingMessage: Message = {
49
- id: 'loading',
50
- content: loadingDots,
51
- role: 'assistant',
52
- };
53
-
54
- const messageWithLoading =
55
- isLoading && messages[messages.length - 1].role !== 'assistant'
56
- ? [...messages, assistantLoadingMessage]
57
- : messages;
58
-
59
  return (
60
  <div className="relative mx-auto max-w-3xl px-8 pr-12">
61
- {messageWithLoading.map((message, index) => (
62
- <div key={index}>
63
- <ChatMessage message={message} />
64
- {index < messageWithLoading.length - 1 && (
65
- <Separator className="my-4 md:my-8" />
66
- )}
67
- </div>
68
- ))}
 
 
69
  </div>
70
  );
71
  }
 
1
  'use client';
2
 
 
 
 
3
  import { Separator } from '@/components/ui/separator';
4
  import { ChatMessage } from '@/components/chat-message';
5
+ import { MessageWithSelectedDataset } from '../../lib/types';
6
 
7
  export interface ChatList {
8
+ messages: MessageWithSelectedDataset[];
 
9
  }
10
 
11
+ export function ChatList({ messages }: ChatList) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  return (
13
  <div className="relative mx-auto max-w-3xl px-8 pr-12">
14
+ {messages
15
+ .filter(message => message.role !== 'system')
16
+ .map((message, index) => (
17
+ <div key={index}>
18
+ <ChatMessage message={message} />
19
+ {index < messages.length - 1 && (
20
+ <Separator className="my-4 md:my-8" />
21
+ )}
22
+ </div>
23
+ ))}
24
  </div>
25
  );
26
  }
components/chat/index.tsx CHANGED
@@ -1,47 +1,28 @@
1
  'use client';
2
-
3
- import { useChat, type Message } from 'ai/react';
4
-
5
  import { cn } from '@/lib/utils';
6
  import { ChatList } from '@/components/chat/ChatList';
7
  import { ChatPanel } from '@/components/chat-panel';
8
  import { ChatScrollAnchor } from '@/components/chat-scroll-anchor';
9
- import { toast } from 'react-hot-toast';
10
- import { useAtom } from 'jotai';
11
- import { datasetAtom } from '@/state';
12
  import ImageList from './ImageList';
 
13
 
14
  export interface ChatProps extends React.ComponentProps<'div'> {
15
- initialMessages?: Message[];
16
  id?: string;
17
  }
18
 
19
- export function Chat({ id, initialMessages, className }: ChatProps) {
20
- const [dataset] = useAtom(datasetAtom);
21
  const { messages, append, reload, stop, isLoading, input, setInput } =
22
- useChat({
23
- initialMessages,
24
- id,
25
- body: {
26
- id,
27
- dataset: dataset,
28
- },
29
- onResponse(response) {
30
- if (response.status === 401) {
31
- toast.error(response.statusText);
32
- }
33
- },
34
- });
35
 
36
  return (
37
  <>
38
  <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
39
  <div className="flex h-full">
40
- <div className="w-1/2 relative border-r-2 border-gray-200">
41
  <ImageList />
42
  </div>
43
  <div className="w-1/2 relative overflow-auto">
44
- <ChatList messages={messages} isLoading={isLoading} />
45
  <ChatScrollAnchor trackVisibility={isLoading} />
46
  </div>
47
  </div>
 
1
  'use client';
 
 
 
2
  import { cn } from '@/lib/utils';
3
  import { ChatList } from '@/components/chat/ChatList';
4
  import { ChatPanel } from '@/components/chat-panel';
5
  import { ChatScrollAnchor } from '@/components/chat-scroll-anchor';
 
 
 
6
  import ImageList from './ImageList';
7
+ import useChatWithDataset from '../../lib/hooks/useChatWithDataset';
8
 
9
  export interface ChatProps extends React.ComponentProps<'div'> {
 
10
  id?: string;
11
  }
12
 
13
+ export function Chat({ id, className }: ChatProps) {
 
14
  const { messages, append, reload, stop, isLoading, input, setInput } =
15
+ useChatWithDataset();
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  return (
18
  <>
19
  <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
20
  <div className="flex h-full">
21
+ <div className="w-1/2 relative border-r-2 border-gray-300 overflow-auto">
22
  <ImageList />
23
  </div>
24
  <div className="w-1/2 relative overflow-auto">
25
+ <ChatList messages={messages} />
26
  <ChatScrollAnchor trackVisibility={isLoading} />
27
  </div>
28
  </div>
lib/hooks/useChatWithDataset.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useChat, type Message } from 'ai/react';
2
+ import { useAtom } from 'jotai';
3
+ import { toast } from 'react-hot-toast';
4
+ import { datasetAtom } from '../../state';
5
+ import { useEffect, useState } from 'react';
6
+ import { MessageWithSelectedDataset } from '../types';
7
+
8
+ const useChatWithDataset = () => {
9
+ const [dataset] = useAtom(datasetAtom);
10
+ const {
11
+ messages,
12
+ append,
13
+ reload,
14
+ stop,
15
+ isLoading,
16
+ input,
17
+ setInput,
18
+ setMessages,
19
+ } = useChat({
20
+ sendExtraMessageFields: true,
21
+ onResponse(response) {
22
+ if (response.status !== 200) {
23
+ toast.error(response.statusText);
24
+ }
25
+ },
26
+ });
27
+
28
+ const [loadingDots, setLoadingDots] = useState('');
29
+
30
+ useEffect(() => {
31
+ let loadingInterval: NodeJS.Timeout;
32
+
33
+ if (isLoading) {
34
+ loadingInterval = setInterval(() => {
35
+ setLoadingDots(prevMessage => {
36
+ switch (prevMessage) {
37
+ case '':
38
+ return '.';
39
+ case '.':
40
+ return '..';
41
+ case '..':
42
+ return '...';
43
+ case '...':
44
+ return '';
45
+ default:
46
+ return '';
47
+ }
48
+ });
49
+ }, 500);
50
+ }
51
+
52
+ return () => {
53
+ clearInterval(loadingInterval);
54
+ };
55
+ }, [isLoading]);
56
+
57
+ const assistantLoadingMessage = {
58
+ id: 'loading',
59
+ content: loadingDots,
60
+ role: 'assistant',
61
+ };
62
+
63
+ const messageWithLoading =
64
+ isLoading &&
65
+ messages.length &&
66
+ messages[messages.length - 1].role !== 'assistant'
67
+ ? [...messages, assistantLoadingMessage]
68
+ : messages;
69
+
70
+ const selectedDataset = dataset.find(entity => entity.selected)
71
+ ? dataset.filter(entity => entity.selected)
72
+ : // If there is no selected dataset, use the entire dataset
73
+ dataset;
74
+
75
+ const appendWithDataset: typeof append = message => {
76
+ const newSystemMessage: Message = {
77
+ id: 'fake-id',
78
+ content:
79
+ 'For the next prompt, here are names of images provided by user, please use these name if you need reference: ' +
80
+ selectedDataset.map(entity => entity.name).join(', '),
81
+ role: 'system',
82
+ };
83
+ setMessages([...messages, newSystemMessage]);
84
+ return append({
85
+ ...message,
86
+ // @ts-ignore this is extra fields
87
+ dataset: selectedDataset,
88
+ } satisfies MessageWithSelectedDataset);
89
+ };
90
+
91
+ return {
92
+ messages: messageWithLoading as MessageWithSelectedDataset[],
93
+ append: appendWithDataset,
94
+ reload,
95
+ stop,
96
+ isLoading,
97
+ input,
98
+ setInput,
99
+ };
100
+ };
101
+
102
+ export default useChatWithDataset;
lib/types.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { type Message } from 'ai';
 
2
 
3
  export interface Chat extends Record<string, any> {
4
  id: string;
@@ -22,3 +23,7 @@ export type DatasetImageEntity = {
22
  selected: boolean;
23
  name: string;
24
  };
 
 
 
 
 
1
  import { type Message } from 'ai';
2
+ import { CreateMessage } from 'ai/react/dist';
3
 
4
  export interface Chat extends Record<string, any> {
5
  id: string;
 
23
  selected: boolean;
24
  name: string;
25
  };
26
+
27
+ export type MessageWithSelectedDataset = (Message | CreateMessage) & {
28
+ dataset: DatasetImageEntity[];
29
+ };