Spaces:
Running
Running
MingruiZhang
commited on
Commit
•
a8e1cb0
1
Parent(s):
3c9f03f
multi image
Browse files- app/api/chat/route.ts +11 -30
- app/globals.css +2 -2
- components/chat-message-actions.tsx +30 -29
- components/chat-message.tsx +2 -2
- components/chat-panel.tsx +6 -10
- components/chat-share-dialog.tsx +91 -91
- components/chat/ChatList.tsx +13 -58
- components/chat/index.tsx +5 -24
- lib/hooks/useChatWithDataset.ts +102 -0
- lib/types.ts +5 -0
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 {
|
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
|
22 |
-
messages:
|
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
|
35 |
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,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:
|
60 |
-
temperature: 0.
|
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,
|
91 |
-
rgba(255, 255, 255,
|
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 |
-
|
12 |
}
|
13 |
|
14 |
export function ChatMessageActions({
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
}: ChatMessageActionsProps) {
|
19 |
-
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
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:
|
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 |
-
| '
|
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
|
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 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
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 |
-
|
22 |
-
|
23 |
-
|
24 |
}
|
25 |
|
26 |
export function ChatShareDialog({
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
}: ChatShareDialogProps) {
|
32 |
-
|
33 |
-
|
34 |
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
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:
|
11 |
-
isLoading: boolean;
|
12 |
}
|
13 |
|
14 |
-
export function ChatList({ messages
|
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 |
-
{
|
62 |
-
|
63 |
-
|
64 |
-
{index
|
65 |
-
<
|
66 |
-
|
67 |
-
|
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,
|
20 |
-
const [dataset] = useAtom(datasetAtom);
|
21 |
const { messages, append, reload, stop, isLoading, input, setInput } =
|
22 |
-
|
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-
|
41 |
<ImageList />
|
42 |
</div>
|
43 |
<div className="w-1/2 relative overflow-auto">
|
44 |
-
<ChatList messages={messages}
|
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 |
+
};
|