MingruiZhang commited on
Commit
d0a1b70
1 Parent(s): f80b091

Fix deployment + Chat id pages (#5)

Browse files

* Fix deployment

* use Response

* done

* done

* pretty

.prettierrc ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "endOfLine": "lf",
3
+ "semi": true,
4
+ "singleQuote": true,
5
+ "arrowParens": "avoid",
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "trailingComma": "all",
9
+ "bracketSpacing": true,
10
+ "importOrder": [
11
+ "^(react/(.*)$)|^(react$)",
12
+ "^(next/(.*)$)|^(next$)",
13
+ "<THIRD_PARTY_MODULES>",
14
+ "",
15
+ "^types$",
16
+ "^@/types/(.*)$",
17
+ "^@/config/(.*)$",
18
+ "^@/lib/(.*)$",
19
+ "^@/hooks/(.*)$",
20
+ "^@/components/ui/(.*)$",
21
+ "^@/components/(.*)$",
22
+ "^@/registry/(.*)$",
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
+ }
app/api/upload/route.ts CHANGED
@@ -1,11 +1,15 @@
1
  import { auth } from '@/auth';
 
2
  import { nanoid } from '@/lib/utils';
3
  import { kv } from '@vercel/kv';
4
- import { format } from 'date-fns';
5
 
6
- export async function POST(req: Request) {
 
 
 
 
 
7
  const session = await auth();
8
- console.log('[Ming] ~ POST ~ session:', session);
9
  const email = session?.user?.email;
10
  if (!email) {
11
  return new Response('Unauthorized', {
@@ -13,12 +17,23 @@ export async function POST(req: Request) {
13
  });
14
  }
15
 
16
- const json = await req.json();
17
- console.log('[Ming] ~ POST ~ json:', json);
 
 
 
 
18
 
19
  const id = nanoid();
20
 
21
- await kv.hmset(`chat:${id}`, json);
 
 
 
 
 
 
 
22
  await kv.zadd(`user:chat:${email}`, {
23
  score: Date.now(),
24
  member: `chat:${id}`,
@@ -28,5 +43,5 @@ export async function POST(req: Request) {
28
  member: `chat:${id}`,
29
  });
30
 
31
- return 'success';
32
  }
 
1
  import { auth } from '@/auth';
2
+ import { ChatEntity, MessageBase } from '@/lib/types';
3
  import { nanoid } from '@/lib/utils';
4
  import { kv } from '@vercel/kv';
 
5
 
6
+ /**
7
+ * TODO: this should be replaced with actual upload to S3
8
+ * @param req
9
+ * @returns
10
+ */
11
+ export async function POST(req: Request): Promise<Response> {
12
  const session = await auth();
 
13
  const email = session?.user?.email;
14
  if (!email) {
15
  return new Response('Unauthorized', {
 
17
  });
18
  }
19
 
20
+ const { url, initMessages } = (await req.json()) as {
21
+ url?: string;
22
+ file?: File;
23
+ base64?: string;
24
+ initMessages?: MessageBase[];
25
+ };
26
 
27
  const id = nanoid();
28
 
29
+ const payload: ChatEntity = {
30
+ url: url!, // TODO can be uploaded as well
31
+ id,
32
+ user: email,
33
+ messages: initMessages ?? [],
34
+ };
35
+
36
+ await kv.hmset(`chat:${id}`, payload);
37
  await kv.zadd(`user:chat:${email}`, {
38
  score: Date.now(),
39
  member: `chat:${id}`,
 
43
  member: `chat:${id}`,
44
  });
45
 
46
+ return Response.json(payload);
47
  }
app/chat/[id]/page.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { nanoid } from '@/lib/utils';
2
+ import { Chat } from '@/components/chat';
3
+
4
+ export default async function Page() {
5
+ const id = nanoid();
6
+
7
+ return <Chat id={id} />;
8
+ }
app/chat/layout.tsx CHANGED
@@ -11,14 +11,14 @@ export default async function Layout({ children }: ChatLayoutProps) {
11
  <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
12
  <div
13
  data-state="open"
14
- className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out translate-x-0 lg:flex lg:w-[250px] xl:w-[300px] h-full flex-col dark:bg-zinc-950 overflow-auto py-2"
15
  >
16
  <Suspense fallback={<Loading />}>
17
  <ChatSidebarList />
18
  </Suspense>
19
  </div>
20
  <Suspense fallback={<Loading />}>
21
- <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
22
  {children}
23
  </div>
24
  </Suspense>
 
11
  <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
12
  <div
13
  data-state="open"
14
+ className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out translate-x-0 lg:flex lg:w-[250px] h-full flex-col dark:bg-zinc-950 overflow-auto py-2"
15
  >
16
  <Suspense fallback={<Loading />}>
17
  <ChatSidebarList />
18
  </Suspense>
19
  </div>
20
  <Suspense fallback={<Loading />}>
21
+ <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px]">
22
  {children}
23
  </div>
24
  </Suspense>
app/chat/page.tsx CHANGED
@@ -1,7 +1,5 @@
1
- import { nanoid } from '@/lib/utils';
2
- import { Chat } from '@/components/chat';
3
 
4
  export default function Page() {
5
- const id = nanoid();
6
- return <Chat id={id} />;
7
  }
 
1
+ import ImageSelector from '@/components/chat/ImageSelector';
 
2
 
3
  export default function Page() {
4
+ return <ImageSelector />;
 
5
  }
components/chat-sidebar/ChatCard.tsx CHANGED
@@ -3,24 +3,35 @@
3
  import Link from 'next/link';
4
  import { useParams } from 'next/navigation';
5
  import { cn } from '@/lib/utils';
 
 
6
 
7
  export interface ChatCardProps {
8
- id: string;
9
- title: string;
10
  }
11
 
12
- const ChatCard: React.FC<ChatCardProps> = ({ id, title }) => {
13
  const { chatId: chatIdFromParam } = useParams();
 
14
  return (
15
  <Link
16
  className={cn(
17
- 'p-4 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
18
  chatIdFromParam === id && 'border-gray-500',
19
  )}
20
  href={`/chat/${id}`}
21
  >
22
- <div className="overflow-hidden">
23
- <p className="text-sm font-medium text-black mb-1">{title}</p>
 
 
 
 
 
 
 
 
 
24
  </div>
25
  </Link>
26
  );
 
3
  import Link from 'next/link';
4
  import { useParams } from 'next/navigation';
5
  import { cn } from '@/lib/utils';
6
+ import { ChatEntity } from '@/lib/types';
7
+ import Image from 'next/image';
8
 
9
  export interface ChatCardProps {
10
+ chat: ChatEntity;
 
11
  }
12
 
13
+ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
14
  const { chatId: chatIdFromParam } = useParams();
15
+ const { id, url, messages, user } = chat;
16
  return (
17
  <Link
18
  className={cn(
19
+ 'p-2 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
20
  chatIdFromParam === id && 'border-gray-500',
21
  )}
22
  href={`/chat/${id}`}
23
  >
24
+ <div className="overflow-hidden flex items-center">
25
+ <Image
26
+ src={url}
27
+ alt={url}
28
+ width={50}
29
+ height={50}
30
+ className="rounded w-1/4 "
31
+ />
32
+ <p className="text-xs text-gray-500 w-3/4 ml-2">
33
+ {messages?.[0].content.slice(0, 50) + ' ...' ?? 'new chat'}
34
+ </p>
35
  </div>
36
  </Link>
37
  );
components/chat-sidebar/ChatListSidebar.tsx CHANGED
@@ -5,11 +5,10 @@ export interface ChatSidebarListProps {}
5
 
6
  export default async function ChatSidebarList({}: ChatSidebarListProps) {
7
  const chats = await getKVChats();
8
- console.log('[Ming] ~ ChatSidebarList ~ chats:', chats);
9
  return (
10
  <>
11
  {chats.map(chat => (
12
- <ChatCard key={chat.id} id={chat.id} title={chat.title} />
13
  ))}
14
  </>
15
  );
 
5
 
6
  export default async function ChatSidebarList({}: ChatSidebarListProps) {
7
  const chats = await getKVChats();
 
8
  return (
9
  <>
10
  {chats.map(chat => (
11
+ <ChatCard key={chat.id} chat={chat} />
12
  ))}
13
  </>
14
  );
components/chat/ImageSelector.tsx CHANGED
@@ -1,21 +1,36 @@
 
 
1
  import React from 'react';
2
  import Image from 'next/image';
3
  import useImageUpload from '../../lib/hooks/useImageUpload';
4
  import { fetcher } from '@/lib/utils';
 
 
5
 
6
  export interface ImageSelectorProps {}
7
 
8
- const examples = [
9
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
10
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
11
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
12
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
 
 
 
 
 
 
 
 
 
 
13
  ];
14
 
15
  const ImageSelector: React.FC<ImageSelectorProps> = () => {
 
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">
20
  <h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
21
  <p>Lets start by choosing an image</p>
@@ -32,20 +47,23 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
32
  You can also choose from below examples we provided
33
  </p>
34
  <div className="flex">
35
- {examples.map((example, index) => (
36
  <Image
37
- src={example}
38
  key={index}
39
  width={120}
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={() =>
44
- fetcher('/api/upload', {
45
  method: 'POST',
46
- body: JSON.stringify({ url: example }),
47
- })
48
- }
 
 
 
49
  />
50
  ))}
51
  </div>
 
1
+ 'use client';
2
+
3
  import React from 'react';
4
  import Image from 'next/image';
5
  import useImageUpload from '../../lib/hooks/useImageUpload';
6
  import { fetcher } from '@/lib/utils';
7
+ import { ChatEntity, MessageBase } from '@/lib/types';
8
+ import { useRouter } from 'next/navigation';
9
 
10
  export interface ImageSelectorProps {}
11
 
12
+ type Example = {
13
+ url: string;
14
+ initMessages: MessageBase[];
15
+ };
16
+
17
+ const examples: Example[] = [
18
+ {
19
+ url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
20
+ initMessages: [
21
+ { role: 'user', content: 'how many cereals are there in the image?' },
22
+ ],
23
+ },
24
+ // 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
25
+ // 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
26
+ // 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
27
  ];
28
 
29
  const ImageSelector: React.FC<ImageSelectorProps> = () => {
30
+ const router = useRouter();
31
  const { getRootProps, getInputProps } = useImageUpload();
32
  return (
33
+ <div className="mx-auto max-w-2xl px-4 mt-8">
34
  <div className="rounded-lg border bg-background p-8">
35
  <h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
36
  <p>Lets start by choosing an image</p>
 
47
  You can also choose from below examples we provided
48
  </p>
49
  <div className="flex">
50
+ {examples.map(({ url, initMessages }, index) => (
51
  <Image
52
+ src={url}
53
  key={index}
54
  width={120}
55
  height={120}
56
  alt="example images"
57
  className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
58
+ onClick={async () => {
59
+ const resp = await fetcher<ChatEntity>('/api/upload', {
60
  method: 'POST',
61
+ body: JSON.stringify({ url, initMessages }),
62
+ });
63
+ if (resp) {
64
+ router.push(`/chat/${resp.id}`);
65
+ }
66
+ }}
67
  />
68
  ))}
69
  </div>
components/chat/index.tsx CHANGED
@@ -26,12 +26,12 @@ export function Chat({ id, className }: ChatProps) {
26
  <ImageSelector />
27
  </div>
28
  <div className="w-1/2 relative overflow-auto">
29
- <ChatList messages={messages} />
30
  <ChatScrollAnchor trackVisibility={isLoading} />
31
  </div>
32
  </div>
33
  </div>
34
- <ChatPanel
35
  id={id}
36
  isLoading={isLoading}
37
  stop={stop}
@@ -40,7 +40,7 @@ export function Chat({ id, className }: ChatProps) {
40
  messages={messages}
41
  input={input}
42
  setInput={setInput}
43
- />
44
  </>
45
  );
46
  }
 
26
  <ImageSelector />
27
  </div>
28
  <div className="w-1/2 relative overflow-auto">
29
+ {/* <ChatList messages={messages} /> */}
30
  <ChatScrollAnchor trackVisibility={isLoading} />
31
  </div>
32
  </div>
33
  </div>
34
+ {/* <ChatPanel
35
  id={id}
36
  isLoading={isLoading}
37
  stop={stop}
 
40
  messages={messages}
41
  input={input}
42
  setInput={setInput}
43
+ /> */}
44
  </>
45
  );
46
  }
lib/hooks/useImageUpload.ts CHANGED
@@ -5,7 +5,6 @@ import { toast } from 'react-hot-toast';
5
  import { DatasetImageEntity } from '../types';
6
 
7
  const useImageUpload = (options?: Partial<DropzoneOptions>) => {
8
- const [, setTarget] = useAtom(datasetAtom);
9
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
10
  accept: {
11
  'image/*': ['.jpeg', '.png'],
@@ -21,27 +20,27 @@ const useImageUpload = (options?: Partial<DropzoneOptions>) => {
21
  try {
22
  const reader = new FileReader();
23
  reader.onloadend = () => {
24
- const newImage = reader.result as string;
25
- setTarget(prev => {
26
- // Check if the image already exists in the state
27
- if (
28
- // prev.length >= 10 ||
29
- prev.find(entity => entity.url === newImage)
30
- ) {
31
- // If it does, return the state unchanged
32
- return prev;
33
- } else {
34
- // If it doesn't, add the new image to the state
35
- return [
36
- ...prev,
37
- {
38
- url: newImage,
39
- selected: false,
40
- name: `i-${prev.length + 1}`,
41
- } satisfies DatasetImageEntity,
42
- ];
43
- }
44
- });
45
  };
46
  reader.readAsDataURL(file);
47
  } catch (err) {
 
5
  import { DatasetImageEntity } from '../types';
6
 
7
  const useImageUpload = (options?: Partial<DropzoneOptions>) => {
 
8
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
9
  accept: {
10
  'image/*': ['.jpeg', '.png'],
 
20
  try {
21
  const reader = new FileReader();
22
  reader.onloadend = () => {
23
+ // const newImage = reader.result as string;
24
+ // setTarget(prev => {
25
+ // // Check if the image already exists in the state
26
+ // if (
27
+ // // prev.length >= 10 ||
28
+ // prev.find(entity => entity.url === newImage)
29
+ // ) {
30
+ // // If it does, return the state unchanged
31
+ // return prev;
32
+ // } else {
33
+ // // If it doesn't, add the new image to the state
34
+ // return [
35
+ // ...prev,
36
+ // {
37
+ // url: newImage,
38
+ // selected: false,
39
+ // name: `i-${prev.length + 1}`,
40
+ // } satisfies DatasetImageEntity,
41
+ // ];
42
+ // }
43
+ // });
44
  };
45
  reader.readAsDataURL(file);
46
  } catch (err) {
lib/kv/chat.ts CHANGED
@@ -1,11 +1,10 @@
1
  'use server';
2
 
3
  import { revalidatePath } from 'next/cache';
4
- import { redirect } from 'next/navigation';
5
  import { kv } from '@vercel/kv';
6
 
7
  import { auth } from '@/auth';
8
- import { type Chat } from '@/lib/types';
9
 
10
  export async function getKVChats() {
11
  const session = await auth();
@@ -27,18 +26,14 @@ export async function getKVChats() {
27
 
28
  const results = await pipeline.exec();
29
 
30
- return results as Chat[];
31
  } catch (error) {
32
  return [];
33
  }
34
  }
35
 
36
- export async function getKVChat(id: string, userId: string) {
37
- const chat = await kv.hgetall<Chat>(`chat:${id}`);
38
-
39
- if (!chat || (userId && chat.userId !== userId)) {
40
- return null;
41
- }
42
 
43
  return chat;
44
  }
 
1
  'use server';
2
 
3
  import { revalidatePath } from 'next/cache';
 
4
  import { kv } from '@vercel/kv';
5
 
6
  import { auth } from '@/auth';
7
+ import { ChatEntity } from '@/lib/types';
8
 
9
  export async function getKVChats() {
10
  const session = await auth();
 
26
 
27
  const results = await pipeline.exec();
28
 
29
+ return results as ChatEntity[];
30
  } catch (error) {
31
  return [];
32
  }
33
  }
34
 
35
+ export async function getKVChat(id: string) {
36
+ const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
 
 
 
 
37
 
38
  return chat;
39
  }
lib/types.ts CHANGED
@@ -1,14 +1,5 @@
1
  import { type Message } from 'ai';
2
 
3
- export interface Chat extends Record<string, any> {
4
- id: string;
5
- title: string;
6
- createdAt: Date;
7
- userId: string;
8
- path: string;
9
- messages: Message[];
10
- }
11
-
12
  export type ServerActionResult<Result> = Promise<
13
  | Result
14
  | {
@@ -16,12 +7,30 @@ export type ServerActionResult<Result> = Promise<
16
  }
17
  >;
18
 
 
 
 
19
  export type DatasetImageEntity = {
20
  url: string;
21
  selected?: boolean;
22
  name: string;
23
  };
24
 
 
 
 
25
  export type MessageWithSelectedDataset = Message & {
26
  dataset: DatasetImageEntity[];
27
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { type Message } from 'ai';
2
 
 
 
 
 
 
 
 
 
 
3
  export type ServerActionResult<Result> = Promise<
4
  | Result
5
  | {
 
7
  }
8
  >;
9
 
10
+ /**
11
+ * @deprecated
12
+ */
13
  export type DatasetImageEntity = {
14
  url: string;
15
  selected?: boolean;
16
  name: string;
17
  };
18
 
19
+ /**
20
+ * @deprecated
21
+ */
22
  export type MessageWithSelectedDataset = Message & {
23
  dataset: DatasetImageEntity[];
24
  };
25
+
26
+ export type MessageBase = {
27
+ role: 'user' | 'system' | 'assistant';
28
+ content: string;
29
+ };
30
+
31
+ export type ChatEntity = {
32
+ url: string;
33
+ id: string;
34
+ user: string; // email
35
+ messages: MessageBase[];
36
+ };
lib/utils.ts CHANGED
@@ -16,7 +16,6 @@ export async function fetcher<JSON = any>(
16
  init?: RequestInit,
17
  ): Promise<JSON> {
18
  const res = await fetch(input, init);
19
- console.log('[Ming] ~ res:', res);
20
 
21
  if (!res.ok) {
22
  const json = await res.json();
 
16
  init?: RequestInit,
17
  ): Promise<JSON> {
18
  const res = await fetch(input, init);
 
19
 
20
  if (!res.ok) {
21
  const json = await res.json();
prettier.config.cjs DELETED
@@ -1,36 +0,0 @@
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
- useTabs: false,
10
- trailingComma: 'all',
11
- bracketSpacing: true,
12
- importOrder: [
13
- '^(react/(.*)$)|^(react$)',
14
- '^(next/(.*)$)|^(next$)',
15
- '<THIRD_PARTY_MODULES>',
16
- '',
17
- '^types$',
18
- '^@/types/(.*)$',
19
- '^@/config/(.*)$',
20
- '^@/lib/(.*)$',
21
- '^@/hooks/(.*)$',
22
- '^@/components/ui/(.*)$',
23
- '^@/components/(.*)$',
24
- '^@/registry/(.*)$',
25
- '^@/styles/(.*)$',
26
- '^@/app/(.*)$',
27
- '',
28
- '^[./]',
29
- ],
30
- importOrderSeparation: false,
31
- importOrderSortSpecifiers: true,
32
- importOrderBuiltinModulesToTop: true,
33
- importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
34
- importOrderMergeDuplicateImports: true,
35
- importOrderCombineTypeAndValueImports: true,
36
- };