MingruiZhang commited on
Commit
a1c5622
·
unverified ·
1 Parent(s): 11e3c5e

feat: DB change - combine user and assistant message (#70)

Browse files
app/api/chat/route.ts CHANGED
@@ -6,14 +6,14 @@ import {
6
  ChatCompletionMessageParam,
7
  ChatCompletionContentPart,
8
  } from 'openai/resources';
9
- import { MessageBase } from '../../../lib/types';
10
 
11
  export const runtime = 'edge';
12
 
13
  export const POST = async (req: Request) => {
14
  const json = await req.json();
15
  const { messages } = json as {
16
- messages: MessageBase[];
17
  id: string;
18
  url: string;
19
  };
 
6
  ChatCompletionMessageParam,
7
  ChatCompletionContentPart,
8
  } from 'openai/resources';
9
+ import { MessageUI } from '@/lib/types';
10
 
11
  export const runtime = 'edge';
12
 
13
  export const POST = async (req: Request) => {
14
  const json = await req.json();
15
  const { messages } = json as {
16
+ messages: MessageUI[];
17
  id: string;
18
  url: string;
19
  };
app/api/vision-agent/route.ts CHANGED
@@ -1,10 +1,10 @@
1
  import { StreamingTextResponse } from 'ai';
2
 
3
  // import { auth } from '@/auth';
4
- import { MessageBase, SignedPayload } from '../../../lib/types';
5
  import { logger, withLogging } from '@/lib/logger';
6
  import { CLEANED_SEPARATOR } from '@/lib/constants';
7
- import { cleanAnswerMessage, cleanInputMessage } from '@/lib/messageUtils';
8
  import { fetcher } from '@/lib/utils';
9
 
10
  // export const runtime = 'edge';
@@ -53,7 +53,7 @@ export const POST = withLogging(
53
  async (
54
  session,
55
  json: {
56
- messages: MessageBase[];
57
  id: string;
58
  mediaUrl: string;
59
  },
 
1
  import { StreamingTextResponse } from 'ai';
2
 
3
  // import { auth } from '@/auth';
4
+ import { MessageUI, SignedPayload } from '@/lib/types';
5
  import { logger, withLogging } from '@/lib/logger';
6
  import { CLEANED_SEPARATOR } from '@/lib/constants';
7
+ import { cleanAnswerMessage, cleanInputMessage } from '@/lib/utils/content';
8
  import { fetcher } from '@/lib/utils';
9
 
10
  // export const runtime = 'edge';
 
53
  async (
54
  session,
55
  json: {
56
+ messages: MessageUI[];
57
  id: string;
58
  mediaUrl: string;
59
  },
app/chat/layout.tsx CHANGED
@@ -1,38 +1,7 @@
1
- // import { sessionUser } from '@/auth';
2
- // import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
3
- // import Loading from '@/components/ui/Loading';
4
- // import { dbGetMyChatListWithMessages } from '@/lib/db/functions';
5
- // import { Suspense } from 'react';
6
-
7
  interface ChatLayoutProps {
8
  children: React.ReactNode;
9
  }
10
 
11
- // export default async function Layout({ children }: ChatLayoutProps) {
12
- // const { email, user, id } = await sessionUser();
13
- // const chats = await dbGetMyChatListWithMessages();
14
-
15
- // return (
16
- // <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
17
- // {user && (
18
- // <div
19
- // data-state={email ? 'open' : 'closed'}
20
- // className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out -translate-x-full data-[state=open]:translate-x-0 lg:flex lg:w-[250px] h-full flex-col overflow-auto py-2"
21
- // >
22
- // <Suspense fallback={<Loading />}>
23
- // <ChatSidebarList chats={chats} />
24
- // </Suspense>
25
- // </div>
26
- // )}
27
- // <Suspense fallback={<Loading />}>
28
- // <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px]">
29
- // {children}
30
- // </div>
31
- // </Suspense>
32
- // </div>
33
- // );
34
- // }
35
-
36
  export default async function Layout({ children }: ChatLayoutProps) {
37
  // return <Suspense fallback={<Loading />}>{children}</Suspense>;
38
  return children;
 
 
 
 
 
 
 
1
  interface ChatLayoutProps {
2
  children: React.ReactNode;
3
  }
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  export default async function Layout({ children }: ChatLayoutProps) {
6
  // return <Suspense fallback={<Loading />}>{children}</Suspense>;
7
  return children;
app/page.tsx CHANGED
@@ -1,15 +1,13 @@
1
  'use client';
2
 
3
- import { generateInputImageMarkdown } from '@/lib/messageUtils';
4
  import { useRouter } from 'next/navigation';
5
 
6
- import { MessageRaw } from '@/lib/db/types';
7
  import { useRef, useState } from 'react';
8
  import Composer, { ComposerRef } from '@/components/chat/Composer';
9
  import { dbPostCreateChat } from '@/lib/db/functions';
10
  import { nanoid } from '@/lib/utils';
11
  import Chip from '@/components/ui/Chip';
12
- import { IconArrowUpRight, IconImage } from '@/components/ui/Icons';
13
 
14
  const EXAMPLES = [
15
  {
@@ -18,12 +16,6 @@ const EXAMPLES = [
18
  'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png',
19
  prompt: 'Count the number of flowers in this image.',
20
  },
21
- // {
22
- // heading: 'Detecting',
23
- // url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
24
- // subheading: 'number of cereals in an image',
25
- // message: `How many cereals are there in the image?`,
26
- // },
27
  ];
28
 
29
  export default function Page() {
@@ -46,19 +38,12 @@ export default function Page() {
46
  const newId = nanoid();
47
  const resp = await dbPostCreateChat({
48
  id: newId,
49
- mediaUrl: mediaUrl,
50
  title: `conversation-${newId}`,
51
- initMessages: [
52
- {
53
- role: 'user',
54
- content:
55
- input +
56
- (mediaUrl
57
- ? '\n\n' + generateInputImageMarkdown(mediaUrl)
58
- : ''),
59
- result: null,
60
- },
61
- ],
62
  });
63
  if (resp) {
64
  router.push(`/chat/${newId}`);
@@ -66,25 +51,23 @@ export default function Page() {
66
  }}
67
  />
68
  </div>
69
- <>
70
- {EXAMPLES.map((example, index) => {
71
- return (
72
- <Chip
73
- key={index}
74
- className="bg-transparent border border-zinc-500 cursor-pointer px-4"
75
- onClick={() => {
76
- composerRef.current?.setInput(example.prompt);
77
- composerRef.current?.setMediaUrl(example.mediaUrl);
78
- }}
79
- >
80
- <div className="flex flex-row items-center space-x-2">
81
- <p className="text-primary text-sm">{example.title}</p>
82
- <IconArrowUpRight className="text-primary" />
83
- </div>
84
- </Chip>
85
- );
86
- })}
87
- </>
88
  </div>
89
  </div>
90
  );
 
1
  'use client';
2
 
 
3
  import { useRouter } from 'next/navigation';
4
 
 
5
  import { useRef, useState } from 'react';
6
  import Composer, { ComposerRef } from '@/components/chat/Composer';
7
  import { dbPostCreateChat } from '@/lib/db/functions';
8
  import { nanoid } from '@/lib/utils';
9
  import Chip from '@/components/ui/Chip';
10
+ import { IconArrowUpRight } from '@/components/ui/Icons';
11
 
12
  const EXAMPLES = [
13
  {
 
16
  'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png',
17
  prompt: 'Count the number of flowers in this image.',
18
  },
 
 
 
 
 
 
19
  ];
20
 
21
  export default function Page() {
 
38
  const newId = nanoid();
39
  const resp = await dbPostCreateChat({
40
  id: newId,
 
41
  title: `conversation-${newId}`,
42
+ mediaUrl,
43
+ message: {
44
+ prompt: input,
45
+ mediaUrl,
46
+ },
 
 
 
 
 
 
47
  });
48
  if (resp) {
49
  router.push(`/chat/${newId}`);
 
51
  }}
52
  />
53
  </div>
54
+ {EXAMPLES.map((example, index) => {
55
+ return (
56
+ <Chip
57
+ key={index}
58
+ className="bg-transparent border border-zinc-500 cursor-pointer px-4"
59
+ onClick={() => {
60
+ composerRef.current?.setInput(example.prompt);
61
+ composerRef.current?.setMediaUrl(example.mediaUrl);
62
+ }}
63
+ >
64
+ <div className="flex flex-row items-center space-x-2">
65
+ <p className="text-primary text-sm">{example.title}</p>
66
+ <IconArrowUpRight className="text-primary" />
67
+ </div>
68
+ </Chip>
69
+ );
70
+ })}
 
 
71
  </div>
72
  </div>
73
  );
auth.ts CHANGED
@@ -46,19 +46,21 @@ export const {
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;
55
  }
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
  },
 
46
  return false;
47
  },
48
  async jwt({ token, profile, user }) {
 
 
 
49
  if (profile) {
50
  token.id = profile.id || profile.sub;
51
  token.image = profile.avatar_url || profile.picture;
52
  }
53
  return token;
54
  },
55
+ async session({ session, token }) {
56
+ // TODO: this is temporary between we switch DB and make migration
57
+ // so also UI might still have session, DB might already have cleaned up
58
+ const email = session?.user?.email;
59
+ const name = session?.user?.name;
60
+ if (email && name) {
61
+ const dbUser = await dbFindOrCreateUser(email, name);
62
  // put db user id into session
63
+ session.user.id = dbUser.id;
64
  }
65
  return session;
66
  },
components/chat-sidebar/ChatAdminToggle.tsx DELETED
@@ -1,14 +0,0 @@
1
- 'use client';
2
-
3
- import { chatViewMode } from '@/state/chat';
4
- import { useAtom } from 'jotai';
5
- import React from 'react';
6
-
7
- export interface ChatAdminToggleProps {}
8
-
9
- const ChatAdminToggle: React.FC<ChatAdminToggleProps> = () => {
10
- const modeAtom = useAtom(chatViewMode);
11
- return null;
12
- };
13
-
14
- export default ChatAdminToggle;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/chat-sidebar/ChatCard.tsx DELETED
@@ -1,75 +0,0 @@
1
- 'use client';
2
-
3
- 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 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
-
21
- export const ChatCardLayout: React.FC<
22
- PropsWithChildren<{ link: string; classNames?: clsx.ClassValue }>
23
- > = ({ link, children, classNames }) => {
24
- return (
25
- <Link
26
- className={cn(
27
- 'p-2 bg-background max-h-[100px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer w-full',
28
- classNames,
29
- )}
30
- href={link}
31
- >
32
- {children}
33
- </Link>
34
- );
35
- };
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
- }
43
- const firstMessage = cleanInputMessage(messages?.[0]?.content ?? '');
44
- const title = firstMessage
45
- ? firstMessage.length > 50
46
- ? firstMessage.slice(0, 50) + '...'
47
- : firstMessage
48
- : '(No messages yet)';
49
- return (
50
- <ChatCardLayout
51
- link={isAdminView ? `/all/chat/${id}` : `/chat/${id}`}
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
- />
69
- </div>
70
- </div>
71
- </ChatCardLayout>
72
- );
73
- };
74
-
75
- export default ChatCard;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/chat-sidebar/ChatListSidebar.tsx DELETED
@@ -1,69 +0,0 @@
1
- 'use client';
2
-
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
-
16
- const getItemSize = (message: string, isAdminView?: boolean) => {
17
- if (message.length >= 45) return 116;
18
- else if (message.length >= 20) return 104;
19
- else return 88;
20
- };
21
-
22
- export default function ChatSidebarList({
23
- chats,
24
- isAdminView,
25
- }: ChatSidebarListProps) {
26
- return (
27
- <>
28
- {!isAdminView && (
29
- <div className="p-2">
30
- <ChatCardLayout link="/chat">
31
- <div className="overflow-hidden flex items-center size-full">
32
- <IconPlus className="w-1/4 font-bold" />
33
- <p className="text-sm w-3/4 ml-3 font-bold">New chat</p>
34
- </div>
35
- </ChatCardLayout>
36
- </div>
37
- )}
38
- <AutoSizer>
39
- {({ height, width }) => (
40
- <List
41
- itemData={chats}
42
- height={height}
43
- itemCount={chats.length}
44
- itemSize={index =>
45
- getItemSize(
46
- cleanInputMessage(chats[index].messages?.[0]?.content ?? ''),
47
- isAdminView,
48
- )
49
- }
50
- width={width}
51
- >
52
- {({ style, index, data }) => (
53
- <div
54
- style={style}
55
- className="px-2 flex items-center overflow-hidden"
56
- >
57
- <ChatCard
58
- key={data[index].id}
59
- chat={data[index]}
60
- isAdminView={isAdminView}
61
- />
62
- </div>
63
- )}
64
- </List>
65
- )}
66
- </AutoSizer>
67
- </>
68
- );
69
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/chat/ChatClient.tsx CHANGED
@@ -6,12 +6,12 @@ import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
  import { Session } from 'next-auth';
8
  import { useEffect } from 'react';
9
- import { ChatWithMessages } from '@/lib/db/types';
10
  import { ChatMessage } from './ChatMessage';
11
  import { Button } from '../ui/Button';
12
  import { cn } from '@/lib/utils';
13
  import { IconArrowDown } from '../ui/Icons';
14
- import { generateInputImageMarkdown } from '@/lib/messageUtils';
15
 
16
  export interface ChatClientProps {
17
  chat: ChatWithMessages;
@@ -20,8 +20,8 @@ export interface ChatClientProps {
20
  export const SCROLL_BOTTOM = 120;
21
 
22
  const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
23
- const { mediaUrl, id } = chat;
24
- const { messages, append, isLoading, reload } = useVisionAgent(chat);
25
 
26
  const { messagesRef, scrollRef, visibilityRef, isVisible, scrollToBottom } =
27
  useScrollAnchor(SCROLL_BOTTOM);
@@ -56,17 +56,13 @@ const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
56
  </div>
57
  <div className="absolute bottom-4 w-full">
58
  <Composer
59
- initMediaUrl={mediaUrl}
 
60
  isLoading={isLoading}
61
  onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
62
  append({
63
- id,
64
- content:
65
- input +
66
- (newMediaUrl
67
- ? '\n\n' + generateInputImageMarkdown(newMediaUrl)
68
- : ''),
69
- role: 'user',
70
  });
71
  }}
72
  />
 
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
  import { Session } from 'next-auth';
8
  import { useEffect } from 'react';
9
+ import { ChatWithMessages, MessageUserInput } from '@/lib/types';
10
  import { ChatMessage } from './ChatMessage';
11
  import { Button } from '../ui/Button';
12
  import { cn } from '@/lib/utils';
13
  import { IconArrowDown } from '../ui/Icons';
14
+ import { dbPostCreateMessage } from '@/lib/db/functions';
15
 
16
  export interface ChatClientProps {
17
  chat: ChatWithMessages;
 
20
  export const SCROLL_BOTTOM = 120;
21
 
22
  const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
23
+ const { id, messages: dbMessages } = chat;
24
+ const { messages, append, isLoading } = useVisionAgent(chat);
25
 
26
  const { messagesRef, scrollRef, visibilityRef, isVisible, scrollToBottom } =
27
  useScrollAnchor(SCROLL_BOTTOM);
 
56
  </div>
57
  <div className="absolute bottom-4 w-full">
58
  <Composer
59
+ // Use the last message mediaUrl as the initial mediaUrl
60
+ initMediaUrl={dbMessages[dbMessages.length - 1]?.mediaUrl}
61
  isLoading={isLoading}
62
  onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
63
  append({
64
+ prompt: input,
65
+ mediaUrl: newMediaUrl,
 
 
 
 
 
66
  });
67
  }}
68
  />
components/chat/ChatList.tsx CHANGED
@@ -2,13 +2,13 @@
2
 
3
  import { Separator } from '@/components/ui/Separator';
4
  import { ChatMessage } from '@/components/chat/ChatMessage';
5
- import { MessageBase } from '../../lib/types';
6
  import { Session } from 'next-auth';
7
  import { IconExclamationTriangle } from '../ui/Icons';
8
  import Link from 'next/link';
9
 
10
  export interface ChatList {
11
- messages: MessageBase[];
12
  session: Session | null;
13
  isLoading: boolean;
14
  }
 
2
 
3
  import { Separator } from '@/components/ui/Separator';
4
  import { ChatMessage } from '@/components/chat/ChatMessage';
5
+ import { MessageUI } from '@/lib/types';
6
  import { Session } from 'next-auth';
7
  import { IconExclamationTriangle } from '../ui/Icons';
8
  import Link from 'next/link';
9
 
10
  export interface ChatList {
11
+ messages: MessageUI[];
12
  session: Session | null;
13
  isLoading: boolean;
14
  }
components/chat/ChatMessage.tsx CHANGED
@@ -21,9 +21,9 @@ import {
21
  IconOutput,
22
  IconLog,
23
  } from '@/components/ui/Icons';
24
- import { MessageBase } from '../../lib/types';
25
  import Img from '../ui/Img';
26
- import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/messageUtils';
27
  import {
28
  Table,
29
  TableBody,
@@ -43,7 +43,7 @@ import {
43
  import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
44
 
45
  export interface ChatMessageProps {
46
- message: MessageBase;
47
  isLoading: boolean;
48
  }
49
 
@@ -194,7 +194,7 @@ const ChunkPayloadAction: React.FC<{
194
  </Button>
195
  </DialogTrigger>
196
  <DialogContent className="max-w-5xl">
197
- <Table className="border rounded-lg bg-zinc-700 overflow-hidden">
198
  <TableHeader>
199
  <TableRow className="border-primary/50">
200
  {keyArray.map(header => (
@@ -331,7 +331,7 @@ const AssistantChatMessage: React.FC<{
331
  <IconLandingAI />
332
  </div>
333
  <div className="flex-1 px-1 space-y-4 ml-4 overflow-hidden">
334
- <Table className="border rounded-lg bg-zinc-700 overflow-hidden w-[400px]">
335
  <TableBody>
336
  {formattedSections.map(section => (
337
  <TableRow className="border-primary/50" key={section.type}>
 
21
  IconOutput,
22
  IconLog,
23
  } from '@/components/ui/Icons';
24
+ import { MessageUI } from '@/lib/types';
25
  import Img from '../ui/Img';
26
+ import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/utils/content';
27
  import {
28
  Table,
29
  TableBody,
 
43
  import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
44
 
45
  export interface ChatMessageProps {
46
+ message: MessageUI;
47
  isLoading: boolean;
48
  }
49
 
 
194
  </Button>
195
  </DialogTrigger>
196
  <DialogContent className="max-w-5xl">
197
+ <Table>
198
  <TableHeader>
199
  <TableRow className="border-primary/50">
200
  {keyArray.map(header => (
 
331
  <IconLandingAI />
332
  </div>
333
  <div className="flex-1 px-1 space-y-4 ml-4 overflow-hidden">
334
+ <Table className="w-[400px]">
335
  <TableBody>
336
  {formattedSections.map(section => (
337
  <TableRow className="border-primary/50" key={section.type}>
components/chat/ChatMessageActions.tsx CHANGED
@@ -6,10 +6,10 @@ import { Button } from '@/components/ui/Button';
6
  import { IconCheck, IconCopy } from '@/components/ui/Icons';
7
  import { useCopyToClipboard } from '@/lib/hooks/useCopyToClipboard';
8
  import { cn } from '@/lib/utils';
9
- import { MessageBase } from '../../lib/types';
10
 
11
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
- message: MessageBase;
13
  }
14
 
15
  export function ChatMessageActions({
 
6
  import { IconCheck, IconCopy } from '@/components/ui/Icons';
7
  import { useCopyToClipboard } from '@/lib/hooks/useCopyToClipboard';
8
  import { cn } from '@/lib/utils';
9
+ import { MessageUI } from '../../lib/types';
10
 
11
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
+ message: MessageUI;
13
  }
14
 
15
  export function ChatMessageActions({
components/ui/Table.tsx CHANGED
@@ -9,7 +9,10 @@ const Table = React.forwardRef<
9
  <div className="relative w-full overflow-auto">
10
  <table
11
  ref={ref}
12
- className={cn('w-full caption-bottom text-sm', className)}
 
 
 
13
  {...props}
14
  />
15
  </div>
@@ -87,7 +90,7 @@ const TableCell = React.forwardRef<
87
  >(({ className, ...props }, ref) => (
88
  <td
89
  ref={ref}
90
- className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
91
  {...props}
92
  />
93
  ));
 
9
  <div className="relative w-full overflow-auto">
10
  <table
11
  ref={ref}
12
+ className={cn(
13
+ 'w-full caption-bottom text-sm border rounded-lg bg-zinc-700 overflow-hidden',
14
+ className,
15
+ )}
16
  {...props}
17
  />
18
  </div>
 
90
  >(({ className, ...props }, ref) => (
91
  <td
92
  ref={ref}
93
+ className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0', className)}
94
  {...props}
95
  />
96
  ));
lib/db/functions.ts CHANGED
@@ -2,10 +2,25 @@
2
 
3
  import { sessionUser } from '@/auth';
4
  import prisma from './prisma';
5
- import { ChatWithMessages, MessageRaw } from './types';
 
 
 
 
6
  import { revalidatePath } from 'next/cache';
7
  import { Chat } from '@prisma/client';
8
 
 
 
 
 
 
 
 
 
 
 
 
9
  /**
10
  * Finds or creates a user in the database based on the provided email and name.
11
  * If the user already exists, it returns the existing user.
@@ -48,25 +63,6 @@ export async function dbGetMyChatList(): Promise<Chat[]> {
48
  });
49
  }
50
 
51
- /**
52
- * Retrieves all chats with their associated messages for the current user.
53
- * @returns A promise that resolves to an array of `ChatWithMessages` objects.
54
- */
55
- export async function dbGetMyChatListWithMessages(): Promise<
56
- ChatWithMessages[]
57
- > {
58
- const { id: userId } = await sessionUser();
59
-
60
- if (!userId) return [];
61
-
62
- return prisma.chat.findMany({
63
- where: { userId },
64
- include: {
65
- messages: true,
66
- },
67
- });
68
- }
69
-
70
  /**
71
  * Retrieves a chat with messages from the database based on the provided ID.
72
  * @param id - The ID of the chat to retrieve.
@@ -82,45 +78,34 @@ export async function dbGetChat(id: string): Promise<ChatWithMessages | null> {
82
  }
83
 
84
  /**
85
- * Creates a new chat in the database.
86
- *
87
  * @param {string} options.id - The ID of the chat (optional).
88
- * @param {string} options.mediaUrl - The media URL for the chat.
89
- * @param {MessageRaw[]} options.initMessages - The initial messages for the chat (optional).
90
- * @returns {Promise<Chat>} The created chat object.
91
  */
92
  export async function dbPostCreateChat({
93
  id,
94
- mediaUrl,
95
  title,
96
- initMessages = [],
 
97
  }: {
98
  id?: string;
99
- mediaUrl: string;
100
  title?: string;
101
- initMessages?: MessageRaw[];
 
102
  }) {
103
- const { id: userId } = await sessionUser();
104
- const userConnect = userId
105
- ? {
106
- user: {
107
- connect: { id: userId }, // Connect the chat to an existing user
108
- },
109
- }
110
- : {};
111
  try {
112
  const response = await prisma.chat.create({
113
  data: {
114
  id,
115
- mediaUrl: mediaUrl,
116
  ...userConnect,
 
117
  title,
118
  messages: {
119
- create: initMessages.map(message => ({
120
- ...message,
121
- ...userConnect,
122
- result: undefined,
123
- })),
124
  },
125
  },
126
  include: {
@@ -136,29 +121,46 @@ export async function dbPostCreateChat({
136
  }
137
 
138
  /**
139
- * Creates a new message in the database.
140
- * @param chatId - The ID of the chat where the message belongs.
141
- * @param message - The message object to be created.
142
- * @returns A promise that resolves to the created message.
143
  */
144
- export async function dbPostCreateMessage(chatId: string, message: MessageRaw) {
145
- const { id: userId } = await sessionUser();
146
- const userConnect = userId
147
- ? {
148
- user: {
149
- connect: { id: userId }, // Connect the chat to an existing user
150
- },
151
- }
152
- : {};
153
 
154
- await prisma.message.create({
155
  data: {
156
- content: message.content,
157
- role: message.role,
158
  chat: {
159
  connect: { id: chatId },
160
  },
161
- ...userConnect,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  },
163
  });
164
 
 
2
 
3
  import { sessionUser } from '@/auth';
4
  import prisma from './prisma';
5
+ import {
6
+ ChatWithMessages,
7
+ MessageAssistantResponse,
8
+ MessageUserInput,
9
+ } from '../types';
10
  import { revalidatePath } from 'next/cache';
11
  import { Chat } from '@prisma/client';
12
 
13
+ async function getUserConnect() {
14
+ const { id: userId } = await sessionUser();
15
+ return userId
16
+ ? {
17
+ user: {
18
+ connect: { id: userId }, // Connect the chat to an existing user
19
+ },
20
+ }
21
+ : {};
22
+ }
23
+
24
  /**
25
  * Finds or creates a user in the database based on the provided email and name.
26
  * If the user already exists, it returns the existing user.
 
63
  });
64
  }
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  /**
67
  * Retrieves a chat with messages from the database based on the provided ID.
68
  * @param id - The ID of the chat to retrieve.
 
78
  }
79
 
80
  /**
81
+ * Creates a new chat in the database with the provided information.
82
+ * @param {Object} options - The options for creating the chat.
83
  * @param {string} options.id - The ID of the chat (optional).
84
+ * @param {string} options.title - The title of the chat (optional).
85
+ * @param {MessageUserInput} options.message - The message to be added to the chat.
86
+ * @returns {Promise<Chat>} - A promise that resolves to the created chat.
87
  */
88
  export async function dbPostCreateChat({
89
  id,
 
90
  title,
91
+ mediaUrl,
92
+ message,
93
  }: {
94
  id?: string;
 
95
  title?: string;
96
+ mediaUrl: string;
97
+ message: MessageUserInput;
98
  }) {
99
+ const userConnect = await getUserConnect();
 
 
 
 
 
 
 
100
  try {
101
  const response = await prisma.chat.create({
102
  data: {
103
  id,
 
104
  ...userConnect,
105
+ mediaUrl,
106
  title,
107
  messages: {
108
+ create: [{ ...message, ...userConnect }],
 
 
 
 
109
  },
110
  },
111
  include: {
 
121
  }
122
 
123
  /**
124
+ * Creates a new message in the database for a given chat.
125
+ * @param chatId - The ID of the chat where the message will be created.
126
+ * @param message - The message to be created.
 
127
  */
128
+ export async function dbPostCreateMessage(
129
+ chatId: string,
130
+ message: MessageUserInput,
131
+ ) {
132
+ const userConnect = await getUserConnect();
 
 
 
 
133
 
134
+ const resp = await prisma.message.create({
135
  data: {
136
+ ...message,
137
+ ...userConnect,
138
  chat: {
139
  connect: { id: chatId },
140
  },
141
+ },
142
+ });
143
+
144
+ revalidatePath('/chat');
145
+ return resp;
146
+ }
147
+
148
+ /**
149
+ * Updates a message in the database with the provided response.
150
+ * @param messageId - The ID of the message to update.
151
+ * @param messageResponse - The response to update the message with.
152
+ */
153
+ export async function dbPostUpdateMessageResponse(
154
+ messageId: string,
155
+ messageResponse: MessageAssistantResponse,
156
+ ) {
157
+ await prisma.message.update({
158
+ data: {
159
+ response: messageResponse.response,
160
+ result: messageResponse.result ?? undefined,
161
+ },
162
+ where: {
163
+ id: messageId,
164
  },
165
  });
166
 
lib/db/types.ts DELETED
@@ -1,10 +0,0 @@
1
- import { Chat, Message } from '@prisma/client';
2
-
3
- export type ChatWithMessages = Chat & { messages: Message[] };
4
- export type ChatMessage = Message;
5
-
6
- export type MessageRaw = {
7
- role: Message['role'];
8
- result: Message['result'];
9
- content: Message['content'];
10
- };
 
 
 
 
 
 
 
 
 
 
 
lib/hooks/useVisionAgent.ts CHANGED
@@ -1,24 +1,26 @@
1
- import { useChat, UseChatHelpers } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
- import { useEffect, useRef } from 'react';
4
- import { ChatWithMessages } from '../db/types';
5
- import { dbPostCreateMessage } from '../db/functions';
6
  import {
7
- convertDbMessageToMessage,
8
- convertMessageToDbMessage,
9
- } from '../messageUtils';
 
 
 
 
10
 
11
  const useVisionAgent = (chat: ChatWithMessages) => {
12
- const { messages: initialMessages, id, mediaUrl } = chat;
 
13
 
14
- const {
15
- messages,
16
- append: appendRaw,
17
- isLoading,
18
- reload,
19
- } = useChat({
20
  api: '/api/vision-agent',
21
- // @ts-ignore https://sdk.vercel.ai/docs/troubleshooting/common-issues/use-chat-failed-to-parse-stream
22
  streamMode: 'text',
23
  onResponse(response) {
24
  if (response.status !== 200) {
@@ -26,11 +28,15 @@ const useVisionAgent = (chat: ChatWithMessages) => {
26
  }
27
  },
28
  onFinish: async message => {
29
- await dbPostCreateMessage(id, convertMessageToDbMessage(message));
 
 
 
30
  },
31
- initialMessages: initialMessages.map(convertDbMessageToMessage),
 
32
  body: {
33
- mediaUrl,
34
  id,
35
  },
36
  onError: err => {
@@ -54,18 +60,18 @@ const useVisionAgent = (chat: ChatWithMessages) => {
54
  }
55
  }, [isLoading, messages, reload]);
56
 
57
- const append: UseChatHelpers['append'] = async message => {
58
- dbPostCreateMessage(id, {
59
- role: message.role as 'user' | 'assistant',
60
- content: message.content,
61
- result: null,
62
- });
63
- return appendRaw(message);
64
- };
65
-
66
  return {
67
  messages,
68
- append,
 
 
 
 
 
 
 
 
 
69
  reload,
70
  isLoading,
71
  };
 
1
+ import { useChat } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { ChatWithMessages, MessageUserInput } from '../types';
 
5
  import {
6
+ dbPostCreateMessage,
7
+ dbPostUpdateMessageResponse,
8
+ } from '../db/functions';
9
+ import {
10
+ convertAssistantUIMessageToDBMessageResponse,
11
+ convertDBMessageToUIMessage,
12
+ } from '../utils/message';
13
 
14
  const useVisionAgent = (chat: ChatWithMessages) => {
15
+ const { messages: dbMessages, id, mediaUrl } = chat;
16
+ const latestDbMessage = dbMessages[dbMessages.length - 1];
17
 
18
+ // Temporary solution for now while single we have to pass mediaUrl separately outside of the messages
19
+ const currMediaUrl = useRef<string>(mediaUrl);
20
+ const currMessageId = useRef<string>(latestDbMessage?.id);
21
+
22
+ const { messages, append, isLoading, reload } = useChat({
 
23
  api: '/api/vision-agent',
 
24
  streamMode: 'text',
25
  onResponse(response) {
26
  if (response.status !== 200) {
 
28
  }
29
  },
30
  onFinish: async message => {
31
+ await dbPostUpdateMessageResponse(
32
+ currMessageId.current,
33
+ convertAssistantUIMessageToDBMessageResponse(message),
34
+ );
35
  },
36
+ sendExtraMessageFields: true,
37
+ initialMessages: convertDBMessageToUIMessage(dbMessages),
38
  body: {
39
+ mediaUrl: currMediaUrl.current,
40
  id,
41
  },
42
  onError: err => {
 
60
  }
61
  }, [isLoading, messages, reload]);
62
 
 
 
 
 
 
 
 
 
 
63
  return {
64
  messages,
65
+ append: async (messageInput: MessageUserInput) => {
66
+ currMediaUrl.current = messageInput.mediaUrl;
67
+ append({
68
+ id,
69
+ role: 'user',
70
+ content: messageInput.prompt,
71
+ });
72
+ const resp = await dbPostCreateMessage(id, messageInput);
73
+ currMessageId.current = resp.id;
74
+ },
75
  reload,
76
  isLoading,
77
  };
lib/types.ts CHANGED
@@ -1,10 +1,14 @@
1
- import { type Message } from 'ai';
 
2
 
3
- export type MessageBase = {
4
- role: Message['role'];
5
- content: string;
6
- id: string;
7
- };
 
 
 
8
 
9
  export interface SignedPayload {
10
  id: string;
 
1
+ import { Chat, Message } from '@prisma/client';
2
+ import { type Message as MessageAI } from 'ai';
3
 
4
+ export type ChatWithMessages = Chat & { messages: Message[] };
5
+
6
+ export type MessageUserInput = Pick<Message, 'prompt' | 'mediaUrl'>;
7
+ export type MessageAssistantResponse = Partial<
8
+ Pick<Message, 'response' | 'result'>
9
+ >;
10
+
11
+ export type MessageUI = Pick<MessageAI, 'role' | 'content' | 'id'>;
12
 
13
  export interface SignedPayload {
14
  id: string;
lib/{messageUtils.ts → utils/content.ts} RENAMED
@@ -1,5 +1,4 @@
1
  import toast from 'react-hot-toast';
2
- import type { ChatMessage } from '@/lib/db/types';
3
  import { Message } from 'ai';
4
 
5
  const PAIRS: Record<string, string> = {
@@ -13,22 +12,11 @@ const MIDDLE_STARTER = '┝';
13
  const MIDDLE_SEPARATOR = '┿';
14
 
15
  const ANSWERS_PREFIX = 'answers';
16
- const INPUT_PREFIX = 'input';
17
 
18
  export const generateAnswersImageMarkdown = (index: number, url: string) => {
19
  return `![${ANSWERS_PREFIX}-${index}](${url})`;
20
  };
21
 
22
- export const generateInputImageMarkdown = (url: string, index = 0) => {
23
- if (url.toLowerCase().endsWith('.mp4')) {
24
- const prefix = 'input-video';
25
- return `![${INPUT_PREFIX}-${index}](<${url}>)`;
26
- } else {
27
- const prefix = 'input';
28
- return `![${INPUT_PREFIX}-${index}](<${url}>)`;
29
- }
30
- };
31
-
32
  export const cleanInputMessage = (content: string) => {
33
  return content
34
  .replace(/!\[input-.*?\)/g, '')
@@ -244,100 +232,21 @@ const parseLine = (json: MessageBody) => {
244
  }
245
  };
246
 
247
- export const getFormattedMessage = ({
248
- content,
249
- role,
250
- }: Pick<ChatMessage, 'role' | 'content'>) => {
251
- if (role === 'user') {
252
- return {
253
- content,
254
- };
255
- }
256
- const lines = content.split('\n');
257
- let formattedLogs = '';
258
- let finalResult = null;
259
- const jsons: MessageBody[] = [];
260
- for (let line of lines) {
261
- console.log(line);
262
- if (!line.trim()) {
263
- continue;
264
- }
265
- try {
266
- const json = JSON.parse(line) as MessageBody;
267
- if (json.type === 'final_code') {
268
- const result = JSON.parse(
269
- json.payload.result as unknown as string,
270
- ) as PrismaJson.FinalChatResult['payload']['result'];
271
- finalResult = generateFinalCodeMarkdown(
272
- json.payload.code,
273
- json.payload.test,
274
- result,
275
- );
276
- } else if (
277
- jsons.length > 0 &&
278
- json.type === jsons[jsons.length - 1].type &&
279
- json.status !== 'started'
280
- ) {
281
- jsons[jsons.length - 1] = json;
282
- } else {
283
- jsons.push(json);
284
- }
285
- } catch (e) {
286
- console.error((e as Error).message);
287
- console.error(line);
288
- }
289
- }
290
- jsons.forEach(json => (formattedLogs += parseLine(json)));
291
- return {
292
- content: formattedLogs + (finalResult ?? ''),
293
- };
294
- };
295
-
296
- export const convertDbMessageToMessage = (message: ChatMessage): Message => {
297
- return {
298
- id: message.id,
299
- role: message.role,
300
- createdAt: message.createdAt,
301
- content: message.result
302
- ? message.content + '\n' + JSON.stringify(message.result)
303
- : message.content,
304
- };
305
- };
306
-
307
- export const convertMessageToDbMessage = (
308
- message: Message,
309
- ): Pick<ChatMessage, 'content' | 'result' | 'role'> => {
310
- let result = null;
311
- const lines = message.content.split('\n');
312
- for (let line of lines) {
313
- try {
314
- const json = JSON.parse(line) as MessageBody;
315
- if (json.type == 'final_code') {
316
- result = json as PrismaJson.FinalChatResult;
317
- break;
318
- }
319
- } catch (e) {
320
- console.error((e as Error).message);
321
- }
322
- }
323
- return {
324
- role: message.role as ChatMessage['role'],
325
- content: message.content,
326
- result,
327
- };
328
- };
329
-
330
  export type CodeResult = {
331
  code: string;
332
  test: string;
333
  result: string;
334
  };
335
 
336
- export type ChunkBody = {
337
- type: 'plans' | 'tools' | 'code' | 'final_code';
338
- status: 'started' | 'completed' | 'failed' | 'running';
339
- payload: Array<Record<string, string>> | CodeResult;
340
- };
 
 
 
 
341
 
342
  /**
343
  * Formats the stream logs and returns an array of grouped sections.
 
1
  import toast from 'react-hot-toast';
 
2
  import { Message } from 'ai';
3
 
4
  const PAIRS: Record<string, string> = {
 
12
  const MIDDLE_SEPARATOR = '┿';
13
 
14
  const ANSWERS_PREFIX = 'answers';
 
15
 
16
  export const generateAnswersImageMarkdown = (index: number, url: string) => {
17
  return `![${ANSWERS_PREFIX}-${index}](${url})`;
18
  };
19
 
 
 
 
 
 
 
 
 
 
 
20
  export const cleanInputMessage = (content: string) => {
21
  return content
22
  .replace(/!\[input-.*?\)/g, '')
 
232
  }
233
  };
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  export type CodeResult = {
236
  code: string;
237
  test: string;
238
  result: string;
239
  };
240
 
241
+ export type ChunkBody =
242
+ | {
243
+ type: 'plans' | 'tools' | 'code' | 'final_code';
244
+ status: 'started' | 'completed' | 'failed' | 'running';
245
+ payload:
246
+ | Array<Record<string, string>> // PlansBody | ToolsBody
247
+ | CodeResult; // CodeBody
248
+ }
249
+ | PrismaJson.FinalChatResult;
250
 
251
  /**
252
  * Formats the stream logs and returns an array of grouped sections.
lib/utils/message.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Message } from '@prisma/client';
2
+ import { MessageAssistantResponse, MessageUI } from '../types';
3
+ import { ChunkBody } from './content';
4
+
5
+ const INPUT_PREFIX = 'input';
6
+
7
+ const generateInputImageMarkdown = (url: string) => {
8
+ if (url.toLowerCase().endsWith('.mp4')) {
9
+ return `![${INPUT_PREFIX}](<${url}>)`;
10
+ } else {
11
+ return `![${INPUT_PREFIX}](<${url}>)`;
12
+ }
13
+ };
14
+
15
+ /**
16
+ * The Message we saved to database consists of a prompt and a response
17
+ * for the UI to use, we need to break them to 2 messages, User and Assistant(if responded)
18
+ */
19
+ export const convertDBMessageToUIMessage = (
20
+ messages: Message[],
21
+ ): MessageUI[] => {
22
+ return messages.reduce((acc, message) => {
23
+ const { id, mediaUrl, prompt, response, result } = message;
24
+ if (mediaUrl && prompt) {
25
+ acc.push({
26
+ id: id + '-user',
27
+ role: 'user',
28
+ // add mediaUrl to the prompt
29
+ content: prompt + '\n\n' + generateInputImageMarkdown(mediaUrl),
30
+ });
31
+ }
32
+ if (response) {
33
+ acc.push({
34
+ id: id + '-assistant',
35
+ role: 'assistant',
36
+ content: response,
37
+ });
38
+ }
39
+ return acc;
40
+ }, [] as MessageUI[]);
41
+ };
42
+
43
+ export const convertAssistantUIMessageToDBMessageResponse = (
44
+ message: MessageUI,
45
+ ): MessageAssistantResponse => {
46
+ let result = null;
47
+ const lines = message.content.split('\n');
48
+ for (let line of lines) {
49
+ try {
50
+ const json = JSON.parse(line) as ChunkBody;
51
+ if (json.type == 'final_code') {
52
+ result = json as PrismaJson.FinalChatResult;
53
+ break;
54
+ }
55
+ } catch (e) {
56
+ console.error((e as Error).message);
57
+ }
58
+ }
59
+ return {
60
+ response: message.content,
61
+ result,
62
+ };
63
+ };
prisma/schema.prisma CHANGED
@@ -29,8 +29,8 @@ model Chat {
29
  createdAt DateTime @default(now()) @map("created_at")
30
  updatedAt DateTime @updatedAt @map("updated_at")
31
  title String @default("(no title)")
32
- userId String?
33
  mediaUrl String
 
34
  user User? @relation(fields: [userId], references: [id])
35
  messages Message[]
36
 
@@ -38,23 +38,18 @@ model Chat {
38
  }
39
 
40
  model Message {
41
- id String @id @default(cuid())
42
- createdAt DateTime @default(now()) @map("created_at")
43
- updatedAt DateTime @updatedAt @map("updated_at")
44
  userId String?
45
  chatId String
46
- content String
 
 
47
  /// [FinalChatResult]
48
  result Json?
49
- role MessageRole
50
- chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
51
- user User? @relation(fields: [userId], references: [id])
52
 
53
  @@map("message")
54
  }
55
-
56
- enum MessageRole {
57
- system
58
- user
59
- assistant
60
- }
 
29
  createdAt DateTime @default(now()) @map("created_at")
30
  updatedAt DateTime @updatedAt @map("updated_at")
31
  title String @default("(no title)")
 
32
  mediaUrl String
33
+ userId String?
34
  user User? @relation(fields: [userId], references: [id])
35
  messages Message[]
36
 
 
38
  }
39
 
40
  model Message {
41
+ id String @id @default(cuid())
42
+ createdAt DateTime @default(now()) @map("created_at")
43
+ updatedAt DateTime @updatedAt @map("updated_at")
44
  userId String?
45
  chatId String
46
+ mediaUrl String
47
+ prompt String
48
+ response String?
49
  /// [FinalChatResult]
50
  result Json?
51
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
52
+ user User? @relation(fields: [userId], references: [id])
 
53
 
54
  @@map("message")
55
  }