MingruiZhang commited on
Commit
52b4c36
1 Parent(s): 9eec735

chatgpt-4v

Browse files
.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
- const id = nanoid()
6
 
7
- return <Chat id={id} />
 
 
 
 
 
 
 
 
 
 
 
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
- apiKey: process.env.OPENAI_API_KEY
11
- })
12
 
13
  export async function POST(req: Request) {
14
- const json = await req.json()
15
- const { messages, previewToken } = json
16
- const userId = (await auth())?.user.id
17
-
18
- if (!userId) {
19
- return new Response('Unauthorized', {
20
- status: 401
21
- })
22
- }
23
-
24
- if (previewToken) {
25
- openai.apiKey = previewToken
26
- }
27
-
28
- const res = await openai.chat.completions.create({
29
- model: 'gpt-3.5-turbo',
30
- messages,
31
- temperature: 0.7,
32
- stream: true
33
- })
34
-
35
- const stream = OpenAIStream(res, {
36
- async onCompletion(completion) {
37
- const title = json.messages[0].content.substring(0, 100)
38
- const id = json.id ?? nanoid()
39
- const createdAt = Date.now()
40
- const payload = {
41
- id,
42
- title,
43
- userId,
44
- createdAt,
45
- messages: [
46
- ...messages,
47
- {
48
- content: completion,
49
- role: 'assistant'
50
- }
51
- ]
52
- }
53
- }
54
- })
55
-
56
- return new StreamingTextResponse(stream)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- :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
  }
 
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
- message: Message
16
  }
17
 
18
  export function ChatMessage({ message, ...props }: ChatMessageProps) {
19
- return (
20
- <div
21
- className={cn('group relative mb-4 flex items-start md:-ml-12')}
22
- {...props}
23
- >
24
- <div
25
- className={cn(
26
- 'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
27
- message.role === 'user'
28
- ? 'bg-background'
29
- : 'bg-primary text-primary-foreground'
30
- )}
31
- >
32
- {message.role === 'user' ? <IconUser /> : <IconOpenAI />}
33
- </div>
34
- <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
35
- <MemoizedReactMarkdown
36
- className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0"
37
- remarkPlugins={[remarkGfm, remarkMath]}
38
- components={{
39
- p({ children }) {
40
- return <p className="mb-2 last:mb-0">{children}</p>
41
- },
42
- code({ node, inline, className, children, ...props }) {
43
- if (children.length) {
44
- if (children[0] == '▍') {
45
- return (
46
- <span className="mt-1 cursor-default animate-pulse">▍</span>
47
- )
48
- }
49
 
50
- children[0] = (children[0] as string).replace('`▍`', '▍')
51
- }
52
 
53
- const match = /language-(\w+)/.exec(className || '')
54
 
55
- if (inline) {
56
- return (
57
- <code className={className} {...props}>
58
- {children}
59
- </code>
60
- )
61
- }
62
 
63
- return (
64
- <CodeBlock
65
- key={Math.random()}
66
- language={(match && match[1]) || ''}
67
- value={String(children).replace(/\n$/, '')}
68
- {...props}
69
- />
70
- )
71
- }
72
- }}
73
- >
74
- {message.content}
75
- </MemoizedReactMarkdown>
76
- <ChatMessageActions message={message} />
77
- </div>
78
- </div>
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
- Dialog,
13
- DialogContent,
14
- DialogDescription,
15
- DialogFooter,
16
- DialogHeader,
17
- DialogTitle
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
- initialMessages?: Message[]
27
- id?: string
28
  }
29
 
30
  export function Chat({ id, initialMessages, className }: ChatProps) {
31
- const router = useRouter()
32
- const path = usePathname()
33
- const [previewToken, setPreviewToken] = useLocalStorage<string | null>(
34
- 'ai-token',
35
- null
36
- )
37
- const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '')
38
- const { messages, append, reload, stop, isLoading, input, setInput } =
39
- useChat({
40
- initialMessages,
41
- id,
42
- body: {
43
- id,
44
- previewToken
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-[200px] pt-4 md:pt-10', className)}>
55
- {messages.length ? (
56
- <>
57
- <ChatList messages={messages} />
58
- <ChatScrollAnchor trackVisibility={isLoading} />
59
- </>
60
- ) : (
61
- <EmptyScreen setInput={setInput} />
62
- )}
63
- </div>
64
- <ChatPanel
65
- id={id}
66
- isLoading={isLoading}
67
- stop={stop}
68
- append={append}
69
- reload={reload}
70
- messages={messages}
71
- input={input}
72
- setInput={setInput}
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
- return (
24
- <div className="mx-auto max-w-2xl px-4">
25
- <div className="rounded-lg border bg-background p-8">
26
- <h1 className="md-2 text-lg font-semibold">Welcome to Vision Agent</h1>
27
- <p>Start by uploading an image</p>
28
- </div>
29
- </div>
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
- 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
  }
 
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
- images: {
4
- remotePatterns: [
5
- {
6
- protocol: 'https',
7
- hostname: 'avatars.githubusercontent.com',
8
- port: '',
9
- pathname: '**'
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: false,
5
- useTabs: false,
6
  singleQuote: true,
7
  arrowParens: 'avoid',
8
  tabWidth: 2,
9
- trailingComma: 'none',
 
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==