MingruiZhang commited on
Commit
f3a9ef2
β€’
1 Parent(s): 1d92adf

multi image

Browse files
app/(chat)/chat/[id]/page.tsx CHANGED
@@ -1,47 +1,47 @@
1
- import { type Metadata } from 'next'
2
- import { notFound, redirect } from 'next/navigation'
3
 
4
- import { auth } from '@/auth'
5
- import { getChat } from '@/app/actions'
6
- import { Chat } from '@/components/chat'
7
 
8
  export interface ChatPageProps {
9
- params: {
10
- id: string
11
- }
12
  }
13
 
14
  export async function generateMetadata({
15
- params
16
  }: ChatPageProps): Promise<Metadata> {
17
- const session = await auth()
18
 
19
- if (!session?.user) {
20
- return {}
21
- }
22
 
23
- const chat = await getChat(params.id, session.user.id)
24
- return {
25
- title: chat?.title.toString().slice(0, 50) ?? 'Chat'
26
- }
27
  }
28
 
29
  export default async function ChatPage({ params }: ChatPageProps) {
30
- const session = await auth()
31
 
32
- if (!session?.user) {
33
- redirect(`/sign-in?next=/chat/${params.id}`)
34
- }
35
 
36
- const chat = await getChat(params.id, session.user.id)
37
 
38
- if (!chat) {
39
- notFound()
40
- }
41
 
42
- if (chat?.userId !== session?.user?.id) {
43
- notFound()
44
- }
45
 
46
- return <Chat id={chat.id} initialMessages={chat.messages} />
47
  }
 
1
+ import { type Metadata } from 'next';
2
+ import { notFound, redirect } from 'next/navigation';
3
 
4
+ import { auth } from '@/auth';
5
+ import { getChat } from '@/app/actions';
6
+ import { Chat } from '@/components/chat';
7
 
8
  export interface ChatPageProps {
9
+ params: {
10
+ id: string;
11
+ };
12
  }
13
 
14
  export async function generateMetadata({
15
+ params,
16
  }: ChatPageProps): Promise<Metadata> {
17
+ const session = await auth();
18
 
19
+ if (!session?.user) {
20
+ return {};
21
+ }
22
 
23
+ const chat = await getChat(params.id, session.user.id);
24
+ return {
25
+ title: chat?.title.toString().slice(0, 50) ?? 'Chat',
26
+ };
27
  }
28
 
29
  export default async function ChatPage({ params }: ChatPageProps) {
30
+ const session = await auth();
31
 
32
+ if (!session?.user) {
33
+ redirect(`/sign-in?next=/chat/${params.id}`);
34
+ }
35
 
36
+ const chat = await getChat(params.id, session.user.id);
37
 
38
+ if (!chat) {
39
+ notFound();
40
+ }
41
 
42
+ if (chat?.userId !== session?.user?.id) {
43
+ notFound();
44
+ }
45
 
46
+ return <Chat id={chat.id} initialMessages={chat.messages} />;
47
  }
app/(chat)/page.tsx CHANGED
@@ -4,14 +4,14 @@ import { nanoid } from '@/lib/utils';
4
  import { Chat } from '@/components/chat';
5
  import { ThemeToggle } from '../../components/theme-toggle';
6
  import { useAtomValue } from 'jotai';
7
- import { targetImageAtom } from '../../state';
8
  import { EmptyScreen } from '../../components/empty-screen';
9
 
