Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
feat: Setup postgres and prisma (#50)
Browse fileshttps://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 +7 -5
- app/all/layout.tsx +26 -26
- app/api/auth/[...nextauth]/route.ts +1 -1
- app/api/{upload → chat/create}/route.ts +11 -26
- app/api/vision-agent/route.ts +3 -3
- app/chat/[id]/page.tsx +6 -3
- app/chat/layout.tsx +4 -4
- app/chat/page.tsx +8 -6
- app/project/layout.tsx +2 -2
- auth.ts +31 -13
- components/Header.tsx +4 -4
- components/chat-sidebar/ChatCard.tsx +7 -8
- components/chat-sidebar/ChatListSidebar.tsx +2 -2
- components/chat/ImageSelector.tsx +3 -2
- components/chat/index.tsx +4 -4
- components/project/ProjectChat.tsx +37 -39
- components/ui/Icons.tsx +2 -2
- lib/db/functions.ts +145 -0
- lib/db/prisma.ts +5 -0
- lib/db/types.ts +8 -0
- lib/hooks/useVisionAgent.ts +27 -32
- lib/kv/chat.ts +118 -118
- package.json +5 -2
- pnpm-lock.yaml +64 -0
- prisma/schema.prisma +59 -0
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 |
-
|
13 |
-
const
|
14 |
-
const
|
15 |
-
|
|
|
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 {
|
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 |
-
|
|
|
14 |
|
15 |
-
if (!isAdmin) {
|
16 |
-
|
17 |
-
}
|
18 |
-
const chats = await adminGetAllKVChats();
|
19 |
|
20 |
-
return (
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
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 {
|
2 |
-
import {
|
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 |
-
|
16 |
json: {
|
17 |
id?: string;
|
18 |
url: string;
|
19 |
-
initMessages?:
|
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 {
|
31 |
-
|
32 |
-
const payload: ChatEntity = {
|
33 |
-
url,
|
34 |
-
id: id ?? nanoid(),
|
35 |
-
user,
|
36 |
-
messages: initMessages ?? [],
|
37 |
-
updatedAt: Date.now(),
|
38 |
-
};
|
39 |
|
40 |
-
await
|
|
|
|
|
|
|
|
|
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 |
-
|
20 |
enableSelfReflection: boolean;
|
21 |
},
|
22 |
request,
|
23 |
) => {
|
24 |
-
const { messages,
|
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',
|
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 {
|
|
|
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
|
|
|
|
|
|
|
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 {
|
2 |
import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
|
3 |
import Loading from '@/components/ui/Loading';
|
4 |
-
import {
|
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
|
13 |
-
const chats = await
|
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 =
|
|
|
21 |
const EXAMPLE_HEADER = 'Counting and find';
|
22 |
-
const EXAMPLE_SUBHEADER =
|
23 |
-
|
|
|
|
|
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<
|
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 {
|
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
|
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 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
44 |
-
|
|
|
45 |
}
|
46 |
return session;
|
47 |
},
|
@@ -59,12 +76,13 @@ export const {
|
|
59 |
},
|
60 |
});
|
61 |
|
62 |
-
export async function
|
63 |
const session = await auth();
|
64 |
-
const email = session?.user
|
65 |
return {
|
66 |
email,
|
67 |
isAdmin: !!email?.endsWith('landing.ai'),
|
68 |
-
|
|
|
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,
|
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
|
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 {
|
15 |
-
|
16 |
|
17 |
type ChatCardProps = PropsWithChildren<{
|
18 |
-
chat:
|
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,
|
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={
|
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">{
|
63 |
<IconClose
|
64 |
onClick={async e => {
|
65 |
e.stopPropagation();
|
66 |
-
await
|
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:
|
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
|
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<
|
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:
|
13 |
isAdminView?: boolean;
|
14 |
session: Session | null;
|
15 |
}
|
16 |
|
17 |
export function Chat({ chat, session, isAdminView }: ChatProps) {
|
18 |
-
const {
|
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={
|
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 |
-
|
19 |
-
//
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
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 |
-
|
570 |
-
|
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 {
|
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:
|
54 |
-
const { messages: initialMessages, id,
|
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:
|
|
|
105 |
createdAt: new Date(),
|
106 |
} satisfies Message,
|
107 |
]
|
108 |
: []),
|
109 |
newMessage,
|
110 |
]);
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
114 |
} else {
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
});
|
120 |
-
}
|
121 |
}
|
122 |
},
|
123 |
initialMessages: initialMessages,
|
124 |
body: {
|
125 |
-
|
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 |
-
|
156 |
-
|
157 |
-
|
|
|
158 |
return appendRaw(message);
|
159 |
};
|
160 |
|
161 |
return {
|
162 |
-
messages:
|
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,
|
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 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
}
|
34 |
-
|
35 |
-
export async function adminGetAllKVChats() {
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
}
|
59 |
-
|
60 |
-
export async function getKVChat(id: string) {
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
}
|
70 |
-
|
71 |
-
export async function createKVChat(chat: ChatEntity) {
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
}
|
88 |
-
|
89 |
-
export async function saveKVChatMessage(id: string, message: MessageBase) {
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
}
|
102 |
-
|
103 |
-
export async function removeKVChat(id: string) {
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
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.
|
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/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
@@ -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/[email protected]':
|
700 |
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
|
701 |
|
@@ -3016,6 +3046,11 @@ packages:
|
|
3016 | |
3017 |
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
3018 |
|
|
|
|
|
|
|
|
|
|
|
3019 | |
3020 |
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
|
3021 |
engines: {node: '>=6'}
|
@@ -4562,6 +4597,31 @@ snapshots:
|
|
4562 |
'@pkgjs/[email protected]':
|
4563 |
optional: true
|
4564 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4565 |
'@radix-ui/[email protected]':
|
4566 |
dependencies:
|
4567 |
'@babel/runtime': 7.24.4
|
@@ -7362,6 +7422,10 @@ snapshots:
|
|
7362 |
|
7363 | |
7364 |
|
|
|
|
|
|
|
|
|
7365 | |
7366 |
|
7367 |
|
|
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([email protected])
|
23 |
'@radix-ui/react-dropdown-menu':
|
24 |
specifier: ^2.0.6
|
25 |
version: 2.0.6(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
|
|
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/[email protected]':
|
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/[email protected]':
|
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/[email protected]':
|
721 |
+
resolution: {integrity: sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==}
|
722 |
+
|
723 |
+
'@prisma/[email protected]':
|
724 |
+
resolution: {integrity: sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==}
|
725 |
+
|
726 |
+
'@prisma/[email protected]':
|
727 |
+
resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==}
|
728 |
+
|
729 |
'@radix-ui/[email protected]':
|
730 |
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
|
731 |
|
|
|
3046 | |
3047 |
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
3048 |
|
3049 | |
3050 |
+
resolution: {integrity: sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==}
|
3051 |
+
engines: {node: '>=16.13'}
|
3052 |
+
hasBin: true
|
3053 |
+
|
3054 | |
3055 |
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
|
3056 |
engines: {node: '>=6'}
|
|
|
4597 |
'@pkgjs/[email protected]':
|
4598 |
optional: true
|
4599 |
|
4600 |
+
'@prisma/[email protected]([email protected])':
|
4601 |
+
optionalDependencies:
|
4602 |
+
prisma: 5.14.0
|
4603 |
+
|
4604 |
+
'@prisma/[email protected]': {}
|
4605 |
+
|
4606 |
+
'@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {}
|
4607 |
+
|
4608 |
+
'@prisma/[email protected]':
|
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/[email protected]':
|
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/[email protected]':
|
4622 |
+
dependencies:
|
4623 |
+
'@prisma/debug': 5.14.0
|
4624 |
+
|
4625 |
'@radix-ui/[email protected]':
|
4626 |
dependencies:
|
4627 |
'@babel/runtime': 7.24.4
|
|
|
7422 |
|
7423 | |
7424 |
|
7425 | |
7426 |
+
dependencies:
|
7427 |
+
'@prisma/engines': 5.14.0
|
7428 |
+
|
7429 | |
7430 |
|
7431 |
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 |
+
}
|