MingruiZhang commited on
Commit
42501f7
1 Parent(s): 52b4c36

select image

Browse files
app/api/chat/route.ts CHANGED
@@ -20,7 +20,7 @@ export async function POST(req: Request) {
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) {
 
20
  messages: ChatCompletionMessageParam[];
21
  image: string;
22
  };
23
+
24
  const userId = (await auth())?.user.id;
25
 
26
  if (!userId) {
app/layout.tsx CHANGED
@@ -1,62 +1,62 @@
1
- import { Toaster } from 'react-hot-toast'
2
- import { GeistSans } from 'geist/font/sans'
3
- import { GeistMono } from 'geist/font/mono'
4
 
5
- import '@/app/globals.css'
6
- import { cn } from '@/lib/utils'
7
- import { TailwindIndicator } from '@/components/tailwind-indicator'
8
- import { Providers } from '@/components/providers'
9
- import { Header } from '@/components/header'
10
 
11
  export const metadata = {
12
- metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
13
- title: {
14
- default: 'Next.js AI Chatbot',
15
- template: `%s - Next.js AI Chatbot`
16
- },
17
- description: 'An AI-powered chatbot template built with Next.js and Vercel.',
18
- icons: {
19
- icon: '/favicon.ico',
20
- shortcut: '/favicon-16x16.png',
21
- apple: '/apple-touch-icon.png'
22
- }
23
- }
24
 
25
  export const viewport = {
26
- themeColor: [
27
- { media: '(prefers-color-scheme: light)', color: 'white' },
28
- { media: '(prefers-color-scheme: dark)', color: 'black' }
29
- ]
30
- }
31
 
32
  interface RootLayoutProps {
33
- children: React.ReactNode
34
  }
35
 
36
  export default function RootLayout({ children }: RootLayoutProps) {
37
- return (
38
- <html lang="en" suppressHydrationWarning>
39
- <body
40
- className={cn(
41
- 'font-sans antialiased',
42
- GeistSans.variable,
43
- GeistMono.variable
44
- )}
45
- >
46
- <Toaster />
47
- <Providers
48
- attribute="class"
49
- defaultTheme="system"
50
- enableSystem
51
- disableTransitionOnChange
52
- >
53
- <div className="flex flex-col min-h-screen">
54
- <Header />
55
- <main className="flex flex-col flex-1 bg-muted/50">{children}</main>
56
- </div>
57
- <TailwindIndicator />
58
- </Providers>
59
- </body>
60
- </html>
61
- )
62
  }
 
1
+ import { Toaster } from 'react-hot-toast';
2
+ import { GeistSans } from 'geist/font/sans';
3
+ import { GeistMono } from 'geist/font/mono';
4
 
5
+ import '@/app/globals.css';
6
+ import { cn } from '@/lib/utils';
7
+ import { TailwindIndicator } from '@/components/tailwind-indicator';
8
+ import { Providers } from '@/components/providers';
9
+ import { Header } from '@/components/header';
10
 
11
  export const metadata = {
12
+ metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
13
+ title: {
14
+ default: 'Vision Agent',
15
+ template: `%s - Vision Agent`,
16
+ },
17
+ description: 'By Landing AI',
18
+ icons: {
19
+ icon: '/landing.png',
20
+ shortcut: '/landing.png',
21
+ apple: '/landing.png',
22
+ },
23
+ };
24
 
25
  export const viewport = {
26
+ themeColor: [
27
+ { media: '(prefers-color-scheme: light)', color: 'white' },
28
+ { media: '(prefers-color-scheme: dark)', color: 'black' },
29
+ ],
30
+ };
31
 
32
  interface RootLayoutProps {
33
+ children: React.ReactNode;
34
  }
35
 
36
  export default function RootLayout({ children }: RootLayoutProps) {
37
+ return (
38
+ <html lang="en" suppressHydrationWarning>
39
+ <body
40
+ className={cn(
41
+ 'font-sans antialiased',
42
+ GeistSans.variable,
43
+ GeistMono.variable,
44
+ )}
45
+ >
46
+ <Toaster />
47
+ <Providers
48
+ attribute="class"
49
+ defaultTheme="system"
50
+ enableSystem
51
+ disableTransitionOnChange
52
+ >
53
+ <div className="flex flex-col min-h-screen">
54
+ <Header />
55
+ <main className="flex flex-col flex-1 bg-muted/50">{children}</main>
56
+ </div>
57
+ <TailwindIndicator />
58
+ </Providers>
59
+ </body>
60
+ </html>
61
+ );
62
  }
components/chat-list.tsx CHANGED
@@ -1,27 +1,69 @@
1
- import { type Message } from 'ai'
 
2
 
3
- import { Separator } from '@/components/ui/separator'
4
- import { ChatMessage } from '@/components/chat-message'
5
 
6
  export interface ChatList {
7
- messages: Message[]
 
8
  }
9
 
10
- export function ChatList({ messages }: ChatList) {
11
- if (!messages.length) {
12
- return null
13
- }
14
-
15
- return (
16
- <div className="relative mx-auto max-w-2xl px-4">
17
- {messages.map((message, index) => (
18
- <div key={index}>
19
- <ChatMessage message={message} />
20
- {index < messages.length - 1 && (
21
- <Separator className="my-4 md:my-8" />
22
- )}
23
- </div>
24
- ))}
25
- </div>
26
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
 
1
+ import { type Message } from 'ai';
2
+ import { useEffect, useState } from 'react';
3
 
4
+ import { Separator } from '@/components/ui/separator';
5
+ import { ChatMessage } from '@/components/chat-message';
6
 
7
  export interface ChatList {
8
+ messages: Message[];
9
+ isLoading: boolean;
10
  }
11
 
12
+ export function ChatList({ messages, isLoading }: ChatList) {
13
+ const [loadingDots, setLoadingDots] = useState('');
14
+
15
+ useEffect(() => {
16
+ let loadingInterval: NodeJS.Timeout;
17
+
18
+ if (isLoading) {
19
+ loadingInterval = setInterval(() => {
20
+ setLoadingDots(prevMessage => {
21
+ switch (prevMessage) {
22
+ case '':
23
+ return '.';
24
+ case '.':
25
+ return '..';
26
+ case '..':
27
+ return '...';
28
+ case '...':
29
+ return '';
30
+ default:
31
+ return '';
32
+ }
33
+ });
34
+ }, 500);
35
+ }
36
+
37
+ return () => {
38
+ clearInterval(loadingInterval);
39
+ };
40
+ }, [isLoading]);
41
+
42
+ if (!messages.length) {
43
+ return null;
44
+ }
45
+
46
+ const assistantLoadingMessage: Message = {
47
+ id: 'loading',
48
+ content: loadingDots,
49
+ role: 'assistant',
50
+ };
51
+
52
+ const messageWithLoading =
53
+ isLoading && messages[messages.length - 1].role !== 'assistant'
54
+ ? [...messages, assistantLoadingMessage]
55
+ : messages;
56
+
57
+ return (
58
+ <div className="relative mx-auto max-w-2xl px-8 pr-12">
59
+ {messageWithLoading.map((message, index) => (
60
+ <div key={index}>
61
+ <ChatMessage message={message} />
62
+ {index < messageWithLoading.length - 1 && (
63
+ <Separator className="my-4 md:my-8" />
64
+ )}
65
+ </div>
66
+ ))}
67
+ </div>
68
+ );
69
  }
components/chat.tsx CHANGED
@@ -8,21 +8,12 @@ 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
 
@@ -34,7 +25,7 @@ export interface ChatProps extends React.ComponentProps<'div'> {
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,
@@ -49,34 +40,55 @@ export function Chat({ id, initialMessages, className }: ChatProps) {
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}
 
8
  import { ChatPanel } from '@/components/chat-panel';
9
  import { EmptyScreen } from '@/components/empty-screen';
10
  import { ChatScrollAnchor } from '@/components/chat-scroll-anchor';
 
 
 
 
 
 
 
 
 
11
  import { useState } from 'react';
12
  import { Button } from './ui/button';
13
  import { Input } from './ui/input';
14
  import { toast } from 'react-hot-toast';
15
  import { usePathname, useRouter } from 'next/navigation';
16
+ import { useAtom } from 'jotai';
17
  import { targetImageAtom } from '@/state';
18
  import Image from 'next/image';
19
 
 
25
  export function Chat({ id, initialMessages, className }: ChatProps) {
26
  const router = useRouter();
27
  const path = usePathname();
28
+ const [targetImage, setTargetImage] = useAtom(targetImageAtom);
29
  const { messages, append, reload, stop, isLoading, input, setInput } =
30
  useChat({
31
  initialMessages,
 
40
  }
41
  },
42
  });
43
+
44
+ if (!targetImage)
45
+ return (
46
+ <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
47
+ <EmptyScreen />/
48
+ </div>
49
+ );
50
  return (
51
  <>
52
  <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
53
+ <div className="flex h-full">
54
+ <div className="w-1/2 relative border-r-2 border-gray-200">
55
+ <div className="relative aspect-[1/1] w-full px-12">
56
+ <Image
57
+ src={targetImage}
58
+ alt="target image"
59
+ layout="responsive"
60
+ objectFit="contain"
61
+ width={1000}
62
+ height={1000}
63
+ className="rounded-xl shadow-lg"
64
+ />
65
+ <button
66
+ className="px-2 py-1 rounded-lg text-blue-500 border-2 border-blue-400 flex items-center mt-4"
67
+ onClick={() => setTargetImage(null)}
68
+ >
69
+ <svg
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ fill="none"
72
+ viewBox="0 0 24 24"
73
+ stroke="currentColor"
74
+ className="size-4"
75
+ >
76
+ <path
77
+ strokeLinecap="round"
78
+ strokeLinejoin="round"
79
+ strokeWidth={2}
80
+ d="M15 19l-7-7 7-7"
81
  />
82
+ </svg>
83
+ Back
84
+ </button>
 
 
85
  </div>
86
+ </div>
87
+ <div className="w-1/2 relative overflow-auto">
88
+ <ChatList messages={messages} isLoading={isLoading} />
89
+ </div>
90
+ </div>
91
+ <ChatScrollAnchor trackVisibility={isLoading} />
92
  </div>
93
  <ChatPanel
94
  id={id}
components/empty-screen.tsx CHANGED
@@ -1,11 +1,65 @@
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
  );
 
1
+ import { useAtom } from 'jotai';
2
+ import { useDropzone } from 'react-dropzone';
3
+ import { targetImageAtom } from '../state';
4
+ import Image from 'next/image';
5
 
6
+ const examples = [
7
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
8
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
9
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
10
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
11
+ ];
12
+
13
+ export function EmptyScreen() {
14
+ const [, setTarget] = useAtom(targetImageAtom);
15
+ const { getRootProps, getInputProps } = useDropzone({
16
+ accept: {
17
+ 'image/*': ['.jpeg', '.png'],
18
+ },
19
+ multiple: false,
20
+ onDrop: async acceptedFiles => {
21
+ try {
22
+ const file = acceptedFiles[0];
23
+ const reader = new FileReader();
24
+ reader.onloadend = () => {
25
+ setTarget(reader.result as string);
26
+ };
27
+ reader.readAsDataURL(file);
28
+ } catch (err) {
29
+ console.error(err);
30
+ }
31
+ },
32
+ });
33
  return (
34
  <div className="mx-auto max-w-2xl px-4">
35
  <div className="rounded-lg border bg-background p-8">
36
  <h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
37
+ <p>Lets start by choosing an image</p>
38
+ <div
39
+ {...getRootProps()}
40
+ className="dropzone border-2 border-dashed border-blue-300 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer"
41
+ >
42
+ <input {...getInputProps()} />
43
+ <p className="text-blue-300 text-lg">
44
+ Drag or drop image here, or click to select images
45
+ </p>
46
+ </div>
47
+ <p className="mt-4 mb-2">
48
+ You can also choose from below examples we provided
49
+ </p>
50
+ <div className="flex">
51
+ {examples.map((example, index) => (
52
+ <Image
53
+ src={example}
54
+ key={index}
55
+ width={120}
56
+ height={120}
57
+ alt="example images"
58
+ className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
59
+ onClick={() => setTarget(example)}
60
+ />
61
+ ))}
62
+ </div>
63
  </div>
64
  </div>
65
  );
package.json CHANGED
@@ -38,6 +38,7 @@
38
  "openai": "^4.24.7",
39
  "react": "^18.2.0",
40
  "react-dom": "^18.2.0",
 
41
  "react-hot-toast": "^2.4.1",
42
  "react-intersection-observer": "^9.5.3",
43
  "react-markdown": "^8.0.7",
 
38
  "openai": "^4.24.7",
39
  "react": "^18.2.0",
40
  "react-dom": "^18.2.0",
41
+ "react-dropzone": "^14.2.3",
42
  "react-hot-toast": "^2.4.1",
43
  "react-intersection-observer": "^9.5.3",
44
  "react-markdown": "^8.0.7",
public/apple-touch-icon.png DELETED
Binary file (10.4 kB)
 
public/favicon-16x16.png DELETED
Binary file (539 Bytes)
 
public/favicon.ico DELETED
Binary file (15.4 kB)
 
public/landing.png ADDED
state/index.ts CHANGED
@@ -1,5 +1,3 @@
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
- );
 
1
  import { atom } from 'jotai';
2
 
3
+ export const targetImageAtom = atom<string | null>(null);
 
 
yarn.lock CHANGED
@@ -1054,6 +1054,11 @@ asynckit@^0.4.0:
1054
  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
1055
  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
1056
 
 
 
 
 
 
1057
  autoprefixer@^10.4.17:
1058
  version "10.4.17"
1059
  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.17.tgz#35cd5695cbbe82f536a50fa025d561b01fdec8be"
@@ -1914,6 +1919,13 @@ file-entry-cache@^6.0.1:
1914
  dependencies:
1915
  flat-cache "^3.0.4"
1916
 
 
 
 
 
 
 
 
1917
  fill-range@^7.0.1:
1918
  version "7.0.1"
1919
  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -3682,6 +3694,15 @@ react-dom@^18.2.0:
3682
  loose-envify "^1.1.0"
3683
  scheduler "^0.23.0"
3684
 
 
 
 
 
 
 
 
 
 
3685
  react-hot-toast@^2.4.1:
3686
  version "2.4.1"
3687
  resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994"
 
1054
  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
1055
  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
1056
 
1057
+ attr-accept@^2.2.2:
1058
+ version "2.2.2"
1059
+ resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
1060
+ integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
1061
+
1062
  autoprefixer@^10.4.17:
1063
  version "10.4.17"
1064
  resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.17.tgz#35cd5695cbbe82f536a50fa025d561b01fdec8be"
 
1919
  dependencies:
1920
  flat-cache "^3.0.4"
1921
 
1922
+ file-selector@^0.6.0:
1923
+ version "0.6.0"
1924
+ resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
1925
+ integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
1926
+ dependencies:
1927
+ tslib "^2.4.0"
1928
+
1929
  fill-range@^7.0.1:
1930
  version "7.0.1"
1931
  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
 
3694
  loose-envify "^1.1.0"
3695
  scheduler "^0.23.0"
3696
 
3697
+ react-dropzone@^14.2.3:
3698
+ version "14.2.3"
3699
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b"
3700
+ integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==
3701
+ dependencies:
3702
+ attr-accept "^2.2.2"
3703
+ file-selector "^0.6.0"
3704
+ prop-types "^15.8.1"
3705
+
3706
  react-hot-toast@^2.4.1:
3707
  version "2.4.1"
3708
  resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994"