10
  export default function IndexPage() {
11
  const id = nanoid();
12
- const targetImage = useAtomValue(targetImageAtom);
13
 
14
- if (!targetImage)
15
  return (
16
  <div className="pb-[150px] pt-4 md:pt-10 h-full">
17
  <EmptyScreen />
@@ -20,16 +20,7 @@ export default function IndexPage() {
20
 
21
  return (
22
  <>
23
- <Chat
24
- id={id}
25
- initialMessages={[
26
- {
27
- id: '123',
28
- content: 'Hi, what do you want to know about this image?',
29
- role: 'system',
30
- },
31
- ]}
32
- />
33
  <ThemeToggle />
34
  </>
35
  );
 
4
  import { Chat } from '@/components/chat';
5
  import { ThemeToggle } from '../../components/theme-toggle';
6
  import { useAtomValue } from 'jotai';
7
+ import { datasetAtom } from '../../state';
8
  import { EmptyScreen } from '../../components/empty-screen';
9
 
10
  export default function IndexPage() {
11
  const id = nanoid();
12
+ const dataset = useAtomValue(datasetAtom);
13
 
14
+ if (!dataset.length)
15
  return (
16
  <div className="pb-[150px] pt-4 md:pt-10 h-full">
17
  <EmptyScreen />
 
20
 
21
  return (
22
  <>
23
+ <Chat id={id} />
 
 
 
 
 
 
 
 
 
24
  <ThemeToggle />
25
  </>
26
  );
app/api/chat/route.ts CHANGED
@@ -5,6 +5,7 @@ import { auth } from '@/auth';
5
  import { nanoid } from '@/lib/utils';
6
  import {
7
  ChatCompletionContentPart,
 
8
  ChatCompletionMessageParam,
9
  } from 'openai/resources';
10
 
@@ -16,9 +17,9 @@ const openai = new OpenAI({
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
 
24
  const userId = (await auth())?.user.id;
@@ -37,10 +38,13 @@ export async function POST(req: Request) {
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',
 
5
  import { nanoid } from '@/lib/utils';
6
  import {
7
  ChatCompletionContentPart,
8
+ ChatCompletionContentPartImage,
9
  ChatCompletionMessageParam,
10
  } from 'openai/resources';
11
 
 
17
 
18
  export async function POST(req: Request) {
19
  const json = await req.json();
20
+ const { messages, dataset } = json as {
21
  messages: ChatCompletionMessageParam[];
22
+ dataset: string[];
23
  };
24
 
25
  const userId = (await auth())?.user.id;
 
38
  type: 'text',
39
  text: message.content as string,
40
  },
41
+ ...dataset.map(
42
+ image =>
43
+ ({
44
+ type: 'image_url',
45
+ image_url: { url: image },
46
+ }) satisfies ChatCompletionContentPartImage,
47
+ ),
48
  ];
49
  return {
50
  role: 'user',
app/share/[id]/page.tsx CHANGED
@@ -3,7 +3,7 @@ import { notFound } from 'next/navigation';
3
 
4
  import { formatDate } from '@/lib/utils';
5
  import { getSharedChat } from '@/app/actions';
6
- import { ChatList } from '@/components/chat-list';
7
 
8
  interface SharePageProps {
9
  params: {
 
3
 
4
  import { formatDate } from '@/lib/utils';
5
  import { getSharedChat } from '@/app/actions';
6
+ import { ChatList } from '@/components/chat/ChatList';
7
 
8
  interface SharePageProps {
9
  params: {
components/{chat-list.tsx β†’ chat/ChatList.tsx} RENAMED
File without changes
components/chat/ImageList.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import useImageUpload from '../../lib/hooks/useImageUpload';
3
+ import { useAtomValue } from 'jotai';
4
+ import { datasetAtom } from '../../state';
5
+ import Image from 'next/image';
6
+
7
+ export interface ImageListProps {}
8
+
9
+ const ImageList: React.FC<ImageListProps> = () => {
10
+ const { getRootProps, getInputProps, isDragActive } = useImageUpload();
11
+ const dataset = useAtomValue(datasetAtom);
12
+ return (
13
+ <div className="relative aspect-[1/1] size-full px-12">
14
+ {dataset.length < 10 ? (
15
+ <div className="col-span-full px-8 py-4 rounded-xl bg-blue-100 text-blue-400 mb-8">
16
+ You can upload up to 10 images max.
17
+ </div>
18
+ ) : (
19
+ <div className="col-span-full px-8 py-4 rounded-xl bg-red-100 text-red-400 mb-8">
20
+ You have reached the maximum limit of 10 images.
21
+ </div>
22
+ )}
23
+ <div
24
+ {...getRootProps()}
25
+ className="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-4"
26
+ >
27
+ {dataset.map((imageSrc, index) => {
28
+ return (
29
+ <Image
30
+ src={imageSrc}
31
+ key={index}
32
+ alt="dataset images"
33
+ width={500}
34
+ height={500}
35
+ objectFit="cover"
36
+ className="relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105"
37
+ />
38
+ );
39
+ })}
40
+ </div>
41
+
42
+ {isDragActive && (
43
+ <div
44
+ {...getRootProps()}
45
+ className="dropzone border-2 border-dashed border-gray-400 size-full absolute top-0 left-0 flex items-center justify-center rounded-lg cursor-pointer bg-gray-500/50"
46
+ >
47
+ <input {...getInputProps()} />
48
+ <p className="text-white">Drop the files here ...</p>
49
+ </div>
50
+ )}
51
+ </div>
52
+ );
53
+ };
54
+
55
+ export default ImageList;
components/{chat.tsx β†’ chat/index.tsx} RENAMED
@@ -1,22 +1,15 @@
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 { 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
- import { ThemeToggle } from './theme-toggle';
20
 
21
  export interface ChatProps extends React.ComponentProps<'div'> {
22
  initialMessages?: Message[];
@@ -24,14 +17,14 @@ export interface ChatProps extends React.ComponentProps<'div'> {
24
  }
25
 
26
  export function Chat({ id, initialMessages, className }: ChatProps) {
27
- const [targetImage, setTargetImage] = useAtom(targetImageAtom);
28
  const { messages, append, reload, stop, isLoading, input, setInput } =
29
  useChat({
30
  initialMessages,
31
  id,
32
  body: {
33
  id,
34
- image: targetImage,
35
  },
36
  onResponse(response) {
37
  if (response.status === 401) {
@@ -45,43 +38,13 @@ export function Chat({ id, initialMessages, className }: ChatProps) {
45
  <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
46
  <div className="flex h-full">
47
  <div className="w-1/2 relative border-r-2 border-gray-200">
48
- <div className="relative aspect-[1/1] w-full px-12">
49
- <div className="flex items-center h-[600px] relative">
50
- <Image
51
- src={targetImage!}
52
- alt="target image"
53
- layout="fill"
54
- objectFit="contain"
55
- className="rounded-xl bg-gray-200"
56
- />
57
- </div>
58
- <button
59
- className="px-2 py-1 rounded-lg text-gray-600 border-2 border-gray-600 flex items-center mt-4"
60
- onClick={() => setTargetImage(null)}
61
- >
62
- <svg
63
- xmlns="http://www.w3.org/2000/svg"
64
- fill="none"
65
- viewBox="0 0 24 24"
66
- stroke="currentColor"
67
- className="size-4"
68
- >
69
- <path
70
- strokeLinecap="round"
71
- strokeLinejoin="round"
72
- strokeWidth={2}
73
- d="M15 19l-7-7 7-7"
74
- />
75
- </svg>
76
- Back
77
- </button>
78
- </div>
79
  </div>
80
  <div className="w-1/2 relative overflow-auto">
81
  <ChatList messages={messages} isLoading={isLoading} />
 
82
  </div>
83
  </div>
84
- <ChatScrollAnchor trackVisibility={isLoading} />
85
  </div>
86
  <ChatPanel
87
  id={id}
 
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[];
 
17
  }
18
 
19
  export function Chat({ id, initialMessages, className }: ChatProps) {
20
+ const [dataset] = useAtom(datasetAtom);
21
  const { messages, append, reload, stop, isLoading, input, setInput } =
22
  useChat({
23
  initialMessages,
24
  id,
25
  body: {
26
  id,
27
+ dataset: dataset,
28
  },
29
  onResponse(response) {
30
  if (response.status === 401) {
 
38
  <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
39
  <div className="flex h-full">
40
  <div className="w-1/2 relative border-r-2 border-gray-200">
41
+ <ImageList />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  </div>
43
  <div className="w-1/2 relative overflow-auto">
44
  <ChatList messages={messages} isLoading={isLoading} />
45
+ <ChatScrollAnchor trackVisibility={isLoading} />
46
  </div>
47
  </div>
 
48
  </div>
49
  <ChatPanel
50
  id={id}
components/empty-screen.tsx CHANGED
@@ -1,7 +1,8 @@
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',
@@ -11,25 +12,8 @@ const examples = [
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">
@@ -56,7 +40,7 @@ export function EmptyScreen() {
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>
 
1
  import { useAtom } from 'jotai';
2
  import { useDropzone } from 'react-dropzone';
3
+ import { datasetAtom } from '../state';
4
  import Image from 'next/image';
5
+ import useImageUpload from '../lib/hooks/useImageUpload';
6
 
7
  const examples = [
8
  'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
 
12
  ];
13
 
14
  export function EmptyScreen() {
15
+ const [, setTarget] = useAtom(datasetAtom);
16
+ const { getRootProps, getInputProps } = useImageUpload();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  return (
18
  <div className="mx-auto max-w-2xl px-4">
19
  <div className="rounded-lg border bg-background p-8">
 
40
  height={120}
41
  alt="example images"
42
  className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
43
+ onClick={() => setTarget([example])}
44
  />
45
  ))}
46
  </div>
lib/hooks/useImageUpload.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useAtom } from 'jotai';
2
+ import { useDropzone } from 'react-dropzone';
3
+ import { datasetAtom } from '../../state';
4
+
5
+ const useImageUpload = () => {
6
+ const [, setTarget] = useAtom(datasetAtom);
7
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
8
+ accept: {
9
+ 'image/*': ['.jpeg', '.png'],
10
+ },
11
+ maxFiles: 10,
12
+ multiple: true,
13
+ onDrop: acceptedFiles => {
14
+ acceptedFiles.forEach(file => {
15
+ try {
16
+ const reader = new FileReader();
17
+ reader.onloadend = () => {
18
+ const newImage = reader.result as string;
19
+ setTarget(prev => {
20
+ // Check if the image already exists in the state
21
+ if (prev.length >= 10 || prev.includes(newImage)) {
22
+ // If it does, return the state unchanged
23
+ return prev;
24
+ } else {
25
+ // If it doesn't, add the new image to the state
26
+ return [...prev, newImage];
27
+ }
28
+ });
29
+ };
30
+ reader.readAsDataURL(file);
31
+ } catch (err) {
32
+ console.error(err);
33
+ }
34
+ });
35
+ },
36
+ });
37
+
38
+ return { getRootProps, getInputProps, isDragActive };
39
+ };
40
+
41
+ export default useImageUpload;
state/index.ts CHANGED
@@ -1,3 +1,4 @@
1
  import { atom } from 'jotai';
2
 
3
- export const targetImageAtom = atom<string | null>(null);
 
 
1
  import { atom } from 'jotai';
2
 
3
+ // list of image urls or base64 strings
4
+ export const datasetAtom = atom<string[]>([]);