Spaces:
Sleeping
Sleeping
feat: DB change - combine user and assistant message (#70)
Browse files- app/api/chat/route.ts +2 -2
- app/api/vision-agent/route.ts +3 -3
- app/chat/layout.tsx +0 -31
- app/page.tsx +23 -40
- auth.ts +8 -6
- components/chat-sidebar/ChatAdminToggle.tsx +0 -14
- components/chat-sidebar/ChatCard.tsx +0 -75
- components/chat-sidebar/ChatListSidebar.tsx +0 -69
- components/chat/ChatClient.tsx +8 -12
- components/chat/ChatList.tsx +2 -2
- components/chat/ChatMessage.tsx +5 -5
- components/chat/ChatMessageActions.tsx +2 -2
- components/ui/Table.tsx +5 -2
- lib/db/functions.ts +62 -60
- lib/db/types.ts +0 -10
- lib/hooks/useVisionAgent.ts +34 -28
- lib/types.ts +10 -6
- lib/{messageUtils.ts → utils/content.ts} +9 -100
- lib/utils/message.ts +63 -0
- prisma/schema.prisma +9 -14
app/api/chat/route.ts
CHANGED
@@ -6,14 +6,14 @@ import {
|
|
6 |
ChatCompletionMessageParam,
|
7 |
ChatCompletionContentPart,
|
8 |
} from 'openai/resources';
|
9 |
-
import {
|
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:
|
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 {
|
5 |
import { logger, withLogging } from '@/lib/logger';
|
6 |
import { CLEANED_SEPARATOR } from '@/lib/constants';
|
7 |
-
import { cleanAnswerMessage, cleanInputMessage } from '@/lib/
|
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:
|
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
|
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 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
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 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
>
|
80 |
-
<
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
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
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
60 |
// put db user id into session
|
61 |
-
session.user.id =
|
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/
|
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 {
|
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 {
|
24 |
-
const { messages, append, isLoading
|
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 |
-
|
|
|
60 |
isLoading={isLoading}
|
61 |
onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
|
62 |
append({
|
63 |
-
|
64 |
-
|
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 {
|
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:
|
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 {
|
25 |
import Img from '../ui/Img';
|
26 |
-
import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/
|
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:
|
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
|
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="
|
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 {
|
10 |
|
11 |
interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
|
12 |
-
message:
|
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(
|
|
|
|
|
|
|
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-
|
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 {
|
|
|
|
|
|
|
|
|
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.
|
89 |
-
* @param {
|
90 |
-
* @returns {Promise<Chat>}
|
91 |
*/
|
92 |
export async function dbPostCreateChat({
|
93 |
id,
|
94 |
-
mediaUrl,
|
95 |
title,
|
96 |
-
|
|
|
97 |
}: {
|
98 |
id?: string;
|
99 |
-
mediaUrl: string;
|
100 |
title?: string;
|
101 |
-
|
|
|
102 |
}) {
|
103 |
-
const
|
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:
|
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
|
141 |
-
* @param message - The message
|
142 |
-
* @returns A promise that resolves to the created message.
|
143 |
*/
|
144 |
-
export async function dbPostCreateMessage(
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
connect: { id: userId }, // Connect the chat to an existing user
|
150 |
-
},
|
151 |
-
}
|
152 |
-
: {};
|
153 |
|
154 |
-
await prisma.message.create({
|
155 |
data: {
|
156 |
-
|
157 |
-
|
158 |
chat: {
|
159 |
connect: { id: chatId },
|
160 |
},
|
161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2 |
import { toast } from 'react-hot-toast';
|
3 |
-
import { useEffect, useRef } from 'react';
|
4 |
-
import { ChatWithMessages } from '../
|
5 |
-
import { dbPostCreateMessage } from '../db/functions';
|
6 |
import {
|
7 |
-
|
8 |
-
|
9 |
-
} from '../
|
|
|
|
|
|
|
|
|
10 |
|
11 |
const useVisionAgent = (chat: ChatWithMessages) => {
|
12 |
-
const { messages:
|
|
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
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
|
|
|
|
|
|
|
30 |
},
|
31 |
-
|
|
|
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 {
|
|
|
2 |
|
3 |
-
export type
|
4 |
-
|
5 |
-
|
6 |
-
|
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 |
-
|
338 |
-
|
339 |
-
|
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
|
42 |
-
createdAt DateTime
|
43 |
-
updatedAt DateTime
|
44 |
userId String?
|
45 |
chatId String
|
46 |
-
|
|
|
|
|
47 |
/// [FinalChatResult]
|
48 |
result Json?
|
49 |
-
|
50 |
-
|
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 |
}
|
|
|
|
|
|
|
|
|
|
|
|