Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
Commit
·
52b4c36
1
Parent(s):
9eec735
chatgpt-4v
Browse files- .vscode.settings.json +3 -0
- app/(chat)/page.tsx +15 -4
- app/actions.ts +7 -7
- app/api/chat/route.ts +75 -50
- app/globals.css +70 -49
- components/chat-message.tsx +63 -66
- components/chat.tsx +84 -67
- components/empty-screen.tsx +9 -28
- components/login-button.tsx +33 -33
- next.config.js +9 -11
- package.json +1 -0
- prettier.config.cjs +6 -5
- state/index.ts +5 -0
- yarn.lock +6 -0
.vscode.settings.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"prettier.prettierPath": "./node_modules/prettier/index.cjs"
|
3 |
+
}
|
app/(chat)/page.tsx
CHANGED
@@ -1,8 +1,19 @@
|
|
1 |
-
import { nanoid } from '@/lib/utils'
|
2 |
-
import { Chat } from '@/components/chat'
|
3 |
|
4 |
export default function IndexPage() {
|
5 |
-
|
6 |
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
}
|
|
|
1 |
+
import { nanoid } from '@/lib/utils';
|
2 |
+
import { Chat } from '@/components/chat';
|
3 |
|
4 |
export default function IndexPage() {
|
5 |
+
const id = nanoid();
|
6 |
|
7 |
+
return (
|
8 |
+
<Chat
|
9 |
+
id={id}
|
10 |
+
initialMessages={[
|
11 |
+
{
|
12 |
+
id: '123',
|
13 |
+
content: 'Hi, what do you want to know about this image?',
|
14 |
+
role: 'system',
|
15 |
+
},
|
16 |
+
]}
|
17 |
+
/>
|
18 |
+
);
|
19 |
}
|
app/actions.ts
CHANGED
@@ -15,7 +15,7 @@ export async function getChats(userId?: string | null) {
|
|
15 |
try {
|
16 |
const pipeline = kv.pipeline()
|
17 |
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
|
18 |
-
rev: true
|
19 |
})
|
20 |
|
21 |
for (const chat of chats) {
|
@@ -45,7 +45,7 @@ export async function removeChat({ id, path }: { id: string; path: string }) {
|
|
45 |
|
46 |
if (!session) {
|
47 |
return {
|
48 |
-
error: 'Unauthorized'
|
49 |
}
|
50 |
}
|
51 |
|
@@ -54,7 +54,7 @@ export async function removeChat({ id, path }: { id: string; path: string }) {
|
|
54 |
|
55 |
if (uid !== session?.user?.id) {
|
56 |
return {
|
57 |
-
error: 'Unauthorized'
|
58 |
}
|
59 |
}
|
60 |
|
@@ -70,7 +70,7 @@ export async function clearChats() {
|
|
70 |
|
71 |
if (!session?.user?.id) {
|
72 |
return {
|
73 |
-
error: 'Unauthorized'
|
74 |
}
|
75 |
}
|
76 |
|
@@ -106,7 +106,7 @@ export async function shareChat(id: string) {
|
|
106 |
|
107 |
if (!session?.user?.id) {
|
108 |
return {
|
109 |
-
error: 'Unauthorized'
|
110 |
}
|
111 |
}
|
112 |
|
@@ -114,13 +114,13 @@ export async function shareChat(id: string) {
|
|
114 |
|
115 |
if (!chat || chat.userId !== session.user.id) {
|
116 |
return {
|
117 |
-
error: 'Something went wrong'
|
118 |
}
|
119 |
}
|
120 |
|
121 |
const payload = {
|
122 |
...chat,
|
123 |
-
sharePath: `/share/${chat.id}
|
124 |
}
|
125 |
|
126 |
await kv.hmset(`chat:${chat.id}`, payload)
|
|
|
15 |
try {
|
16 |
const pipeline = kv.pipeline()
|
17 |
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
|
18 |
+
rev: true,
|
19 |
})
|
20 |
|
21 |
for (const chat of chats) {
|
|
|
45 |
|
46 |
if (!session) {
|
47 |
return {
|
48 |
+
error: 'Unauthorized',
|
49 |
}
|
50 |
}
|
51 |
|
|
|
54 |
|
55 |
if (uid !== session?.user?.id) {
|
56 |
return {
|
57 |
+
error: 'Unauthorized',
|
58 |
}
|
59 |
}
|
60 |
|
|
|
70 |
|
71 |
if (!session?.user?.id) {
|
72 |
return {
|
73 |
+
error: 'Unauthorized',
|
74 |
}
|
75 |
}
|
76 |
|
|
|
106 |
|
107 |
if (!session?.user?.id) {
|
108 |
return {
|
109 |
+
error: 'Unauthorized',
|
110 |
}
|
111 |
}
|
112 |
|
|
|
114 |
|
115 |
if (!chat || chat.userId !== session.user.id) {
|
116 |
return {
|
117 |
+
error: 'Something went wrong',
|
118 |
}
|
119 |
}
|
120 |
|
121 |
const payload = {
|
122 |
...chat,
|
123 |
+
sharePath: `/share/${chat.id}`,
|
124 |
}
|
125 |
|
126 |
await kv.hmset(`chat:${chat.id}`, payload)
|
app/api/chat/route.ts
CHANGED
@@ -1,57 +1,82 @@
|
|
1 |
-
import { OpenAIStream, StreamingTextResponse } from 'ai'
|
2 |
-
import OpenAI from 'openai'
|
3 |
|
4 |
-
import { auth } from '@/auth'
|
5 |
-
import { nanoid } from '@/lib/utils'
|
|
|
|
|
|
|
|
|
6 |
|
7 |
-
export const runtime = 'edge'
|
8 |
|
9 |
const openai = new OpenAI({
|
10 |
-
|
11 |
-
})
|
12 |
|
13 |
export async function POST(req: Request) {
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
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 |
}
|
|
|
1 |
+
import { OpenAIStream, StreamingTextResponse } from 'ai';
|
2 |
+
import OpenAI from 'openai';
|
3 |
|
4 |
+
import { auth } from '@/auth';
|
5 |
+
import { nanoid } from '@/lib/utils';
|
6 |
+
import {
|
7 |
+
ChatCompletionContentPart,
|
8 |
+
ChatCompletionMessageParam,
|
9 |
+
} from 'openai/resources';
|
10 |
|
11 |
+
export const runtime = 'edge';
|
12 |
|
13 |
const openai = new OpenAI({
|
14 |
+
apiKey: process.env.OPENAI_API_KEY,
|
15 |
+
});
|
16 |
|
17 |
export async function POST(req: Request) {
|
18 |
+
const json = await req.json();
|
19 |
+
const { messages, image } = json as {
|
20 |
+
messages: ChatCompletionMessageParam[];
|
21 |
+
image: string;
|
22 |
+
};
|
23 |
+
console.log('[Ming] ~ POST ~ messages:', messages);
|
24 |
+
const userId = (await auth())?.user.id;
|
25 |
+
|
26 |
+
if (!userId) {
|
27 |
+
return new Response('Unauthorized', {
|
28 |
+
status: 401,
|
29 |
+
});
|
30 |
+
}
|
31 |
+
|
32 |
+
const messagesWithImage: ChatCompletionMessageParam[] = messages.map(
|
33 |
+
message => {
|
34 |
+
if (message.role !== 'user') return message;
|
35 |
+
const contentWithImage: ChatCompletionContentPart[] = [
|
36 |
+
{
|
37 |
+
type: 'text',
|
38 |
+
text: message.content as string,
|
39 |
+
},
|
40 |
+
{
|
41 |
+
type: 'image_url',
|
42 |
+
image_url: { url: image },
|
43 |
+
},
|
44 |
+
];
|
45 |
+
return {
|
46 |
+
role: 'user',
|
47 |
+
content: contentWithImage,
|
48 |
+
};
|
49 |
+
},
|
50 |
+
);
|
51 |
+
|
52 |
+
const res = await openai.chat.completions.create({
|
53 |
+
model: 'gpt-4-vision-preview',
|
54 |
+
messages: messagesWithImage,
|
55 |
+
temperature: 0.7,
|
56 |
+
stream: true,
|
57 |
+
max_tokens: 300,
|
58 |
+
});
|
59 |
+
|
60 |
+
const stream = OpenAIStream(res, {
|
61 |
+
async onCompletion(completion) {
|
62 |
+
const title = json.messages[0].content.substring(0, 100);
|
63 |
+
const id = json.id ?? nanoid();
|
64 |
+
const createdAt = Date.now();
|
65 |
+
const payload = {
|
66 |
+
id,
|
67 |
+
title,
|
68 |
+
userId,
|
69 |
+
createdAt,
|
70 |
+
messages: [
|
71 |
+
...messages,
|
72 |
+
{
|
73 |
+
content: completion,
|
74 |
+
role: 'assistant',
|
75 |
+
},
|
76 |
+
],
|
77 |
+
};
|
78 |
+
},
|
79 |
+
});
|
80 |
+
|
81 |
+
return new StreamingTextResponse(stream);
|
82 |
}
|
app/globals.css
CHANGED
@@ -3,76 +3,97 @@
|
|
3 |
@tailwind utilities;
|
4 |
|
5 |
@layer base {
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
|
10 |
-
|
11 |
-
|
12 |
|
13 |
-
|
14 |
-
|
15 |
|
16 |
-
|
17 |
-
|
18 |
|
19 |
-
|
20 |
-
|
21 |
|
22 |
-
|
23 |
-
|
24 |
|
25 |
-
|
26 |
-
|
27 |
|
28 |
-
|
29 |
-
|
30 |
|
31 |
-
|
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 |
@layer base {
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
}
|
|
|
3 |
@tailwind utilities;
|
4 |
|
5 |
@layer base {
|
6 |
+
:root {
|
7 |
+
--background: 0 0% 100%;
|
8 |
+
--foreground: 240 10% 3.9%;
|
9 |
|
10 |
+
--muted: 240 4.8% 95.9%;
|
11 |
+
--muted-foreground: 240 3.8% 46.1%;
|
12 |
|
13 |
+
--popover: 0 0% 100%;
|
14 |
+
--popover-foreground: 240 10% 3.9%;
|
15 |
|
16 |
+
--card: 0 0% 100%;
|
17 |
+
--card-foreground: 240 10% 3.9%;
|
18 |
|
19 |
+
--border: 240 5.9% 90%;
|
20 |
+
--input: 240 5.9% 90%;
|
21 |
|
22 |
+
--primary: 240 5.9% 10%;
|
23 |
+
--primary-foreground: 0 0% 98%;
|
24 |
|
25 |
+
--secondary: 240 4.8% 95.9%;
|
26 |
+
--secondary-foreground: 240 5.9% 10%;
|
27 |
|
28 |
+
--accent: 240 4.8% 95.9%;
|
29 |
+
--accent-foreground: ;
|
30 |
|
31 |
+
--destructive: 0 84.2% 60.2%;
|
32 |
+
--destructive-foreground: 0 0% 98%;
|
33 |
|
34 |
+
--ring: 240 5% 64.9%;
|
35 |
|
36 |
+
--radius: 0.5rem;
|
37 |
+
}
|
38 |
|
39 |
+
.dark {
|
40 |
+
--background: 240 10% 3.9%;
|
41 |
+
--foreground: 0 0% 98%;
|
42 |
|
43 |
+
--muted: 240 3.7% 15.9%;
|
44 |
+
--muted-foreground: 240 5% 64.9%;
|
45 |
|
46 |
+
--popover: 240 10% 3.9%;
|
47 |
+
--popover-foreground: 0 0% 98%;
|
48 |
|
49 |
+
--card: 240 10% 3.9%;
|
50 |
+
--card-foreground: 0 0% 98%;
|
51 |
|
52 |
+
--border: 240 3.7% 15.9%;
|
53 |
+
--input: 240 3.7% 15.9%;
|
54 |
|
55 |
+
--primary: 0 0% 98%;
|
56 |
+
--primary-foreground: 240 5.9% 10%;
|
57 |
|
58 |
+
--secondary: 240 3.7% 15.9%;
|
59 |
+
--secondary-foreground: 0 0% 98%;
|
60 |
|
61 |
+
--accent: 240 3.7% 15.9%;
|
62 |
+
--accent-foreground: ;
|
63 |
|
64 |
+
--destructive: 0 62.8% 30.6%;
|
65 |
+
--destructive-foreground: 0 85.7% 97.3%;
|
66 |
|
67 |
+
--ring: 240 3.7% 15.9%;
|
68 |
+
}
|
69 |
}
|
70 |
|
71 |
@layer base {
|
72 |
+
* {
|
73 |
+
@apply border-border;
|
74 |
+
}
|
75 |
+
body {
|
76 |
+
@apply bg-background text-foreground;
|
77 |
+
}
|
78 |
+
}
|
79 |
+
|
80 |
+
@layer components {
|
81 |
+
.scroll-fade::after {
|
82 |
+
content: '';
|
83 |
+
position: absolute;
|
84 |
+
bottom: 0;
|
85 |
+
left: 0;
|
86 |
+
right: 0;
|
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 |
+
}
|
95 |
+
.scroll-fade:active::after,
|
96 |
+
.scroll-fade:hover::after {
|
97 |
+
background: none;
|
98 |
+
}
|
99 |
}
|
components/chat-message.tsx
CHANGED
@@ -1,80 +1,77 @@
|
|
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 |
|
8 |
-
import { cn } from '@/lib/utils'
|
9 |
-
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 |
-
|
16 |
}
|
17 |
|
18 |
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
<span className="mt-1 cursor-default animate-pulse">▍</span>
|
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 |
}
|
|
|
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 |
|
8 |
+
import { cn } from '@/lib/utils';
|
9 |
+
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) {
|
19 |
+
return (
|
20 |
+
<div className={cn('group relative mb-4 flex items-start')} {...props}>
|
21 |
+
<div
|
22 |
+
className={cn(
|
23 |
+
'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
|
24 |
+
message.role === 'user'
|
25 |
+
? 'bg-background'
|
26 |
+
: 'bg-primary text-primary-foreground',
|
27 |
+
)}
|
28 |
+
>
|
29 |
+
{message.role === 'user' ? <IconUser /> : <IconOpenAI />}
|
30 |
+
</div>
|
31 |
+
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
32 |
+
<MemoizedReactMarkdown
|
33 |
+
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0"
|
34 |
+
remarkPlugins={[remarkGfm, remarkMath]}
|
35 |
+
components={{
|
36 |
+
p({ children }) {
|
37 |
+
return <p className="mb-2 last:mb-0">{children}</p>;
|
38 |
+
},
|
39 |
+
code({ node, inline, className, children, ...props }) {
|
40 |
+
if (children.length) {
|
41 |
+
if (children[0] == '▍') {
|
42 |
+
return (
|
43 |
+
<span className="mt-1 cursor-default animate-pulse">▍</span>
|
44 |
+
);
|
45 |
+
}
|
|
|
|
|
|
|
46 |
|
47 |
+
children[0] = (children[0] as string).replace('`▍`', '▍');
|
48 |
+
}
|
49 |
|
50 |
+
const match = /language-(\w+)/.exec(className || '');
|
51 |
|
52 |
+
if (inline) {
|
53 |
+
return (
|
54 |
+
<code className={className} {...props}>
|
55 |
+
{children}
|
56 |
+
</code>
|
57 |
+
);
|
58 |
+
}
|
59 |
|
60 |
+
return (
|
61 |
+
<CodeBlock
|
62 |
+
key={Math.random()}
|
63 |
+
language={(match && match[1]) || ''}
|
64 |
+
value={String(children).replace(/\n$/, '')}
|
65 |
+
{...props}
|
66 |
+
/>
|
67 |
+
);
|
68 |
+
},
|
69 |
+
}}
|
70 |
+
>
|
71 |
+
{message.content}
|
72 |
+
</MemoizedReactMarkdown>
|
73 |
+
<ChatMessageActions message={message} />
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
);
|
77 |
}
|
components/chat.tsx
CHANGED
@@ -1,76 +1,93 @@
|
|
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-list'
|
7 |
-
import { ChatPanel } from '@/components/chat-panel'
|
8 |
-
import { EmptyScreen } from '@/components/empty-screen'
|
9 |
-
import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
|
10 |
-
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
|
11 |
import {
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
} from '@/components/ui/dialog'
|
19 |
-
import { useState } from 'react'
|
20 |
-
import { Button } from './ui/button'
|
21 |
-
import { Input } from './ui/input'
|
22 |
-
import { toast } from 'react-hot-toast'
|
23 |
-
import { usePathname, useRouter } from 'next/navigation'
|
|
|
|
|
|
|
24 |
|
25 |
export interface ChatProps extends React.ComponentProps<'div'> {
|
26 |
-
|
27 |
-
|
28 |
}
|
29 |
|
30 |
export function Chat({ id, initialMessages, className }: ChatProps) {
|
31 |
-
|
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 |
}
|
|
|
1 |
+
'use client';
|
2 |
|
3 |
+
import { useChat, type Message } from 'ai/react';
|
4 |
+
import '@/app/globals.css';
|
5 |
|
6 |
+
import { cn } from '@/lib/utils';
|
7 |
+
import { ChatList } from '@/components/chat-list';
|
8 |
+
import { ChatPanel } from '@/components/chat-panel';
|
9 |
+
import { EmptyScreen } from '@/components/empty-screen';
|
10 |
+
import { ChatScrollAnchor } from '@/components/chat-scroll-anchor';
|
11 |
+
import { useLocalStorage } from '@/lib/hooks/use-local-storage';
|
12 |
import {
|
13 |
+
Dialog,
|
14 |
+
DialogContent,
|
15 |
+
DialogDescription,
|
16 |
+
DialogFooter,
|
17 |
+
DialogHeader,
|
18 |
+
DialogTitle,
|
19 |
+
} from '@/components/ui/dialog';
|
20 |
+
import { useState } from 'react';
|
21 |
+
import { Button } from './ui/button';
|
22 |
+
import { Input } from './ui/input';
|
23 |
+
import { toast } from 'react-hot-toast';
|
24 |
+
import { usePathname, useRouter } from 'next/navigation';
|
25 |
+
import { useAtomValue } from 'jotai';
|
26 |
+
import { targetImageAtom } from '@/state';
|
27 |
+
import Image from 'next/image';
|
28 |
|
29 |
export interface ChatProps extends React.ComponentProps<'div'> {
|
30 |
+
initialMessages?: Message[];
|
31 |
+
id?: string;
|
32 |
}
|
33 |
|
34 |
export function Chat({ id, initialMessages, className }: ChatProps) {
|
35 |
+
const router = useRouter();
|
36 |
+
const path = usePathname();
|
37 |
+
const targetImage = useAtomValue(targetImageAtom);
|
38 |
+
const { messages, append, reload, stop, isLoading, input, setInput } =
|
39 |
+
useChat({
|
40 |
+
initialMessages,
|
41 |
+
id,
|
42 |
+
body: {
|
43 |
+
id,
|
44 |
+
image: targetImage,
|
45 |
+
},
|
46 |
+
onResponse(response) {
|
47 |
+
if (response.status === 401) {
|
48 |
+
toast.error(response.statusText);
|
49 |
+
}
|
50 |
+
},
|
51 |
+
});
|
52 |
+
return (
|
53 |
+
<>
|
54 |
+
<div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
|
55 |
+
{targetImage ? (
|
56 |
+
<>
|
57 |
+
<div className="flex h-full">
|
58 |
+
<div className="w-1/2 relative border-r-2 border-gray-200">
|
59 |
+
<div className="relative aspect-[1/1] w-full px-8">
|
60 |
+
<Image
|
61 |
+
src={targetImage}
|
62 |
+
alt="target image"
|
63 |
+
layout="responsive"
|
64 |
+
objectFit="contain"
|
65 |
+
width={1000}
|
66 |
+
height={1000}
|
67 |
+
className="rounded-xl shadow-lg"
|
68 |
+
/>
|
69 |
+
</div>
|
70 |
+
</div>
|
71 |
+
<div className="w-1/2 relative overflow-auto">
|
72 |
+
<ChatList messages={messages} />
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
<ChatScrollAnchor trackVisibility={isLoading} />
|
76 |
+
</>
|
77 |
+
) : (
|
78 |
+
<EmptyScreen setInput={setInput} />
|
79 |
+
)}
|
80 |
+
</div>
|
81 |
+
<ChatPanel
|
82 |
+
id={id}
|
83 |
+
isLoading={isLoading}
|
84 |
+
stop={stop}
|
85 |
+
append={append}
|
86 |
+
reload={reload}
|
87 |
+
messages={messages}
|
88 |
+
input={input}
|
89 |
+
setInput={setInput}
|
90 |
+
/>
|
91 |
+
</>
|
92 |
+
);
|
93 |
}
|
components/empty-screen.tsx
CHANGED
@@ -1,31 +1,12 @@
|
|
1 |
-
import { UseChatHelpers } from 'ai/react'
|
2 |
-
|
3 |
-
import { Button } from '@/components/ui/button'
|
4 |
-
import { ExternalLink } from '@/components/external-link'
|
5 |
-
import { IconArrowRight } from '@/components/ui/icons'
|
6 |
-
|
7 |
-
const exampleMessages = [
|
8 |
-
{
|
9 |
-
heading: 'Explain technical concepts',
|
10 |
-
message: `What is a "serverless function"?`
|
11 |
-
},
|
12 |
-
{
|
13 |
-
heading: 'Summarize an article',
|
14 |
-
message: 'Summarize the following article for a 2nd grader: \n'
|
15 |
-
},
|
16 |
-
{
|
17 |
-
heading: 'Draft an email',
|
18 |
-
message: `Draft an email to my boss about the following: \n`
|
19 |
-
}
|
20 |
-
]
|
21 |
|
22 |
export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
}
|
|
|
1 |
+
import { UseChatHelpers } from 'ai/react';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
|
4 |
+
return (
|
5 |
+
<div className="mx-auto max-w-2xl px-4">
|
6 |
+
<div className="rounded-lg border bg-background p-8">
|
7 |
+
<h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
|
8 |
+
<p>Start by uploading an image</p>
|
9 |
+
</div>
|
10 |
+
</div>
|
11 |
+
);
|
12 |
}
|
components/login-button.tsx
CHANGED
@@ -1,42 +1,42 @@
|
|
1 |
-
'use client'
|
2 |
|
3 |
-
import * as React from 'react'
|
4 |
-
import { signIn } from 'next-auth/react'
|
5 |
|
6 |
-
import { cn } from '@/lib/utils'
|
7 |
-
import { Button, type ButtonProps } from '@/components/ui/button'
|
8 |
-
import { IconGitHub, IconSpinner } from '@/components/ui/icons'
|
9 |
|
10 |
interface LoginButtonProps extends ButtonProps {
|
11 |
-
|
12 |
-
|
13 |
}
|
14 |
|
15 |
export function LoginButton({
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
}: LoginButtonProps) {
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
}
|
|
|
1 |
+
'use client';
|
2 |
|
3 |
+
import * as React from 'react';
|
4 |
+
import { signIn } from 'next-auth/react';
|
5 |
|
6 |
+
import { cn } from '@/lib/utils';
|
7 |
+
import { Button, type ButtonProps } from '@/components/ui/button';
|
8 |
+
import { IconGitHub, IconSpinner } from '@/components/ui/icons';
|
9 |
|
10 |
interface LoginButtonProps extends ButtonProps {
|
11 |
+
showGithubIcon?: boolean;
|
12 |
+
text?: string;
|
13 |
}
|
14 |
|
15 |
export function LoginButton({
|
16 |
+
text = 'Login with GitHub',
|
17 |
+
showGithubIcon = true,
|
18 |
+
className,
|
19 |
+
...props
|
20 |
}: LoginButtonProps) {
|
21 |
+
const [isLoading, setIsLoading] = React.useState(false);
|
22 |
+
return (
|
23 |
+
<Button
|
24 |
+
variant="outline"
|
25 |
+
onClick={() => {
|
26 |
+
setIsLoading(true);
|
27 |
+
// next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
|
28 |
+
signIn('github', { callbackUrl: `/` });
|
29 |
+
}}
|
30 |
+
disabled={isLoading}
|
31 |
+
className={cn(className)}
|
32 |
+
{...props}
|
33 |
+
>
|
34 |
+
{isLoading ? (
|
35 |
+
<IconSpinner className="mr-2 animate-spin" />
|
36 |
+
) : showGithubIcon ? (
|
37 |
+
<IconGitHub className="mr-2" />
|
38 |
+
) : null}
|
39 |
+
{text}
|
40 |
+
</Button>
|
41 |
+
);
|
42 |
}
|
next.config.js
CHANGED
@@ -1,13 +1,11 @@
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
module.exports = {
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
}
|
13 |
-
}
|
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
module.exports = {
|
3 |
+
images: {
|
4 |
+
remotePatterns: [
|
5 |
+
{
|
6 |
+
protocol: 'https',
|
7 |
+
hostname: '**',
|
8 |
+
},
|
9 |
+
],
|
10 |
+
},
|
11 |
+
};
|
|
|
|
package.json
CHANGED
@@ -30,6 +30,7 @@
|
|
30 |
"focus-trap-react": "^10.2.3",
|
31 |
"framer-motion": "^10.18.0",
|
32 |
"geist": "^1.2.1",
|
|
|
33 |
"nanoid": "^5.0.4",
|
34 |
"next": "14.1.0",
|
35 |
"next-auth": "5.0.0-beta.4",
|
|
|
30 |
"focus-trap-react": "^10.2.3",
|
31 |
"framer-motion": "^10.18.0",
|
32 |
"geist": "^1.2.1",
|
33 |
+
"jotai": "^2.7.0",
|
34 |
"nanoid": "^5.0.4",
|
35 |
"next": "14.1.0",
|
36 |
"next-auth": "5.0.0-beta.4",
|
prettier.config.cjs
CHANGED
@@ -1,12 +1,13 @@
|
|
1 |
/** @type {import('prettier').Config} */
|
2 |
module.exports = {
|
3 |
endOfLine: 'lf',
|
4 |
-
semi:
|
5 |
-
useTabs:
|
6 |
singleQuote: true,
|
7 |
arrowParens: 'avoid',
|
8 |
tabWidth: 2,
|
9 |
-
trailingComma: '
|
|
|
10 |
importOrder: [
|
11 |
'^(react/(.*)$)|^(react$)',
|
12 |
'^(next/(.*)$)|^(next$)',
|
@@ -23,12 +24,12 @@ module.exports = {
|
|
23 |
'^@/styles/(.*)$',
|
24 |
'^@/app/(.*)$',
|
25 |
'',
|
26 |
-
'^[./]'
|
27 |
],
|
28 |
importOrderSeparation: false,
|
29 |
importOrderSortSpecifiers: true,
|
30 |
importOrderBuiltinModulesToTop: true,
|
31 |
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
|
32 |
importOrderMergeDuplicateImports: true,
|
33 |
-
importOrderCombineTypeAndValueImports: true
|
34 |
}
|
|
|
1 |
/** @type {import('prettier').Config} */
|
2 |
module.exports = {
|
3 |
endOfLine: 'lf',
|
4 |
+
semi: true,
|
5 |
+
useTabs: true,
|
6 |
singleQuote: true,
|
7 |
arrowParens: 'avoid',
|
8 |
tabWidth: 2,
|
9 |
+
trailingComma: 'all',
|
10 |
+
bracketSpacing: true,
|
11 |
importOrder: [
|
12 |
'^(react/(.*)$)|^(react$)',
|
13 |
'^(next/(.*)$)|^(next$)',
|
|
|
24 |
'^@/styles/(.*)$',
|
25 |
'^@/app/(.*)$',
|
26 |
'',
|
27 |
+
'^[./]',
|
28 |
],
|
29 |
importOrderSeparation: false,
|
30 |
importOrderSortSpecifiers: true,
|
31 |
importOrderBuiltinModulesToTop: true,
|
32 |
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
|
33 |
importOrderMergeDuplicateImports: true,
|
34 |
+
importOrderCombineTypeAndValueImports: true,
|
35 |
}
|
state/index.ts
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { atom } from 'jotai';
|
2 |
+
|
3 |
+
export const targetImageAtom = atom<string | null>(
|
4 |
+
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/9908.jpg',
|
5 |
+
);
|
yarn.lock
CHANGED
@@ -2550,6 +2550,11 @@ jose@^5.1.0:
|
|
2550 |
resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0"
|
2551 |
integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==
|
2552 |
|
|
|
|
|
|
|
|
|
|
|
2553 |
"js-tokens@^3.0.0 || ^4.0.0":
|
2554 |
version "4.0.0"
|
2555 |
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
@@ -4065,6 +4070,7 @@ streamsearch@^1.1.0:
|
|
4065 |
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
4066 |
|
4067 |
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
|
|
4068 |
version "4.2.3"
|
4069 |
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
4070 |
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
|
2550 |
resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0"
|
2551 |
integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==
|
2552 |
|
2553 |
+
jotai@^2.7.0:
|
2554 |
+
version "2.7.0"
|
2555 |
+
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.7.0.tgz#50efe98b94ec742e1c4cf3f4307c2cac4766392c"
|
2556 |
+
integrity sha512-4qsyFKu4MprI39rj2uoItyhu24NoCHzkOV7z70PQr65SpzV6CSyhQvVIfbNlNqOIOspNMdf5OK+kTXLvqe63Jw==
|
2557 |
+
|
2558 |
"js-tokens@^3.0.0 || ^4.0.0":
|
2559 |
version "4.0.0"
|
2560 |
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
|
|
4070 |
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
4071 |
|
4072 |
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
4073 |
+
name string-width-cjs
|
4074 |
version "4.2.3"
|
4075 |
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
4076 |
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|