MingruiZhang commited on
Commit
5ec491a
1 Parent(s): 09e5d25

feat: Setup postgres and prisma (#50)

Browse files

https://vercel.com/landing-platform/~/stores/postgres/store_kmMuRzKbVnJ5IGpL/guides

<img width="311" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/5669963/2bceb4d8-cbfd-45ea-a98e-97220f718d70">

**Server function to migrate**

- [DONE] Create user
- [DONE] Logout user create Chat with init message
- [DONE] Logout out user create message into existing chat
- [DONE] Login user show chat history
- [DONE] Login user create message into existing chat
- [DONE] Delete Chat

app/all/chat/[id]/page.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import { getKVChat } from '@/lib/kv/chat';
2
  import { Chat } from '@/components/chat';
3
  import { auth } from '@/auth';
 
 
4
 
5
  interface PageProps {
6
  params: {
@@ -9,8 +10,9 @@ interface PageProps {
9
  }
10
 
11
  export default async function Page({ params }: PageProps) {
12
- const { id: chatId } = params;
13
- const chat = await getKVChat(chatId);
14
- const session = await auth();
15
- return <Chat chat={chat} session={session} isAdminView />;
 
16
  }
 
 
1
  import { Chat } from '@/components/chat';
2
  import { auth } from '@/auth';
3
+ import { dbGetChat } from '@/lib/db/functions';
4
+ import { redirect } from 'next/navigation';
5
 
6
  interface PageProps {
7
  params: {
 
10
  }
11
 
12
  export default async function Page({ params }: PageProps) {
13
+ return <div>TO BE FIXED</div>;
14
+ // const { id: chatId } = params;
15
+ // const chat = await getKVChat(chatId);
16
+ // const session = await auth();
17
+ // return <Chat chat={chat} session={session} isAdminView />;
18
  }
app/all/layout.tsx CHANGED
@@ -1,8 +1,7 @@
1
  import { Suspense } from 'react';
2
  import Loading from '@/components/ui/Loading';
3
- import { authEmail } from '@/auth';
4
  import { redirect } from 'next/navigation';
5
- import { adminGetAllKVChats } from '@/lib/kv/chat';
6
  import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
7
 
8
  interface ChatLayoutProps {
@@ -10,30 +9,31 @@ interface ChatLayoutProps {
10
  }
11
 
12
  export default async function Layout({ children }: ChatLayoutProps) {
13
- const { isAdmin, user } = await authEmail();
 
14
 
15
- if (!isAdmin) {
16
- redirect('/');
17
- }
18
- const chats = await adminGetAllKVChats();
19
 
20
- return (
21
- <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
22
- {user && (
23
- <div
24
- data-state="open"
25
- 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"
26
- >
27
- <Suspense fallback={<Loading />}>
28
- <ChatSidebarList chats={chats} isAdminView />
29
- </Suspense>
30
- </div>
31
- )}
32
- <Suspense fallback={<Loading />}>
33
- <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]">
34
- {children}
35
- </div>
36
- </Suspense>
37
- </div>
38
- );
39
  }
 
1
  import { Suspense } from 'react';
2
  import Loading from '@/components/ui/Loading';
3
+ import { sessionUser } from '@/auth';
4
  import { redirect } from 'next/navigation';
 
5
  import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
6
 
7
  interface ChatLayoutProps {
 
9
  }
10
 
11
  export default async function Layout({ children }: ChatLayoutProps) {
12
+ return <div>TO BE FIXED</div>;
13
+ // const { isAdmin, user } = await sessionUser();
14
 
15
+ // if (!isAdmin) {
16
+ // redirect('/');
17
+ // }
18
+ // const chats = await adminGetAllKVChats();
19
 
20
+ // return (
21
+ // <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
22
+ // {user && (
23
+ // <div
24
+ // data-state="open"
25
+ // 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"
26
+ // >
27
+ // <Suspense fallback={<Loading />}>
28
+ // <ChatSidebarList chats={chats} isAdminView />
29
+ // </Suspense>
30
+ // </div>
31
+ // )}
32
+ // <Suspense fallback={<Loading />}>
33
+ // <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]">
34
+ // {children}
35
+ // </div>
36
+ // </Suspense>
37
+ // </div>
38
+ // );
39
  }
app/api/auth/[...nextauth]/route.ts CHANGED
@@ -1,2 +1,2 @@
1
  export { GET, POST } from '@/auth';
2
- export const runtime = 'edge';
 
1
  export { GET, POST } from '@/auth';
2
+ // export const runtime = 'edge';
app/api/{upload → chat/create}/route.ts RENAMED
@@ -1,9 +1,6 @@
1
- import { auth } from '@/auth';
2
- import { createKVChat } from '@/lib/kv/chat';
3
  import { withLogging } from '@/lib/logger';
4
- import { ChatEntity, MessageBase } from '@/lib/types';
5
- import { nanoid } from '@/lib/utils';
6
- import { Session } from 'next-auth';
7
  import { revalidatePath } from 'next/cache';
8
 
9
  /**
@@ -12,36 +9,24 @@ import { revalidatePath } from 'next/cache';
12
  */
13
  export const POST = withLogging(
14
  async (
15
- session,
16
  json: {
17
  id?: string;
18
  url: string;
19
- initMessages?: MessageBase[];
20
  },
21
  ): Promise<Response> => {
22
- const user = session?.user?.email ?? 'anonymous';
23
- // if (!email) {
24
- // return new Response('Unauthorized', {
25
- // status: 401,
26
- // });
27
- // }
28
-
29
  try {
30
- const { id, url, initMessages } = json;
31
-
32
- const payload: ChatEntity = {
33
- url,
34
- id: id ?? nanoid(),
35
- user,
36
- messages: initMessages ?? [],
37
- updatedAt: Date.now(),
38
- };
39
 
40
- await createKVChat(payload);
 
 
 
 
41
 
42
  revalidatePath('/chat', 'layout');
43
-
44
- return Response.json(payload);
45
  } catch (error) {
46
  return new Response((error as Error).message, {
47
  status: 400,
 
1
+ import { dbPostCreateChat } from '@/lib/db/functions';
2
+ import { MessageRaw } from '@/lib/db/types';
3
  import { withLogging } from '@/lib/logger';
 
 
 
4
  import { revalidatePath } from 'next/cache';
5
 
6
  /**
 
9
  */
10
  export const POST = withLogging(
11
  async (
12
+ _session,
13
  json: {
14
  id?: string;
15
  url: string;
16
+ initMessages?: MessageRaw[];
17
  },
18
  ): Promise<Response> => {
 
 
 
 
 
 
 
19
  try {
20
+ const { url, id, initMessages } = json;
 
 
 
 
 
 
 
 
21
 
22
+ const response = await dbPostCreateChat({
23
+ id,
24
+ mediaUrl: url,
25
+ initMessages,
26
+ });
27
 
28
  revalidatePath('/chat', 'layout');
29
+ return Response.json(response);
 
30
  } catch (error) {
31
  return new Response((error as Error).message, {
32
  status: 400,
app/api/vision-agent/route.ts CHANGED
@@ -16,12 +16,12 @@ export const POST = withLogging(
16
  json: {
17
  messages: MessageBase[];
18
  id: string;
19
- url: string;
20
  enableSelfReflection: boolean;
21
  },
22
  request,
23
  ) => {
24
- const { messages, url, enableSelfReflection } = json;
25
 
26
  // const session = await auth();
27
  // if (!session?.user?.email) {
@@ -53,7 +53,7 @@ export const POST = withLogging(
53
  }),
54
  ),
55
  );
56
- formData.append('image', url);
57
 
58
  const fetchResponse = await fetch(
59
  `https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&visualize_output=true&self_reflection=${enableSelfReflection}`,
 
16
  json: {
17
  messages: MessageBase[];
18
  id: string;
19
+ mediaUrl: string;
20
  enableSelfReflection: boolean;
21
  },
22
  request,
23
  ) => {
24
+ const { messages, mediaUrl, enableSelfReflection } = json;
25
 
26
  // const session = await auth();
27
  // if (!session?.user?.email) {
 
53
  }),
54
  ),
55
  );
56
+ formData.append('image', mediaUrl);
57
 
58
  const fetchResponse = await fetch(
59
  `https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&visualize_output=true&self_reflection=${enableSelfReflection}`,
app/chat/[id]/page.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import { getKVChat } from '@/lib/kv/chat';
2
  import { Chat } from '@/components/chat';
3
  import { auth } from '@/auth';
4
- import { Session } from 'next-auth';
 
5
 
6
  interface PageProps {
7
  params: {
@@ -11,7 +11,10 @@ interface PageProps {
11
 
12
  export default async function Page({ params }: PageProps) {
13
  const { id: chatId } = params;
14
- const chat = await getKVChat(chatId);
 
 
 
15
  const session = await auth();
16
  return <Chat chat={chat} session={session} />;
17
  }
 
 
1
  import { Chat } from '@/components/chat';
2
  import { auth } from '@/auth';
3
+ import { dbGetChat } from '@/lib/db/functions';
4
+ import { redirect } from 'next/navigation';
5
 
6
  interface PageProps {
7
  params: {
 
11
 
12
  export default async function Page({ params }: PageProps) {
13
  const { id: chatId } = params;
14
+ const chat = await dbGetChat(chatId);
15
+ if (!chat) {
16
+ redirect('/');
17
+ }
18
  const session = await auth();
19
  return <Chat chat={chat} session={session} />;
20
  }
app/chat/layout.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import { authEmail } from '@/auth';
2
  import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
3
  import Loading from '@/components/ui/Loading';
4
- import { getKVChats } from '@/lib/kv/chat';
5
  import { Suspense } from 'react';
6
 
7
  interface ChatLayoutProps {
@@ -9,8 +9,8 @@ interface ChatLayoutProps {
9
  }
10
 
11
  export default async function Layout({ children }: ChatLayoutProps) {
12
- const { email, user } = await authEmail();
13
- const chats = await getKVChats();
14
 
15
  return (
16
  <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
 
1
+ import { sessionUser } from '@/auth';
2
  import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
3
  import Loading from '@/components/ui/Loading';
4
+ import { dbGetAllChat } from '@/lib/db/functions';
5
  import { Suspense } from 'react';
6
 
7
  interface ChatLayoutProps {
 
9
  }
10
 
11
  export default async function Layout({ children }: ChatLayoutProps) {
12
+ const { email, user, id } = await sessionUser();
13
+ const chats = await dbGetAllChat();
14
 
15
  return (
16
  <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
app/chat/page.tsx CHANGED
@@ -2,7 +2,6 @@
2
 
3
  import ImageSelector from '@/components/chat/ImageSelector';
4
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
5
- import { ChatEntity } from '@/lib/types';
6
  import { fetcher } from '@/lib/utils';
7
  import { useRouter } from 'next/navigation';
8
 
@@ -15,12 +14,16 @@ import { IconDiscord, IconGitHub } from '@/components/ui/Icons';
15
  import Link from 'next/link';
16
  import { Button } from '@/components/ui/Button';
17
  import Img from '@/components/ui/Img';
 
18
 
19
  // const EXAMPLE_URL = 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg';
20
- const EXAMPLE_URL = 'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png';
 
21
  const EXAMPLE_HEADER = 'Counting and find';
22
- const EXAMPLE_SUBHEADER = 'number of flowers, area of largest and smallest flower';
23
- const EXAMPLE_PROMPT = 'Count the number of flowers and find the area of the largest and smallest flower';
 
 
24
 
25
  const exampleMessages = [
26
  {
@@ -32,7 +35,6 @@ const exampleMessages = [
32
  role: 'user',
33
  content:
34
  EXAMPLE_PROMPT + '\n\n' + generateInputImageMarkdown(EXAMPLE_URL),
35
- id: 'fake-id-1',
36
  },
37
  ],
38
  },
@@ -91,7 +93,7 @@ export default function Page() {
91
  index > 1 && 'hidden md:block'
92
  }`}
93
  onClick={async () => {
94
- const resp = await fetcher<ChatEntity>('/api/upload', {
95
  method: 'POST',
96
  headers: {
97
  'Content-Type': 'application/json',
 
2
 
3
  import ImageSelector from '@/components/chat/ImageSelector';
4
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
 
5
  import { fetcher } from '@/lib/utils';
6
  import { useRouter } from 'next/navigation';
7
 
 
14
  import Link from 'next/link';
15
  import { Button } from '@/components/ui/Button';
16
  import Img from '@/components/ui/Img';
17
+ import { ChatWithMessages } from '@/lib/db/types';
18
 
19
  // const EXAMPLE_URL = 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg';
20
+ const EXAMPLE_URL =
21
+ 'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png';
22
  const EXAMPLE_HEADER = 'Counting and find';
23
+ const EXAMPLE_SUBHEADER =
24
+ 'number of flowers, area of largest and smallest flower';
25
+ const EXAMPLE_PROMPT =
26
+ 'Count the number of flowers and find the area of the largest and smallest flower';
27
 
28
  const exampleMessages = [
29
  {
 
35
  role: 'user',
36
  content:
37
  EXAMPLE_PROMPT + '\n\n' + generateInputImageMarkdown(EXAMPLE_URL),
 
38
  },
39
  ],
40
  },
 
93
  index > 1 && 'hidden md:block'
94
  }`}
95
  onClick={async () => {
96
+ const resp = await fetcher<ChatWithMessages>('/api/chat/create', {
97
  method: 'POST',
98
  headers: {
99
  'Content-Type': 'application/json',
app/project/layout.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import ProjectListSideBar from '@/components/project-sidebar/ProjectListSideBar';
2
  import { Suspense } from 'react';
3
  import Loading from '@/components/ui/Loading';
4
- import { authEmail } from '@/auth';
5
  import { redirect } from 'next/navigation';
6
 
7
  interface ChatLayoutProps {
@@ -9,7 +9,7 @@ interface ChatLayoutProps {
9
  }
10
 
11
  export default async function Layout({ children }: ChatLayoutProps) {
12
- const { isAdmin } = await authEmail();
13
 
14
  if (!isAdmin) {
15
  redirect('/');
 
1
  import ProjectListSideBar from '@/components/project-sidebar/ProjectListSideBar';
2
  import { Suspense } from 'react';
3
  import Loading from '@/components/ui/Loading';
4
+ import { sessionUser } from '@/auth';
5
  import { redirect } from 'next/navigation';
6
 
7
  interface ChatLayoutProps {
 
9
  }
10
 
11
  export default async function Layout({ children }: ChatLayoutProps) {
12
+ const { isAdmin } = await sessionUser();
13
 
14
  if (!isAdmin) {
15
  redirect('/');
auth.ts CHANGED
@@ -1,6 +1,8 @@
1
  import NextAuth, { type DefaultSession } from 'next-auth';
2
  import GitHub from 'next-auth/providers/github';
3
  import Google from 'next-auth/providers/google';
 
 
4
 
5
  declare module 'next-auth' {
6
  interface Session {
@@ -25,14 +27,28 @@ export const {
25
  }),
26
  ],
27
  callbacks: {
28
- // signIn({ profile }) {
29
- // if (profile?.email?.endsWith('@landing.ai')) {
30
- // return !!profile;
31
- // } else {
32
- // return '/unauthorized';
33
- // }
34
- // },
35
- jwt({ token, profile }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if (profile) {
37
  token.id = profile.id || profile.sub;
38
  token.image = profile.avatar_url || profile.picture;
@@ -40,8 +56,9 @@ export const {
40
  return token;
41
  },
42
  session: ({ session, token }) => {
43
- if (session?.user && token?.id) {
44
- session.user.id = String(token.id);
 
45
  }
46
  return session;
47
  },
@@ -59,12 +76,13 @@ export const {
59
  },
60
  });
61
 
62
- export async function authEmail() {
63
  const session = await auth();
64
- const email = session?.user?.email;
65
  return {
66
  email,
67
  isAdmin: !!email?.endsWith('landing.ai'),
68
- user: session?.user,
 
69
  };
70
  }
 
1
  import NextAuth, { type DefaultSession } from 'next-auth';
2
  import GitHub from 'next-auth/providers/github';
3
  import Google from 'next-auth/providers/google';
4
+ import { dbFindOrCreateUser } from './lib/db/functions';
5
+ import { redirect } from 'next/navigation';
6
 
7
  declare module 'next-auth' {
8
  interface Session {
 
27
  }),
28
  ],
29
  callbacks: {
30
+ async signIn({ profile, user }) {
31
+ if (!profile) {
32
+ return false;
33
+ }
34
+ const { email, name } = profile;
35
+
36
+ if (!email || !name) {
37
+ return false;
38
+ }
39
+
40
+ const dbUser = await dbFindOrCreateUser(email, name);
41
+
42
+ if (dbUser) {
43
+ user.id = dbUser.id;
44
+ return true;
45
+ }
46
+ return false;
47
+ },
48
+ async jwt({ token, profile, user }) {
49
+ // console.log('[Ming] ~ jwt ~ user:', user, token);
50
+ // const dbUser = await dbFindOrCreateUser(email, name);
51
+ // console.log('[Ming] ~ signIn ~ dbUser:', dbUser);
52
  if (profile) {
53
  token.id = profile.id || profile.sub;
54
  token.image = profile.avatar_url || profile.picture;
 
56
  return token;
57
  },
58
  session: ({ session, token }) => {
59
+ if (token) {
60
+ // put db user id into session
61
+ session.user.id = token.sub ?? '';
62
  }
63
  return session;
64
  },
 
76
  },
77
  });
78
 
79
+ export async function sessionUser() {
80
  const session = await auth();
81
+ const email = session?.user.email;
82
  return {
83
  email,
84
  isAdmin: !!email?.endsWith('landing.ai'),
85
+ id: session?.user.id ?? null,
86
+ user: session?.user ?? null,
87
  };
88
  }
components/Header.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import * as React from 'react';
2
  import Link from 'next/link';
3
 
4
- import { auth, authEmail } from '@/auth';
5
  import { Button } from '@/components/ui/Button';
6
  import { UserMenu } from '@/components/UserMenu';
7
  import {
@@ -15,7 +15,7 @@ import { redirect } from 'next/navigation';
15
 
16
  export async function Header() {
17
  const session = await auth();
18
- const { isAdmin } = await authEmail();
19
 
20
  if (process.env.NEXT_PUBLIC_IS_HUGGING_FACE) {
21
  return (
@@ -38,7 +38,7 @@ export async function Header() {
38
  </TooltipTrigger>
39
  <TooltipContent>New chat</TooltipContent>
40
  </Tooltip> */}
41
- {isAdmin && (
42
  <Button variant="link" asChild className="mr-2">
43
  <Link href="/all">All Chats (Internal)</Link>
44
  </Button>
@@ -47,7 +47,7 @@ export async function Header() {
47
  <Button variant="link" asChild className="mr-2">
48
  <Link href="/project">Projects (Internal)</Link>
49
  </Button>
50
- )}
51
  <Button variant="link" asChild className="mr-2">
52
  <Link href="/chat">Chat</Link>
53
  </Button>
 
1
  import * as React from 'react';
2
  import Link from 'next/link';
3
 
4
+ import { auth, sessionUser } from '@/auth';
5
  import { Button } from '@/components/ui/Button';
6
  import { UserMenu } from '@/components/UserMenu';
7
  import {
 
15
 
16
  export async function Header() {
17
  const session = await auth();
18
+ const { isAdmin } = await sessionUser();
19
 
20
  if (process.env.NEXT_PUBLIC_IS_HUGGING_FACE) {
21
  return (
 
38
  </TooltipTrigger>
39
  <TooltipContent>New chat</TooltipContent>
40
  </Tooltip> */}
41
+ {/* {isAdmin && (
42
  <Button variant="link" asChild className="mr-2">
43
  <Link href="/all">All Chats (Internal)</Link>
44
  </Button>
 
47
  <Button variant="link" asChild className="mr-2">
48
  <Link href="/project">Projects (Internal)</Link>
49
  </Button>
50
+ )} */}
51
  <Button variant="link" asChild className="mr-2">
52
  <Link href="/chat">Chat</Link>
53
  </Button>
components/chat-sidebar/ChatCard.tsx CHANGED
@@ -4,18 +4,17 @@ import { PropsWithChildren } from 'react';
4
  import Link from 'next/link';
5
  import { useParams, usePathname, useRouter } from 'next/navigation';
6
  import { cn } from '@/lib/utils';
7
- import { ChatEntity } from '@/lib/types';
8
  import Image from 'next/image';
9
  import clsx from 'clsx';
10
  import Img from '../ui/Img';
11
  import { format } from 'date-fns';
12
  import { cleanInputMessage } from '@/lib/messageUtils';
13
  import { IconClose } from '../ui/Icons';
14
- import { removeKVChat } from '@/lib/kv/chat';
15
- // import { format } from 'date-fns';
16
 
17
  type ChatCardProps = PropsWithChildren<{
18
- chat: ChatEntity;
19
  isAdminView?: boolean;
20
  }>;
21
 
@@ -37,7 +36,7 @@ export const ChatCardLayout: React.FC<
37
 
38
  const ChatCard: React.FC<ChatCardProps> = ({ chat, isAdminView }) => {
39
  const { id: chatIdFromParam } = useParams();
40
- const { id, url, messages, user, updatedAt } = chat;
41
  if (!id) {
42
  return null;
43
  }
@@ -53,17 +52,17 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat, isAdminView }) => {
53
  classNames={chatIdFromParam === id && 'border-gray-500'}
54
  >
55
  <div className="overflow-hidden flex items-center size-full group">
56
- <Img src={url} alt={`chat-${id}-card-image`} className="w-1/4" />
57
  <div className="flex items-start flex-col h-full ml-3 w-3/4">
58
  <p className="text-sm mb-1">{title}</p>
59
  <p className="text-xs text-gray-500">
60
  {updatedAt ? format(Number(updatedAt), 'yyyy-MM-dd') : '-'}
61
  </p>
62
- {isAdminView && <p className="text-xs text-gray-500">{user}</p>}
63
  <IconClose
64
  onClick={async e => {
65
  e.stopPropagation();
66
- await removeKVChat(id);
67
  }}
68
  className="absolute right-4 opacity-0 group-hover:opacity-100 top-1/2 -translate-y-1/2"
69
  />
 
4
  import Link from 'next/link';
5
  import { useParams, usePathname, useRouter } from 'next/navigation';
6
  import { cn } from '@/lib/utils';
 
7
  import Image from 'next/image';
8
  import clsx from 'clsx';
9
  import Img from '../ui/Img';
10
  import { format } from 'date-fns';
11
  import { cleanInputMessage } from '@/lib/messageUtils';
12
  import { IconClose } from '../ui/Icons';
13
+ import { ChatWithMessages } from '@/lib/db/types';
14
+ import { dbDeleteChat } from '@/lib/db/functions';
15
 
16
  type ChatCardProps = PropsWithChildren<{
17
+ chat: ChatWithMessages;
18
  isAdminView?: boolean;
19
  }>;
20
 
 
36
 
37
  const ChatCard: React.FC<ChatCardProps> = ({ chat, isAdminView }) => {
38
  const { id: chatIdFromParam } = useParams();
39
+ const { id, mediaUrl, messages, userId, updatedAt } = chat;
40
  if (!id) {
41
  return null;
42
  }
 
52
  classNames={chatIdFromParam === id && 'border-gray-500'}
53
  >
54
  <div className="overflow-hidden flex items-center size-full group">
55
+ <Img src={mediaUrl} alt={`chat-${id}-card-image`} className="w-1/4" />
56
  <div className="flex items-start flex-col h-full ml-3 w-3/4">
57
  <p className="text-sm mb-1">{title}</p>
58
  <p className="text-xs text-gray-500">
59
  {updatedAt ? format(Number(updatedAt), 'yyyy-MM-dd') : '-'}
60
  </p>
61
+ {isAdminView && <p className="text-xs text-gray-500">{userId}</p>}
62
  <IconClose
63
  onClick={async e => {
64
  e.stopPropagation();
65
+ await dbDeleteChat(id);
66
  }}
67
  className="absolute right-4 opacity-0 group-hover:opacity-100 top-1/2 -translate-y-1/2"
68
  />
components/chat-sidebar/ChatListSidebar.tsx CHANGED
@@ -3,13 +3,13 @@
3
  import ChatCard, { ChatCardLayout } from './ChatCard';
4
  import { IconPlus } from '../ui/Icons';
5
  import { auth } from '@/auth';
6
- import { ChatEntity } from '@/lib/types';
7
  import { VariableSizeList as List } from 'react-window';
8
  import { cleanInputMessage } from '@/lib/messageUtils';
9
  import AutoSizer from 'react-virtualized-auto-sizer';
 
10
 
11
  export interface ChatSidebarListProps {
12
- chats: ChatEntity[];
13
  isAdminView?: boolean;
14
  }
15
 
 
3
  import ChatCard, { ChatCardLayout } from './ChatCard';
4
  import { IconPlus } from '../ui/Icons';
5
  import { auth } from '@/auth';
 
6
  import { VariableSizeList as List } from 'react-window';
7
  import { cleanInputMessage } from '@/lib/messageUtils';
8
  import AutoSizer from 'react-virtualized-auto-sizer';
9
+ import { ChatWithMessages } from '@/lib/db/types';
10
 
11
  export interface ChatSidebarListProps {
12
+ chats: ChatWithMessages[];
13
  isAdminView?: boolean;
14
  }
15
 
components/chat/ImageSelector.tsx CHANGED
@@ -3,7 +3,7 @@
3
  import React, { useCallback, useState } from 'react';
4
  import useImageUpload from '../../lib/hooks/useImageUpload';
5
  import { cn, fetcher } from '@/lib/utils';
6
- import { SignedPayload, MessageBase, ChatEntity } from '@/lib/types';
7
  import { useRouter } from 'next/navigation';
8
  import Loading from '../ui/Loading';
9
  import toast from 'react-hot-toast';
@@ -11,6 +11,7 @@ import {
11
  generateVideoThumbnails,
12
  getVideoCover,
13
  } from '@rajesh896/video-thumbnails-generator';
 
14
 
15
  export interface ImageSelectorProps {}
16
 
@@ -86,7 +87,7 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
86
  return upload(thumbnailFile, resp.id);
87
  });
88
  }
89
- await fetcher<ChatEntity>('/api/upload', {
90
  method: 'POST',
91
  headers: {
92
  'Content-Type': 'application/json',
 
3
  import React, { useCallback, useState } from 'react';
4
  import useImageUpload from '../../lib/hooks/useImageUpload';
5
  import { cn, fetcher } from '@/lib/utils';
6
+ import { SignedPayload, MessageBase } from '@/lib/types';
7
  import { useRouter } from 'next/navigation';
8
  import Loading from '../ui/Loading';
9
  import toast from 'react-hot-toast';
 
11
  generateVideoThumbnails,
12
  getVideoCover,
13
  } from '@rajesh896/video-thumbnails-generator';
14
+ import { ChatWithMessages } from '@/lib/db/types';
15
 
16
  export interface ImageSelectorProps {}
17
 
 
87
  return upload(thumbnailFile, resp.id);
88
  });
89
  }
90
+ await fetcher<ChatWithMessages>('/api/chat/create', {
91
  method: 'POST',
92
  headers: {
93
  'Content-Type': 'application/json',
components/chat/index.tsx CHANGED
@@ -2,20 +2,20 @@
2
 
3
  import { ChatList } from '@/components/chat/ChatList';
4
  import { Composer } from '@/components/chat/Composer';
5
- import { ChatEntity } from '@/lib/types';
6
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
7
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
8
  import { Session } from 'next-auth';
9
  import { useState } from 'react';
 
10
 
11
  export interface ChatProps extends React.ComponentProps<'div'> {
12
- chat: ChatEntity;
13
  isAdminView?: boolean;
14
  session: Session | null;
15
  }
16
 
17
  export function Chat({ chat, session, isAdminView }: ChatProps) {
18
- const { url, id } = chat;
19
  const { messages, append, reload, stop, isLoading, input, setInput } =
20
  useVisionAgent(chat);
21
 
@@ -38,7 +38,7 @@ export function Chat({ chat, session, isAdminView }: ChatProps) {
38
  <div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]">
39
  <Composer
40
  id={id}
41
- url={url}
42
  isLoading={isLoading}
43
  stop={stop}
44
  append={append}
 
2
 
3
  import { ChatList } from '@/components/chat/ChatList';
4
  import { Composer } from '@/components/chat/Composer';
 
5
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
  import { Session } from 'next-auth';
8
  import { useState } from 'react';
9
+ import { ChatWithMessages } from '@/lib/db/types';
10
 
11
  export interface ChatProps extends React.ComponentProps<'div'> {
12
+ chat: ChatWithMessages;
13
  isAdminView?: boolean;
14
  session: Session | null;
15
  }
16
 
17
  export function Chat({ chat, session, isAdminView }: ChatProps) {
18
+ const { mediaUrl, id } = chat;
19
  const { messages, append, reload, stop, isLoading, input, setInput } =
20
  useVisionAgent(chat);
21
 
 
38
  <div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] h-[178px]">
39
  <Composer
40
  id={id}
41
+ url={mediaUrl}
42
  isLoading={isLoading}
43
  stop={stop}
44
  append={append}
components/project/ProjectChat.tsx CHANGED
@@ -15,45 +15,43 @@ export interface ChatProps {
15
  }
16
 
17
  const ProjectChat: React.FC<ChatProps> = ({ mediaList }) => {
18
- const selectedMediaId = useAtomValue(selectedMediaIdAtom);
19
- // fallback to the first media
20
- const selectedMedia =
21
- mediaList.find(media => media.id === selectedMediaId) ?? mediaList[0];
22
- const { messages, append, reload, stop, isLoading, input, setInput } =
23
- useVisionAgent({
24
- url: selectedMedia.url,
25
- messages: [],
26
- user: 'does-not-matter@landing.ai',
27
- updatedAt: Date.now(),
28
- });
29
-
30
- const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
31
- useScrollAnchor();
32
-
33
- return (
34
- <>
35
- <div className="h-full overflow-auto" ref={scrollRef}>
36
- <div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
37
- <ChatList messages={messages} session={null} isLoading={isLoading} />
38
- <div className="h-px w-full" ref={visibilityRef} />
39
- </div>
40
- </div>
41
- <div className="absolute inset-x-0 bottom-0 w-full h-[178px]">
42
- <Composer
43
- url={selectedMedia.url}
44
- isLoading={isLoading}
45
- stop={stop}
46
- append={append}
47
- reload={reload}
48
- messages={messages}
49
- input={input}
50
- setInput={setInput}
51
- isAtBottom={isAtBottom}
52
- scrollToBottom={scrollToBottom}
53
- />
54
- </div>
55
- </>
56
- );
57
  };
58
 
59
  export default ProjectChat;
 
15
  }
16
 
17
  const ProjectChat: React.FC<ChatProps> = ({ mediaList }) => {
18
+ return <div>TO BE FIXED</div>;
19
+ // const selectedMediaId = useAtomValue(selectedMediaIdAtom);
20
+ // // fallback to the first media
21
+ // const selectedMedia =
22
+ // mediaList.find(media => media.id === selectedMediaId) ?? mediaList[0];
23
+ // const { messages, append, reload, stop, isLoading, input, setInput } =
24
+ // useVisionAgent({
25
+ // mediaUrl: selectedMedia.url,
26
+ // messages: [],
27
+ // userId: nanoid(),
28
+ // });
29
+ // const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
30
+ // useScrollAnchor();
31
+ // return (
32
+ // <>
33
+ // <div className="h-full overflow-auto" ref={scrollRef}>
34
+ // <div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
35
+ // <ChatList messages={messages} session={null} isLoading={isLoading} />
36
+ // <div className="h-px w-full" ref={visibilityRef} />
37
+ // </div>
38
+ // </div>
39
+ // <div className="absolute inset-x-0 bottom-0 w-full h-[178px]">
40
+ // <Composer
41
+ // url={selectedMedia.url}
42
+ // isLoading={isLoading}
43
+ // stop={stop}
44
+ // append={append}
45
+ // reload={reload}
46
+ // messages={messages}
47
+ // input={input}
48
+ // setInput={setInput}
49
+ // isAtBottom={isAtBottom}
50
+ // scrollToBottom={scrollToBottom}
51
+ // />
52
+ // </div>
53
+ // </>
54
+ // );
 
 
55
  };
56
 
57
  export default ProjectChat;
components/ui/Icons.tsx CHANGED
@@ -566,8 +566,8 @@ function IconExclamationTriangle({
566
  <path
567
  d="M8.4449 0.608765C8.0183 -0.107015 6.9817 -0.107015 6.55509 0.608766L0.161178 11.3368C-0.275824 12.07 0.252503 13 1.10608 13H13.8939C14.7475 13 15.2758 12.07 14.8388 11.3368L8.4449 0.608765ZM7.4141 1.12073C7.45288 1.05566 7.54712 1.05566 7.5859 1.12073L13.9798 11.8488C14.0196 11.9154 13.9715 12 13.8939 12H1.10608C1.02849 12 0.980454 11.9154 1.02018 11.8488L7.4141 1.12073ZM6.8269 4.48611C6.81221 4.10423 7.11783 3.78663 7.5 3.78663C7.88217 3.78663 8.18778 4.10423 8.1731 4.48612L8.01921 8.48701C8.00848 8.766 7.7792 8.98664 7.5 8.98664C7.2208 8.98664 6.99151 8.766 6.98078 8.48701L6.8269 4.48611ZM8.24989 10.476C8.24989 10.8902 7.9141 11.226 7.49989 11.226C7.08567 11.226 6.74989 10.8902 6.74989 10.476C6.74989 10.0618 7.08567 9.72599 7.49989 9.72599C7.9141 9.72599 8.24989 10.0618 8.24989 10.476Z"
568
  fill="currentColor"
569
- fill-rule="evenodd"
570
- clip-rule="evenodd"
571
  ></path>
572
  </svg>
573
  );
 
566
  <path
567
  d="M8.4449 0.608765C8.0183 -0.107015 6.9817 -0.107015 6.55509 0.608766L0.161178 11.3368C-0.275824 12.07 0.252503 13 1.10608 13H13.8939C14.7475 13 15.2758 12.07 14.8388 11.3368L8.4449 0.608765ZM7.4141 1.12073C7.45288 1.05566 7.54712 1.05566 7.5859 1.12073L13.9798 11.8488C14.0196 11.9154 13.9715 12 13.8939 12H1.10608C1.02849 12 0.980454 11.9154 1.02018 11.8488L7.4141 1.12073ZM6.8269 4.48611C6.81221 4.10423 7.11783 3.78663 7.5 3.78663C7.88217 3.78663 8.18778 4.10423 8.1731 4.48612L8.01921 8.48701C8.00848 8.766 7.7792 8.98664 7.5 8.98664C7.2208 8.98664 6.99151 8.766 6.98078 8.48701L6.8269 4.48611ZM8.24989 10.476C8.24989 10.8902 7.9141 11.226 7.49989 11.226C7.08567 11.226 6.74989 10.8902 6.74989 10.476C6.74989 10.0618 7.08567 9.72599 7.49989 9.72599C7.9141 9.72599 8.24989 10.0618 8.24989 10.476Z"
568
  fill="currentColor"
569
+ fillRule="evenodd"
570
+ clipRule="evenodd"
571
  ></path>
572
  </svg>
573
  );
lib/db/functions.ts ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+
3
+ import { sessionUser } from '@/auth';
4
+ import prisma from './prisma';
5
+ import { ChatWithMessages, MessageRaw } from './types';
6
+
7
+ /**
8
+ * Finds or creates a user in the database based on the provided email and name.
9
+ * If the user already exists, it returns the existing user.
10
+ * If the user doesn't exist, it creates a new user and returns it.
11
+ *
12
+ * @param email - The email of the user.
13
+ * @param name - The name of the user.
14
+ * @returns A promise that resolves to the user object.
15
+ */
16
+ export async function dbFindOrCreateUser(email: string, name: string) {
17
+ // Try to find the user by email
18
+ const user = await prisma.user.findUnique({
19
+ where: { email: email },
20
+ });
21
+
22
+ // If the user doesn't exist, create it
23
+ if (user) {
24
+ return user;
25
+ } else {
26
+ return prisma.user.create({
27
+ data: {
28
+ email: email,
29
+ name: name,
30
+ },
31
+ });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Retrieves all chats with their associated messages for the current user.
37
+ * @returns A promise that resolves to an array of `ChatWithMessages` objects.
38
+ */
39
+ export async function dbGetAllChat(): Promise<ChatWithMessages[]> {
40
+ const { id: userId } = await sessionUser();
41
+
42
+ if (!userId) return [];
43
+
44
+ return prisma.chat.findMany({
45
+ where: { userId },
46
+ include: {
47
+ messages: true,
48
+ },
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Retrieves a chat with messages from the database based on the provided ID.
54
+ * @param id - The ID of the chat to retrieve.
55
+ * @returns A Promise that resolves to a ChatWithMessages object if found, or null if not found.
56
+ */
57
+ export async function dbGetChat(id: string): Promise<ChatWithMessages | null> {
58
+ return prisma.chat.findUnique({
59
+ where: { id },
60
+ include: {
61
+ messages: true,
62
+ },
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Creates a new chat in the database.
68
+ *
69
+ * @param {string} options.id - The ID of the chat (optional).
70
+ * @param {string} options.mediaUrl - The media URL for the chat.
71
+ * @param {MessageRaw[]} options.initMessages - The initial messages for the chat (optional).
72
+ * @returns {Promise<Chat>} The created chat object.
73
+ */
74
+ export async function dbPostCreateChat({
75
+ id,
76
+ mediaUrl,
77
+ initMessages = [],
78
+ }: {
79
+ id?: string;
80
+ mediaUrl: string;
81
+ initMessages?: MessageRaw[];
82
+ }) {
83
+ const { id: userId } = await sessionUser();
84
+ const userConnect = userId
85
+ ? {
86
+ user: {
87
+ connect: { id: userId }, // Connect the chat to an existing user
88
+ },
89
+ }
90
+ : {};
91
+ try {
92
+ return await prisma.chat.create({
93
+ data: {
94
+ id,
95
+ mediaUrl: mediaUrl,
96
+ ...userConnect,
97
+ messages: {
98
+ create: initMessages.map(message => ({
99
+ ...message,
100
+ ...userConnect,
101
+ })),
102
+ },
103
+ },
104
+ include: {
105
+ messages: true,
106
+ },
107
+ });
108
+ } catch (error) {
109
+ console.error(error);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Creates a new message in the database.
115
+ * @param chatId - The ID of the chat where the message belongs.
116
+ * @param message - The message object to be created.
117
+ * @returns A promise that resolves to the created message.
118
+ */
119
+ export async function dbPostCreateMessage(chatId: string, message: MessageRaw) {
120
+ const { id: userId } = await sessionUser();
121
+ const userConnect = userId
122
+ ? {
123
+ user: {
124
+ connect: { id: userId }, // Connect the chat to an existing user
125
+ },
126
+ }
127
+ : {};
128
+
129
+ return prisma.message.create({
130
+ data: {
131
+ content: message.content,
132
+ role: message.role,
133
+ chat: {
134
+ connect: { id: chatId },
135
+ },
136
+ ...userConnect,
137
+ },
138
+ });
139
+ }
140
+
141
+ export async function dbDeleteChat(chatId: string) {
142
+ return prisma.chat.delete({
143
+ where: { id: chatId },
144
+ });
145
+ }
lib/db/prisma.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ const prisma: PrismaClient = new PrismaClient();
4
+
5
+ export default prisma;
lib/db/types.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { Chat, Message } from '@prisma/client';
2
+
3
+ export type ChatWithMessages = Chat & { messages: Message[] };
4
+
5
+ export type MessageRaw = {
6
+ role: Message['role'];
7
+ content: Message['content'];
8
+ };
lib/hooks/useVisionAgent.ts CHANGED
@@ -1,8 +1,7 @@
1
  import { useChat, type Message, UseChatHelpers } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
  import { useEffect, useState } from 'react';
4
- import { ChatEntity, MessageBase, SignedPayload } from '../types';
5
- import { saveKVChatMessage } from '../kv/chat';
6
  import { fetcher, nanoid } from '../utils';
7
  import {
8
  getCleanedUpMessages,
@@ -11,6 +10,8 @@ import {
11
  } from '../messageUtils';
12
  import { CLEANED_SEPARATOR } from '../constants';
13
  import { useSearchParams } from 'next/navigation';
 
 
14
 
15
  const uploadBase64 = async (
16
  base64: string,
@@ -50,8 +51,8 @@ const uploadBase64 = async (
50
  }
51
  };
52
 
53
- const useVisionAgent = (chat: ChatEntity) => {
54
- const { messages: initialMessages, id, url } = chat;
55
  const searchParams = useSearchParams();
56
  const reflectionValue = searchParams.get('reflection');
57
 
@@ -101,33 +102,39 @@ const useVisionAgent = (chat: ChatEntity) => {
101
  {
102
  id: nanoid(),
103
  role: 'user',
104
- content: input + '\n\n' + generateInputImageMarkdown(url),
 
105
  createdAt: new Date(),
106
  } satisfies Message,
107
  ]
108
  : []),
109
  newMessage,
110
  ]);
111
- if (id) {
112
- saveKVChatMessage(id, newMessage);
113
- }
 
114
  } else {
115
- if (id) {
116
- saveKVChatMessage(id, {
117
- ...message,
118
- content: logs + CLEANED_SEPARATOR + content,
119
- });
120
- }
121
  }
122
  },
123
  initialMessages: initialMessages,
124
  body: {
125
- url,
126
  id,
127
  enableSelfReflection: reflectionValue === 'true',
128
  },
129
  });
130
 
 
 
 
 
 
 
131
  useEffect(() => {
132
  if (
133
  !isLoading &&
@@ -138,28 +145,16 @@ const useVisionAgent = (chat: ChatEntity) => {
138
  }
139
  }, [isLoading, messages, reload]);
140
 
141
- const assistantLoadingMessage = {
142
- id: 'loading',
143
- content: '...',
144
- role: 'assistant',
145
- };
146
-
147
- const messageWithLoading =
148
- isLoading &&
149
- messages.length &&
150
- messages[messages.length - 1].role !== 'assistant'
151
- ? [...messages, assistantLoadingMessage]
152
- : messages;
153
-
154
  const append: UseChatHelpers['append'] = async message => {
155
- if (id) {
156
- await saveKVChatMessage(id, message as MessageBase);
157
- }
 
158
  return appendRaw(message);
159
  };
160
 
161
  return {
162
- messages: messageWithLoading as MessageBase[],
163
  append,
164
  reload,
165
  stop,
 
1
  import { useChat, type Message, UseChatHelpers } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
  import { useEffect, useState } from 'react';
4
+ import { MessageBase, SignedPayload } from '../types';
 
5
  import { fetcher, nanoid } from '../utils';
6
  import {
7
  getCleanedUpMessages,
 
10
  } from '../messageUtils';
11
  import { CLEANED_SEPARATOR } from '../constants';
12
  import { useSearchParams } from 'next/navigation';
13
+ import { ChatWithMessages, MessageRaw } from '../db/types';
14
+ import { dbPostCreateMessage } from '../db/functions';
15
 
16
  const uploadBase64 = async (
17
  base64: string,
 
51
  }
52
  };
53
 
54
+ const useVisionAgent = (chat: ChatWithMessages) => {
55
+ const { messages: initialMessages, id, mediaUrl } = chat;
56
  const searchParams = useSearchParams();
57
  const reflectionValue = searchParams.get('reflection');
58
 
 
102
  {
103
  id: nanoid(),
104
  role: 'user',
105
+ content:
106
+ input + '\n\n' + generateInputImageMarkdown(mediaUrl),
107
  createdAt: new Date(),
108
  } satisfies Message,
109
  ]
110
  : []),
111
  newMessage,
112
  ]);
113
+ await dbPostCreateMessage(id, {
114
+ role: newMessage.role as 'user' | 'assistant',
115
+ content: newMessage.content,
116
+ });
117
  } else {
118
+ await dbPostCreateMessage(id, {
119
+ role: message.role as 'user' | 'assistant',
120
+ content: logs + CLEANED_SEPARATOR + content,
121
+ });
 
 
122
  }
123
  },
124
  initialMessages: initialMessages,
125
  body: {
126
+ mediaUrl,
127
  id,
128
  enableSelfReflection: reflectionValue === 'true',
129
  },
130
  });
131
 
132
+ /**
133
+ * If the last message is from the user, reload the chat, this would trigger to get the response from the assistant
134
+ * There are 2 scenarios when this might happen
135
+ * 1. Navigated from example images, init message only include preset user message
136
+ * 2. Last time the assistant message failed or not saved to database.
137
+ */
138
  useEffect(() => {
139
  if (
140
  !isLoading &&
 
145
  }
146
  }, [isLoading, messages, reload]);
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  const append: UseChatHelpers['append'] = async message => {
149
+ dbPostCreateMessage(id, {
150
+ role: message.role as 'user' | 'assistant',
151
+ content: message.content,
152
+ });
153
  return appendRaw(message);
154
  };
155
 
156
  return {
157
+ messages: messages as MessageBase[],
158
  append,
159
  reload,
160
  stop,
lib/kv/chat.ts CHANGED
@@ -1,118 +1,118 @@
1
- 'use server';
2
-
3
- import { revalidatePath } from 'next/cache';
4
- import { kv } from '@vercel/kv';
5
-
6
- import { auth, authEmail } from '@/auth';
7
- import { ChatEntity, MessageBase } from '@/lib/types';
8
- import { notFound, redirect } from 'next/navigation';
9
- import { nanoid } from '../utils';
10
-
11
- export async function getKVChats() {
12
- const { email } = await authEmail();
13
-
14
- try {
15
- const pipeline = kv.pipeline();
16
- const chats: string[] = await kv.zrange(`user:chat:${email}`, 0, -1, {
17
- rev: true,
18
- });
19
-
20
- for (const chat of chats) {
21
- pipeline.hgetall(chat);
22
- }
23
-
24
- const results = (await pipeline.exec()) as ChatEntity[];
25
-
26
- return results
27
- .filter(r => !!r)
28
- .sort((r1, r2) => r2.updatedAt - r1.updatedAt);
29
- } catch (error) {
30
- console.error('getKVChats error:', error);
31
- return [];
32
- }
33
- }
34
-
35
- export async function adminGetAllKVChats() {
36
- const { isAdmin } = await authEmail();
37
-
38
- if (!isAdmin) {
39
- notFound();
40
- }
41
-
42
- try {
43
- const pipeline = kv.pipeline();
44
- const chats: string[] = await kv.zrange(`user:chat:all`, 0, -1, {
45
- rev: true,
46
- });
47
-
48
- for (const chat of chats) {
49
- pipeline.hgetall(chat);
50
- }
51
-
52
- const results = (await pipeline.exec()) as ChatEntity[];
53
-
54
- return results.sort((r1, r2) => r2.updatedAt - r1.updatedAt);
55
- } catch (error) {
56
- return [];
57
- }
58
- }
59
-
60
- export async function getKVChat(id: string) {
61
- // const { email, isAdmin } = await authEmail();
62
- const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
63
-
64
- if (!chat) {
65
- redirect('/');
66
- }
67
-
68
- return chat;
69
- }
70
-
71
- export async function createKVChat(chat: ChatEntity) {
72
- // const { email, isAdmin } = await authEmail();
73
- const { email } = await authEmail();
74
-
75
- await kv.hmset(`chat:${chat.id}`, chat);
76
- if (email) {
77
- await kv.zadd(`user:chat:${email}`, {
78
- score: Date.now(),
79
- member: `chat:${chat.id}`,
80
- });
81
- }
82
- await kv.zadd('user:chat:all', {
83
- score: Date.now(),
84
- member: `chat:${chat.id}`,
85
- });
86
- revalidatePath('/chat', 'layout');
87
- }
88
-
89
- export async function saveKVChatMessage(id: string, message: MessageBase) {
90
- const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
91
- if (!chat) {
92
- notFound();
93
- }
94
- const { messages } = chat;
95
- await kv.hmset(`chat:${id}`, {
96
- ...chat,
97
- messages: [...messages, message],
98
- updatedAt: Date.now(),
99
- });
100
- return revalidatePath('/chat', 'layout');
101
- }
102
-
103
- export async function removeKVChat(id: string) {
104
- const { email } = await authEmail();
105
-
106
- if (!email) {
107
- return {
108
- error: 'Unauthorized',
109
- };
110
- }
111
-
112
- await Promise.all([
113
- kv.zrem(`user:chat:${email}`, `chat:${id}`),
114
- kv.del(`chat:${id}`),
115
- ]);
116
-
117
- return revalidatePath('/chat', 'layout');
118
- }
 
1
+ // 'use server';
2
+
3
+ // import { revalidatePath } from 'next/cache';
4
+ // import { kv } from '@vercel/kv';
5
+
6
+ // import { auth, sessionUser } from '@/auth';
7
+ // import { ChatEntity, MessageBase } from '@/lib/types';
8
+ // import { notFound, redirect } from 'next/navigation';
9
+ // import { nanoid } from '../utils';
10
+
11
+ // export async function getKVChats() {
12
+ // const { email } = await sessionUser();
13
+
14
+ // try {
15
+ // const pipeline = kv.pipeline();
16
+ // const chats: string[] = await kv.zrange(`user:chat:${email}`, 0, -1, {
17
+ // rev: true,
18
+ // });
19
+
20
+ // for (const chat of chats) {
21
+ // pipeline.hgetall(chat);
22
+ // }
23
+
24
+ // const results = (await pipeline.exec()) as ChatEntity[];
25
+
26
+ // return results
27
+ // .filter(r => !!r)
28
+ // .sort((r1, r2) => r2.updatedAt - r1.updatedAt);
29
+ // } catch (error) {
30
+ // console.error('getKVChats error:', error);
31
+ // return [];
32
+ // }
33
+ // }
34
+
35
+ // export async function adminGetAllKVChats() {
36
+ // const { isAdmin } = await sessionUser();
37
+
38
+ // if (!isAdmin) {
39
+ // notFound();
40
+ // }
41
+
42
+ // try {
43
+ // const pipeline = kv.pipeline();
44
+ // const chats: string[] = await kv.zrange(`user:chat:all`, 0, -1, {
45
+ // rev: true,
46
+ // });
47
+
48
+ // for (const chat of chats) {
49
+ // pipeline.hgetall(chat);
50
+ // }
51
+
52
+ // const results = (await pipeline.exec()) as ChatEntity[];
53
+
54
+ // return results.sort((r1, r2) => r2.updatedAt - r1.updatedAt);
55
+ // } catch (error) {
56
+ // return [];
57
+ // }
58
+ // }
59
+
60
+ // export async function getKVChat(id: string) {
61
+ // // const { email, isAdmin } = await sessionUser();
62
+ // const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
63
+
64
+ // if (!chat) {
65
+ // redirect('/');
66
+ // }
67
+
68
+ // return chat;
69
+ // }
70
+
71
+ // export async function createKVChat(chat: ChatEntity) {
72
+ // // const { email, isAdmin } = await sessionUser();
73
+ // const { email } = await sessionUser();
74
+
75
+ // await kv.hmset(`chat:${chat.id}`, chat);
76
+ // if (email) {
77
+ // await kv.zadd(`user:chat:${email}`, {
78
+ // score: Date.now(),
79
+ // member: `chat:${chat.id}`,
80
+ // });
81
+ // }
82
+ // await kv.zadd('user:chat:all', {
83
+ // score: Date.now(),
84
+ // member: `chat:${chat.id}`,
85
+ // });
86
+ // revalidatePath('/chat', 'layout');
87
+ // }
88
+
89
+ // export async function saveKVChatMessage(id: string, message: MessageBase) {
90
+ // const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
91
+ // if (!chat) {
92
+ // notFound();
93
+ // }
94
+ // const { messages } = chat;
95
+ // await kv.hmset(`chat:${id}`, {
96
+ // ...chat,
97
+ // messages: [...messages, message],
98
+ // updatedAt: Date.now(),
99
+ // });
100
+ // return revalidatePath('/chat', 'layout');
101
+ // }
102
+
103
+ // export async function removeKVChat(id: string) {
104
+ // const { email } = await sessionUser();
105
+
106
+ // if (!email) {
107
+ // return {
108
+ // error: 'Unauthorized',
109
+ // };
110
+ // }
111
+
112
+ // await Promise.all([
113
+ // kv.zrem(`user:chat:${email}`, `chat:${id}`),
114
+ // kv.del(`chat:${id}`),
115
+ // ]);
116
+
117
+ // return revalidatePath('/chat', 'layout');
118
+ // }
package.json CHANGED
@@ -2,6 +2,7 @@
2
  "private": true,
3
  "scripts": {
4
  "preinstall": "npx only-allow pnpm",
 
5
  "dev": "next dev --turbo",
6
  "build": "next build",
7
  "start": "next start",
@@ -16,6 +17,7 @@
16
  "@aws-sdk/client-s3": "^3.556.0",
17
  "@aws-sdk/credential-providers": "^3.556.0",
18
  "@aws-sdk/s3-presigned-post": "^3.556.0",
 
19
  "@radix-ui/react-dropdown-menu": "^2.0.6",
20
  "@radix-ui/react-label": "^2.0.2",
21
  "@radix-ui/react-select": "^2.0.0",
@@ -41,6 +43,7 @@
41
  "openai": "^4.24.7",
42
  "pino": "^9.0.0",
43
  "pino-loki": "^2.2.1",
 
44
  "react": "^18.2.0",
45
  "react-dom": "^18.2.0",
46
  "react-dropzone": "^14.2.3",
@@ -77,5 +80,5 @@
77
  "tailwindcss-animate": "^1.0.7",
78
  "typescript": "^5.3.3"
79
  },
80
- "packageManager": "pnpm@9.0.4"
81
- }
 
2
  "private": true,
3
  "scripts": {
4
  "preinstall": "npx only-allow pnpm",
5
+ "postinstall": "prisma generate",
6
  "dev": "next dev --turbo",
7
  "build": "next build",
8
  "start": "next start",
 
17
  "@aws-sdk/client-s3": "^3.556.0",
18
  "@aws-sdk/credential-providers": "^3.556.0",
19
  "@aws-sdk/s3-presigned-post": "^3.556.0",
20
+ "@prisma/client": "5.14.0",
21
  "@radix-ui/react-dropdown-menu": "^2.0.6",
22
  "@radix-ui/react-label": "^2.0.2",
23
  "@radix-ui/react-select": "^2.0.0",
 
43
  "openai": "^4.24.7",
44
  "pino": "^9.0.0",
45
  "pino-loki": "^2.2.1",
46
+ "prisma": "^5.14.0",
47
  "react": "^18.2.0",
48
  "react-dom": "^18.2.0",
49
  "react-dropzone": "^14.2.3",
 
80
  "tailwindcss-animate": "^1.0.7",
81
  "typescript": "^5.3.3"
82
  },
83
+ "packageManager": "pnpm@9.1.1"
84
+ }
pnpm-lock.yaml CHANGED
@@ -17,6 +17,9 @@ importers:
17
  '@aws-sdk/s3-presigned-post':
18
  specifier: ^3.556.0
19
  version: 3.556.0
 
 
 
20
  '@radix-ui/react-dropdown-menu':
21
  specifier: ^2.0.6
22
  version: 2.0.6(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -92,6 +95,9 @@ importers:
92
  pino-loki:
93
  specifier: ^2.2.1
94
  version: 2.2.1
 
 
 
95
  react:
96
  specifier: ^18.2.0
97
  version: 18.2.0
@@ -696,6 +702,30 @@ packages:
696
  resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
697
  engines: {node: '>=14'}
698
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  '@radix-ui/number@1.0.1':
700
  resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
701
 
@@ -3016,6 +3046,11 @@ packages:
3016
  pretty-format@3.8.0:
3017
  resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
3018
 
 
 
 
 
 
3019
  prismjs@1.27.0:
3020
  resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
3021
  engines: {node: '>=6'}
@@ -4562,6 +4597,31 @@ snapshots:
4562
  '@pkgjs/parseargs@0.11.0':
4563
  optional: true
4564
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4565
  '@radix-ui/number@1.0.1':
4566
  dependencies:
4567
  '@babel/runtime': 7.24.4
@@ -7362,6 +7422,10 @@ snapshots:
7362
 
7363
  pretty-format@3.8.0: {}
7364
 
 
 
 
 
7365
  prismjs@1.27.0: {}
7366
 
7367
  prismjs@1.29.0: {}
 
17
  '@aws-sdk/s3-presigned-post':
18
  specifier: ^3.556.0
19
  version: 3.556.0
20
+ '@prisma/client':
21
+ specifier: 5.14.0
22
+ version: 5.14.0(prisma@5.14.0)
23
  '@radix-ui/react-dropdown-menu':
24
  specifier: ^2.0.6
25
  version: 2.0.6(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
 
95
  pino-loki:
96
  specifier: ^2.2.1
97
  version: 2.2.1
98
+ prisma:
99
+ specifier: ^5.14.0
100
+ version: 5.14.0
101
  react:
102
  specifier: ^18.2.0
103
  version: 18.2.0
 
702
  resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
703
  engines: {node: '>=14'}
704
 
705
+ '@prisma/client@5.14.0':
706
+ resolution: {integrity: sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==}
707
+ engines: {node: '>=16.13'}
708
+ peerDependencies:
709
+ prisma: '*'
710
+ peerDependenciesMeta:
711
+ prisma:
712
+ optional: true
713
+
714
+ '@prisma/debug@5.14.0':
715
+ resolution: {integrity: sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==}
716
+
717
+ '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48':
718
+ resolution: {integrity: sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==}
719
+
720
+ '@prisma/engines@5.14.0':
721
+ resolution: {integrity: sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==}
722
+
723
+ '@prisma/fetch-engine@5.14.0':
724
+ resolution: {integrity: sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==}
725
+
726
+ '@prisma/get-platform@5.14.0':
727
+ resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==}
728
+
729
  '@radix-ui/number@1.0.1':
730
  resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
731
 
 
3046
  pretty-format@3.8.0:
3047
  resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
3048
 
3049
+ prisma@5.14.0:
3050
+ resolution: {integrity: sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==}
3051
+ engines: {node: '>=16.13'}
3052
+ hasBin: true
3053
+
3054
  prismjs@1.27.0:
3055
  resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
3056
  engines: {node: '>=6'}
 
4597
  '@pkgjs/parseargs@0.11.0':
4598
  optional: true
4599
 
4600
+ '@prisma/client@5.14.0(prisma@5.14.0)':
4601
+ optionalDependencies:
4602
+ prisma: 5.14.0
4603
+
4604
+ '@prisma/debug@5.14.0': {}
4605
+
4606
+ '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {}
4607
+
4608
+ '@prisma/engines@5.14.0':
4609
+ dependencies:
4610
+ '@prisma/debug': 5.14.0
4611
+ '@prisma/engines-version': 5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48
4612
+ '@prisma/fetch-engine': 5.14.0
4613
+ '@prisma/get-platform': 5.14.0
4614
+
4615
+ '@prisma/fetch-engine@5.14.0':
4616
+ dependencies:
4617
+ '@prisma/debug': 5.14.0
4618
+ '@prisma/engines-version': 5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48
4619
+ '@prisma/get-platform': 5.14.0
4620
+
4621
+ '@prisma/get-platform@5.14.0':
4622
+ dependencies:
4623
+ '@prisma/debug': 5.14.0
4624
+
4625
  '@radix-ui/number@1.0.1':
4626
  dependencies:
4627
  '@babel/runtime': 7.24.4
 
7422
 
7423
  pretty-format@3.8.0: {}
7424
 
7425
+ prisma@5.14.0:
7426
+ dependencies:
7427
+ '@prisma/engines': 5.14.0
7428
+
7429
  prismjs@1.27.0: {}
7430
 
7431
  prismjs@1.29.0: {}
prisma/schema.prisma ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5
+ // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
+
7
+ generator client {
8
+ provider = "prisma-client-js"
9
+ }
10
+
11
+ datasource db {
12
+ provider = "postgresql"
13
+ url = env("POSTGRES_PRISMA_URL") // uses connection pooling
14
+ directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
15
+ }
16
+
17
+ model User {
18
+ id String @id @default(cuid())
19
+ name String
20
+ email String @unique
21
+ createdAt DateTime @default(now()) @map(name: "created_at")
22
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
23
+ chats Chat[]
24
+ message Message[]
25
+
26
+ @@map("user")
27
+ }
28
+
29
+ model Chat {
30
+ id String @id @default(cuid())
31
+ mediaUrl String
32
+ createdAt DateTime @default(now()) @map(name: "created_at")
33
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
34
+ user User? @relation(fields: [userId], references: [id])
35
+ userId String?
36
+ messages Message[]
37
+
38
+ @@map("chat")
39
+ }
40
+
41
+ model Message {
42
+ id String @id @default(cuid())
43
+ role MessageRole
44
+ content String
45
+ createdAt DateTime @default(now()) @map(name: "created_at")
46
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
47
+ user User? @relation(fields: [userId], references: [id])
48
+ userId String?
49
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
50
+ chatId String
51
+
52
+ @@map("message")
53
+ }
54
+
55
+ enum MessageRole {
56
+ system
57
+ user
58
+ assistant
59
+ }