Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
feat: Chat selector in Header (#59)
Browse files![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/bdd9ca3d-7d6f-46ad-b15a-d9988756992c)
- app/all/chat/[id]/page.tsx +0 -18
- app/all/layout.tsx +0 -39
- app/all/page.tsx +0 -5
- app/chat/[id]/page.tsx +14 -13
- app/chat/layout.tsx +32 -27
- app/layout.tsx +2 -1
- components/ChatSelect.tsx +80 -0
- components/ChatSelectServer.tsx +8 -0
- components/Header.tsx +18 -6
- components/chat/{index.tsx → ChatClient.tsx} +0 -0
- components/chat/ChatServer.tsx +20 -0
- components/ui/Img.tsx +2 -1
- components/ui/Loading.tsx +8 -2
- components/ui/Select.tsx +167 -0
- components/ui/{skeleton.tsx → Skeleton.tsx} +4 -4
- lib/db/functions.ts +23 -3
- lib/kv/chat.ts +0 -118
- lib/utils.ts +0 -9
- package.json +1 -0
- pnpm-lock.yaml +53 -0
- prisma/schema.prisma +1 -0
app/all/chat/[id]/page.tsx
DELETED
@@ -1,18 +0,0 @@
|
|
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: {
|
8 |
-
id: string;
|
9 |
-
};
|
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
DELETED
@@ -1,39 +0,0 @@
|
|
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 {
|
8 |
-
children: React.ReactNode;
|
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/all/page.tsx
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
interface PageProps {}
|
2 |
-
|
3 |
-
export default async function Page({}: PageProps) {
|
4 |
-
return <div></div>;
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
app/chat/[id]/page.tsx
CHANGED
@@ -1,8 +1,6 @@
|
|
1 |
-
import {
|
2 |
-
import
|
3 |
-
import
|
4 |
-
import { redirect } from 'next/navigation';
|
5 |
-
import { revalidatePath } from 'next/cache';
|
6 |
|
7 |
interface PageProps {
|
8 |
params: {
|
@@ -12,12 +10,15 @@ interface PageProps {
|
|
12 |
|
13 |
export default async function Page({ params }: PageProps) {
|
14 |
const { id: chatId } = params;
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
23 |
}
|
|
|
1 |
+
import { Suspense } from 'react';
|
2 |
+
import ChatServer from '@/components/chat/ChatServer';
|
3 |
+
import Loading from '@/components/ui/Loading';
|
|
|
|
|
4 |
|
5 |
interface PageProps {
|
6 |
params: {
|
|
|
10 |
|
11 |
export default async function Page({ params }: PageProps) {
|
12 |
const { id: chatId } = params;
|
13 |
+
return (
|
14 |
+
<Suspense
|
15 |
+
fallback={
|
16 |
+
<div className="h-screen w-screen flex justify-center items-center">
|
17 |
+
<Loading />
|
18 |
+
</div>
|
19 |
+
}
|
20 |
+
>
|
21 |
+
<ChatServer id={chatId} />
|
22 |
+
</Suspense>
|
23 |
+
);
|
24 |
}
|
app/chat/layout.tsx
CHANGED
@@ -1,34 +1,39 @@
|
|
1 |
-
import { sessionUser } from '@/auth';
|
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 {
|
8 |
children: React.ReactNode;
|
9 |
}
|
10 |
|
11 |
-
export default async function Layout({ children }: ChatLayoutProps) {
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
|
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 |
}
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
}
|
app/layout.tsx
CHANGED
@@ -33,7 +33,8 @@ interface RootLayoutProps {
|
|
33 |
children: React.ReactNode;
|
34 |
}
|
35 |
|
36 |
-
export default function RootLayout(
|
|
|
37 |
return (
|
38 |
<html lang="en" suppressHydrationWarning>
|
39 |
<body
|
|
|
33 |
children: React.ReactNode;
|
34 |
}
|
35 |
|
36 |
+
export default function RootLayout(props: RootLayoutProps) {
|
37 |
+
const { children } = props;
|
38 |
return (
|
39 |
<html lang="en" suppressHydrationWarning>
|
40 |
<body
|
components/ChatSelect.tsx
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { Chat } from '@prisma/client';
|
4 |
+
import React from 'react';
|
5 |
+
import {
|
6 |
+
SelectItem,
|
7 |
+
Select,
|
8 |
+
SelectTrigger,
|
9 |
+
SelectContent,
|
10 |
+
SelectIcon,
|
11 |
+
SelectGroup,
|
12 |
+
SelectSeparator,
|
13 |
+
} from './ui/Select';
|
14 |
+
import Img from './ui/Img';
|
15 |
+
import { format } from 'date-fns';
|
16 |
+
import { useParams, useRouter } from 'next/navigation';
|
17 |
+
import { IconPlus } from './ui/Icons';
|
18 |
+
|
19 |
+
export interface ChatSelectProps {
|
20 |
+
chat: Chat;
|
21 |
+
}
|
22 |
+
|
23 |
+
const ChatSelectItem: React.FC<ChatSelectProps> = ({ chat }) => {
|
24 |
+
const { id, title, mediaUrl, updatedAt } = chat;
|
25 |
+
return (
|
26 |
+
<SelectItem value={id} className="size-full cursor-pointer">
|
27 |
+
<div className="overflow-hidden flex items-center size-full group">
|
28 |
+
<div className="size-[36px] relative m-1">
|
29 |
+
<Img
|
30 |
+
src={mediaUrl}
|
31 |
+
alt={`chat-${id}-card-image`}
|
32 |
+
className="object-cover size-full"
|
33 |
+
/>
|
34 |
+
</div>
|
35 |
+
<div className="flex items-start flex-col h-full ml-3">
|
36 |
+
<p className="text-sm mb-1">{title ?? '(no title)'}</p>
|
37 |
+
<p className="text-xs text-gray-500">
|
38 |
+
{updatedAt ? format(Number(updatedAt), 'yyyy-MM-dd') : '-'}
|
39 |
+
</p>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
</SelectItem>
|
43 |
+
);
|
44 |
+
};
|
45 |
+
|
46 |
+
const ChatSelect: React.FC<{ myChats: Chat[] }> = ({ myChats }) => {
|
47 |
+
const { id: chatIdFromParam } = useParams();
|
48 |
+
|
49 |
+
const currentChat = myChats.find(chat => chat.id === chatIdFromParam);
|
50 |
+
const router = useRouter();
|
51 |
+
return (
|
52 |
+
<Select
|
53 |
+
defaultValue={currentChat?.id}
|
54 |
+
value={currentChat?.id}
|
55 |
+
onValueChange={id => router.push(`/chat${id === 'new' ? '' : '/' + id}`)}
|
56 |
+
>
|
57 |
+
<SelectTrigger className="w-[240px]">
|
58 |
+
{currentChat?.title ?? 'Select a conversation'}
|
59 |
+
</SelectTrigger>
|
60 |
+
<SelectContent className="w-[320px]">
|
61 |
+
<SelectGroup>
|
62 |
+
<SelectItem value="new">
|
63 |
+
<div className="flex items-center justify-start">
|
64 |
+
<SelectIcon asChild>
|
65 |
+
<IconPlus className="size-4 opacity-50" />
|
66 |
+
</SelectIcon>
|
67 |
+
<div className="ml-4">New conversion</div>
|
68 |
+
</div>
|
69 |
+
</SelectItem>
|
70 |
+
{!!myChats.length && <SelectSeparator />}
|
71 |
+
{myChats.map(chat => (
|
72 |
+
<ChatSelectItem key={chat.id} chat={chat} />
|
73 |
+
))}
|
74 |
+
</SelectGroup>
|
75 |
+
</SelectContent>
|
76 |
+
</Select>
|
77 |
+
);
|
78 |
+
};
|
79 |
+
|
80 |
+
export default ChatSelect;
|
components/ChatSelectServer.tsx
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { dbGetMyChatList } from '@/lib/db/functions';
|
2 |
+
import ChatSelect from './ChatSelect';
|
3 |
+
|
4 |
+
export default async function ChatSelectServer() {
|
5 |
+
const [myChats] = await Promise.all([dbGetMyChatList()]);
|
6 |
+
|
7 |
+
return <ChatSelect myChats={myChats} />;
|
8 |
+
}
|
components/Header.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import
|
2 |
import Link from 'next/link';
|
3 |
|
4 |
import { auth, sessionUser } from '@/auth';
|
@@ -9,25 +9,37 @@ import { LoginMenu } from './LoginMenu';
|
|
9 |
import { redirect } from 'next/navigation';
|
10 |
import Image from 'next/image';
|
11 |
import LandingLogo from '@/assets/svg/LandingAI_white.svg';
|
|
|
|
|
|
|
12 |
|
13 |
export async function Header() {
|
14 |
const session = await auth();
|
15 |
-
const { isAdmin } = await sessionUser();
|
16 |
|
17 |
if (process.env.NEXT_PUBLIC_IS_HUGGING_FACE) {
|
18 |
return (
|
19 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
20 |
<Button variant="link" asChild className="mr-2">
|
21 |
-
<Link href="/chat">New
|
22 |
</Button>
|
23 |
</header>
|
24 |
);
|
25 |
}
|
|
|
26 |
return (
|
27 |
<header className="sticky top-0 z-50 flex items-center justify-start w-full h-16 px-4 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
28 |
-
<
|
|
|
|
|
|
|
29 |
<Image src={LandingLogo} alt="Landing AI" fill />
|
30 |
-
</
|
|
|
|
|
|
|
|
|
|
|
31 |
<div className="grow" />
|
32 |
{/* <Tooltip>
|
33 |
<TooltipTrigger asChild>
|
@@ -50,7 +62,7 @@ export async function Header() {
|
|
50 |
</Button>
|
51 |
)} */}
|
52 |
<Button variant="link" asChild className="mr-2">
|
53 |
-
<Link href="/chat">
|
54 |
</Button>
|
55 |
<IconSeparator className="size-6 text-muted-foreground/50" />
|
56 |
<div className="flex items-center grow-0">
|
|
|
1 |
+
import { Suspense } from 'react';
|
2 |
import Link from 'next/link';
|
3 |
|
4 |
import { auth, sessionUser } from '@/auth';
|
|
|
9 |
import { redirect } from 'next/navigation';
|
10 |
import Image from 'next/image';
|
11 |
import LandingLogo from '@/assets/svg/LandingAI_white.svg';
|
12 |
+
import ChatSelectServer from './ChatSelectServer';
|
13 |
+
import Loading from './ui/Loading';
|
14 |
+
import { Skeleton } from './ui/Skeleton';
|
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 (
|
22 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
23 |
<Button variant="link" asChild className="mr-2">
|
24 |
+
<Link href="/chat">New conversation</Link>
|
25 |
</Button>
|
26 |
</header>
|
27 |
);
|
28 |
}
|
29 |
+
|
30 |
return (
|
31 |
<header className="sticky top-0 z-50 flex items-center justify-start w-full h-16 px-4 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
32 |
+
<Link
|
33 |
+
className="overflow-hidden w-[150px] h-[45px] shrink-0 grow-0 relative mr-4 cursor-pointer"
|
34 |
+
href="/"
|
35 |
+
>
|
36 |
<Image src={LandingLogo} alt="Landing AI" fill />
|
37 |
+
</Link>
|
38 |
+
{session?.user && (
|
39 |
+
<Suspense fallback={<Skeleton className="w-[240px] h-[24px]" />}>
|
40 |
+
<ChatSelectServer />
|
41 |
+
</Suspense>
|
42 |
+
)}
|
43 |
<div className="grow" />
|
44 |
{/* <Tooltip>
|
45 |
<TooltipTrigger asChild>
|
|
|
62 |
</Button>
|
63 |
)} */}
|
64 |
<Button variant="link" asChild className="mr-2">
|
65 |
+
<Link href="/chat">New conversation</Link>
|
66 |
</Button>
|
67 |
<IconSeparator className="size-6 text-muted-foreground/50" />
|
68 |
<div className="flex items-center grow-0">
|
components/chat/{index.tsx → ChatClient.tsx}
RENAMED
File without changes
|
components/chat/ChatServer.tsx
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Chat } from './ChatClient';
|
2 |
+
import { auth } from '@/auth';
|
3 |
+
import { dbGetChat } from '@/lib/db/functions';
|
4 |
+
import { redirect } from 'next/navigation';
|
5 |
+
import { revalidatePath } from 'next/cache';
|
6 |
+
|
7 |
+
interface ChatServerProps {
|
8 |
+
id: string;
|
9 |
+
}
|
10 |
+
|
11 |
+
export default async function ChatServer({ id }: ChatServerProps) {
|
12 |
+
const chat = await dbGetChat(id);
|
13 |
+
|
14 |
+
if (!chat) {
|
15 |
+
revalidatePath('/');
|
16 |
+
redirect('/');
|
17 |
+
}
|
18 |
+
const session = await auth();
|
19 |
+
return <Chat chat={chat} session={session} />;
|
20 |
+
}
|
components/ui/Img.tsx
CHANGED
@@ -40,7 +40,8 @@ const Img = React.forwardRef<
|
|
40 |
startTransition(() => {
|
41 |
setDimensions({
|
42 |
width: width ?? img.naturalWidth,
|
43 |
-
height:
|
|
|
44 |
});
|
45 |
});
|
46 |
return onLoad?.(e);
|
|
|
40 |
startTransition(() => {
|
41 |
setDimensions({
|
42 |
width: width ?? img.naturalWidth,
|
43 |
+
height:
|
44 |
+
height ?? width ? Number(width) / aspectRatio : img.naturalHeight,
|
45 |
});
|
46 |
});
|
47 |
return onLoad?.(e);
|
components/ui/Loading.tsx
CHANGED
@@ -1,8 +1,14 @@
|
|
1 |
import { IconLoading } from '@/components/ui/Icons';
|
|
|
2 |
|
3 |
-
export default function Loading() {
|
4 |
return (
|
5 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
6 |
<IconLoading />
|
7 |
</div>
|
8 |
);
|
|
|
1 |
import { IconLoading } from '@/components/ui/Icons';
|
2 |
+
import { cn } from '@/lib/utils';
|
3 |
|
4 |
+
export default function Loading({ className }: { className?: String }) {
|
5 |
return (
|
6 |
+
<div
|
7 |
+
className={cn(
|
8 |
+
'flex justify-center items-center size-full text-sm',
|
9 |
+
className,
|
10 |
+
)}
|
11 |
+
>
|
12 |
<IconLoading />
|
13 |
</div>
|
14 |
);
|
components/ui/Select.tsx
ADDED
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import * as React from 'react';
|
4 |
+
import {
|
5 |
+
CaretSortIcon,
|
6 |
+
CheckIcon,
|
7 |
+
ChevronDownIcon,
|
8 |
+
ChevronUpIcon,
|
9 |
+
} from '@radix-ui/react-icons';
|
10 |
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
11 |
+
|
12 |
+
import { cn } from '@/lib/utils';
|
13 |
+
|
14 |
+
const Select = SelectPrimitive.Root;
|
15 |
+
|
16 |
+
const SelectIcon = SelectPrimitive.Icon;
|
17 |
+
|
18 |
+
const SelectGroup = SelectPrimitive.Group;
|
19 |
+
|
20 |
+
const SelectValue = SelectPrimitive.Value;
|
21 |
+
|
22 |
+
const SelectTrigger = React.forwardRef<
|
23 |
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
24 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
25 |
+
>(({ className, children, ...props }, ref) => (
|
26 |
+
<SelectPrimitive.Trigger
|
27 |
+
ref={ref}
|
28 |
+
className={cn(
|
29 |
+
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
30 |
+
className,
|
31 |
+
)}
|
32 |
+
{...props}
|
33 |
+
>
|
34 |
+
{children}
|
35 |
+
<SelectPrimitive.Icon asChild>
|
36 |
+
<CaretSortIcon className="size-4 opacity-50" />
|
37 |
+
</SelectPrimitive.Icon>
|
38 |
+
</SelectPrimitive.Trigger>
|
39 |
+
));
|
40 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
41 |
+
|
42 |
+
const SelectScrollUpButton = React.forwardRef<
|
43 |
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
44 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
45 |
+
>(({ className, ...props }, ref) => (
|
46 |
+
<SelectPrimitive.ScrollUpButton
|
47 |
+
ref={ref}
|
48 |
+
className={cn(
|
49 |
+
'flex cursor-default items-center justify-center py-1',
|
50 |
+
className,
|
51 |
+
)}
|
52 |
+
{...props}
|
53 |
+
>
|
54 |
+
<ChevronUpIcon />
|
55 |
+
</SelectPrimitive.ScrollUpButton>
|
56 |
+
));
|
57 |
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
58 |
+
|
59 |
+
const SelectScrollDownButton = React.forwardRef<
|
60 |
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
61 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
62 |
+
>(({ className, ...props }, ref) => (
|
63 |
+
<SelectPrimitive.ScrollDownButton
|
64 |
+
ref={ref}
|
65 |
+
className={cn(
|
66 |
+
'flex cursor-default items-center justify-center py-1',
|
67 |
+
className,
|
68 |
+
)}
|
69 |
+
{...props}
|
70 |
+
>
|
71 |
+
<ChevronDownIcon />
|
72 |
+
</SelectPrimitive.ScrollDownButton>
|
73 |
+
));
|
74 |
+
SelectScrollDownButton.displayName =
|
75 |
+
SelectPrimitive.ScrollDownButton.displayName;
|
76 |
+
|
77 |
+
const SelectContent = React.forwardRef<
|
78 |
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
79 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
80 |
+
>(({ className, children, position = 'popper', ...props }, ref) => (
|
81 |
+
<SelectPrimitive.Portal>
|
82 |
+
<SelectPrimitive.Content
|
83 |
+
ref={ref}
|
84 |
+
className={cn(
|
85 |
+
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
86 |
+
position === 'popper' &&
|
87 |
+
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
88 |
+
className,
|
89 |
+
)}
|
90 |
+
position={position}
|
91 |
+
{...props}
|
92 |
+
>
|
93 |
+
<SelectScrollUpButton />
|
94 |
+
<SelectPrimitive.Viewport
|
95 |
+
className={cn(
|
96 |
+
'p-1',
|
97 |
+
position === 'popper' &&
|
98 |
+
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
99 |
+
)}
|
100 |
+
>
|
101 |
+
{children}
|
102 |
+
</SelectPrimitive.Viewport>
|
103 |
+
<SelectScrollDownButton />
|
104 |
+
</SelectPrimitive.Content>
|
105 |
+
</SelectPrimitive.Portal>
|
106 |
+
));
|
107 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
108 |
+
|
109 |
+
const SelectLabel = React.forwardRef<
|
110 |
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
111 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
112 |
+
>(({ className, ...props }, ref) => (
|
113 |
+
<SelectPrimitive.Label
|
114 |
+
ref={ref}
|
115 |
+
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
116 |
+
{...props}
|
117 |
+
/>
|
118 |
+
));
|
119 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
120 |
+
|
121 |
+
const SelectItem = React.forwardRef<
|
122 |
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
123 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
124 |
+
>(({ className, children, ...props }, ref) => (
|
125 |
+
<SelectPrimitive.Item
|
126 |
+
ref={ref}
|
127 |
+
className={cn(
|
128 |
+
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
129 |
+
className,
|
130 |
+
)}
|
131 |
+
{...props}
|
132 |
+
>
|
133 |
+
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
134 |
+
<SelectPrimitive.ItemIndicator>
|
135 |
+
<CheckIcon className="size-4" />
|
136 |
+
</SelectPrimitive.ItemIndicator>
|
137 |
+
</span>
|
138 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
139 |
+
</SelectPrimitive.Item>
|
140 |
+
));
|
141 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
142 |
+
|
143 |
+
const SelectSeparator = React.forwardRef<
|
144 |
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
145 |
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
146 |
+
>(({ className, ...props }, ref) => (
|
147 |
+
<SelectPrimitive.Separator
|
148 |
+
ref={ref}
|
149 |
+
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
150 |
+
{...props}
|
151 |
+
/>
|
152 |
+
));
|
153 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
154 |
+
|
155 |
+
export {
|
156 |
+
Select,
|
157 |
+
SelectIcon,
|
158 |
+
SelectGroup,
|
159 |
+
SelectValue,
|
160 |
+
SelectTrigger,
|
161 |
+
SelectContent,
|
162 |
+
SelectLabel,
|
163 |
+
SelectItem,
|
164 |
+
SelectSeparator,
|
165 |
+
SelectScrollUpButton,
|
166 |
+
SelectScrollDownButton,
|
167 |
+
};
|
components/ui/{skeleton.tsx → Skeleton.tsx}
RENAMED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { cn } from
|
2 |
|
3 |
function Skeleton({
|
4 |
className,
|
@@ -6,10 +6,10 @@ function Skeleton({
|
|
6 |
}: React.HTMLAttributes<HTMLDivElement>) {
|
7 |
return (
|
8 |
<div
|
9 |
-
className={cn(
|
10 |
{...props}
|
11 |
/>
|
12 |
-
)
|
13 |
}
|
14 |
|
15 |
-
export { Skeleton }
|
|
|
1 |
+
import { cn } from '@/lib/utils';
|
2 |
|
3 |
function Skeleton({
|
4 |
className,
|
|
|
6 |
}: React.HTMLAttributes<HTMLDivElement>) {
|
7 |
return (
|
8 |
<div
|
9 |
+
className={cn('animate-pulse rounded-md bg-muted', className)}
|
10 |
{...props}
|
11 |
/>
|
12 |
+
);
|
13 |
}
|
14 |
|
15 |
+
export { Skeleton };
|
lib/db/functions.ts
CHANGED
@@ -4,6 +4,7 @@ import { sessionUser } from '@/auth';
|
|
4 |
import prisma from './prisma';
|
5 |
import { ChatWithMessages, MessageRaw } from './types';
|
6 |
import { revalidatePath } from 'next/cache';
|
|
|
7 |
|
8 |
/**
|
9 |
* Finds or creates a user in the database based on the provided email and name.
|
@@ -33,11 +34,27 @@ export async function dbFindOrCreateUser(email: string, name: string) {
|
|
33 |
}
|
34 |
}
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
/**
|
37 |
* Retrieves all chats with their associated messages for the current user.
|
38 |
* @returns A promise that resolves to an array of `ChatWithMessages` objects.
|
39 |
*/
|
40 |
-
export async function
|
|
|
|
|
41 |
const { id: userId } = await sessionUser();
|
42 |
|
43 |
if (!userId) return [];
|
@@ -75,10 +92,12 @@ export async function dbGetChat(id: string): Promise<ChatWithMessages | null> {
|
|
75 |
export async function dbPostCreateChat({
|
76 |
id,
|
77 |
mediaUrl,
|
|
|
78 |
initMessages = [],
|
79 |
}: {
|
80 |
id?: string;
|
81 |
mediaUrl: string;
|
|
|
82 |
initMessages?: MessageRaw[];
|
83 |
}) {
|
84 |
const { id: userId } = await sessionUser();
|
@@ -95,6 +114,7 @@ export async function dbPostCreateChat({
|
|
95 |
id,
|
96 |
mediaUrl: mediaUrl,
|
97 |
...userConnect,
|
|
|
98 |
messages: {
|
99 |
create: initMessages.map(message => ({
|
100 |
...message,
|
@@ -107,7 +127,7 @@ export async function dbPostCreateChat({
|
|
107 |
},
|
108 |
});
|
109 |
|
110 |
-
revalidatePath('/chat'
|
111 |
return response;
|
112 |
} catch (error) {
|
113 |
console.error(error);
|
@@ -147,7 +167,7 @@ export async function dbDeleteChat(chatId: string) {
|
|
147 |
where: { id: chatId },
|
148 |
});
|
149 |
|
150 |
-
revalidatePath('/chat'
|
151 |
|
152 |
return;
|
153 |
}
|
|
|
4 |
import prisma from './prisma';
|
5 |
import { ChatWithMessages, MessageRaw } from './types';
|
6 |
import { revalidatePath } from 'next/cache';
|
7 |
+
import { Chat } from '@prisma/client';
|
8 |
|
9 |
/**
|
10 |
* Finds or creates a user in the database based on the provided email and name.
|
|
|
34 |
}
|
35 |
}
|
36 |
|
37 |
+
/**
|
38 |
+
* Retrieves all chat records from the database for the current user.
|
39 |
+
* @returns A promise that resolves to an array of Chat objects.
|
40 |
+
*/
|
41 |
+
export async function dbGetMyChatList(): Promise<Chat[]> {
|
42 |
+
const { id: userId } = await sessionUser();
|
43 |
+
|
44 |
+
if (!userId) return [];
|
45 |
+
|
46 |
+
return prisma.chat.findMany({
|
47 |
+
where: { userId },
|
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 [];
|
|
|
92 |
export async function dbPostCreateChat({
|
93 |
id,
|
94 |
mediaUrl,
|
95 |
+
title,
|
96 |
initMessages = [],
|
97 |
}: {
|
98 |
id?: string;
|
99 |
mediaUrl: string;
|
100 |
+
title?: string;
|
101 |
initMessages?: MessageRaw[];
|
102 |
}) {
|
103 |
const { id: userId } = await sessionUser();
|
|
|
114 |
id,
|
115 |
mediaUrl: mediaUrl,
|
116 |
...userConnect,
|
117 |
+
title,
|
118 |
messages: {
|
119 |
create: initMessages.map(message => ({
|
120 |
...message,
|
|
|
127 |
},
|
128 |
});
|
129 |
|
130 |
+
revalidatePath('/chat');
|
131 |
return response;
|
132 |
} catch (error) {
|
133 |
console.error(error);
|
|
|
167 |
where: { id: chatId },
|
168 |
});
|
169 |
|
170 |
+
revalidatePath('/chat');
|
171 |
|
172 |
return;
|
173 |
}
|
lib/kv/chat.ts
DELETED
@@ -1,118 +0,0 @@
|
|
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 |
-
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lib/utils.ts
CHANGED
@@ -32,12 +32,3 @@ export async function fetcher<JSON = any>(
|
|
32 |
|
33 |
return res.json();
|
34 |
}
|
35 |
-
|
36 |
-
export function formatDate(input: string | number | Date): string {
|
37 |
-
const date = new Date(input);
|
38 |
-
return date.toLocaleDateString('en-US', {
|
39 |
-
month: 'long',
|
40 |
-
day: 'numeric',
|
41 |
-
year: 'numeric',
|
42 |
-
});
|
43 |
-
}
|
|
|
32 |
|
33 |
return res.json();
|
34 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package.json
CHANGED
@@ -21,6 +21,7 @@
|
|
21 |
"@radix-ui/react-dialog": "^1.0.5",
|
22 |
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
23 |
"@radix-ui/react-icons": "^1.3.0",
|
|
|
24 |
"@radix-ui/react-separator": "^1.0.3",
|
25 |
"@radix-ui/react-slot": "^1.0.2",
|
26 |
"@radix-ui/react-switch": "^1.0.3",
|
|
|
21 |
"@radix-ui/react-dialog": "^1.0.5",
|
22 |
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
23 |
"@radix-ui/react-icons": "^1.3.0",
|
24 |
+
"@radix-ui/react-select": "^2.0.0",
|
25 |
"@radix-ui/react-separator": "^1.0.3",
|
26 |
"@radix-ui/react-slot": "^1.0.2",
|
27 |
"@radix-ui/react-switch": "^1.0.3",
|
pnpm-lock.yaml
CHANGED
@@ -29,6 +29,9 @@ importers:
|
|
29 |
'@radix-ui/react-icons':
|
30 |
specifier: ^1.3.0
|
31 |
version: 1.3.0([email protected])
|
|
|
|
|
|
|
32 |
'@radix-ui/react-separator':
|
33 |
specifier: ^1.0.3
|
34 |
version: 1.0.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
@@ -732,6 +735,9 @@ packages:
|
|
732 |
'@prisma/[email protected]':
|
733 |
resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==}
|
734 |
|
|
|
|
|
|
|
735 |
'@radix-ui/[email protected]':
|
736 |
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
|
737 |
|
@@ -941,6 +947,19 @@ packages:
|
|
941 |
'@types/react-dom':
|
942 |
optional: true
|
943 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
944 |
'@radix-ui/[email protected]':
|
945 |
resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
|
946 |
peerDependencies:
|
@@ -4703,6 +4722,10 @@ snapshots:
|
|
4703 |
dependencies:
|
4704 |
'@prisma/debug': 5.14.0
|
4705 |
|
|
|
|
|
|
|
|
|
4706 |
'@radix-ui/[email protected]':
|
4707 |
dependencies:
|
4708 |
'@babel/runtime': 7.24.4
|
@@ -4930,6 +4953,36 @@ snapshots:
|
|
4930 |
'@types/react': 18.2.79
|
4931 |
'@types/react-dom': 18.2.25
|
4932 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4933 |
'@radix-ui/[email protected](@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
|
4934 |
dependencies:
|
4935 |
'@babel/runtime': 7.24.4
|
|
|
29 |
'@radix-ui/react-icons':
|
30 |
specifier: ^1.3.0
|
31 |
version: 1.3.0([email protected])
|
32 |
+
'@radix-ui/react-select':
|
33 |
+
specifier: ^2.0.0
|
34 |
+
version: 2.0.0(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
35 |
'@radix-ui/react-separator':
|
36 |
specifier: ^1.0.3
|
37 |
version: 1.0.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
|
|
735 |
'@prisma/[email protected]':
|
736 |
resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==}
|
737 |
|
738 |
+
'@radix-ui/[email protected]':
|
739 |
+
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
|
740 |
+
|
741 |
'@radix-ui/[email protected]':
|
742 |
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
|
743 |
|
|
|
947 |
'@types/react-dom':
|
948 |
optional: true
|
949 |
|
950 |
+
'@radix-ui/[email protected]':
|
951 |
+
resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==}
|
952 |
+
peerDependencies:
|
953 |
+
'@types/react': '*'
|
954 |
+
'@types/react-dom': '*'
|
955 |
+
react: ^16.8 || ^17.0 || ^18.0
|
956 |
+
react-dom: ^16.8 || ^17.0 || ^18.0
|
957 |
+
peerDependenciesMeta:
|
958 |
+
'@types/react':
|
959 |
+
optional: true
|
960 |
+
'@types/react-dom':
|
961 |
+
optional: true
|
962 |
+
|
963 |
'@radix-ui/[email protected]':
|
964 |
resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
|
965 |
peerDependencies:
|
|
|
4722 |
dependencies:
|
4723 |
'@prisma/debug': 5.14.0
|
4724 |
|
4725 |
+
'@radix-ui/[email protected]':
|
4726 |
+
dependencies:
|
4727 |
+
'@babel/runtime': 7.24.4
|
4728 |
+
|
4729 |
'@radix-ui/[email protected]':
|
4730 |
dependencies:
|
4731 |
'@babel/runtime': 7.24.4
|
|
|
4953 |
'@types/react': 18.2.79
|
4954 |
'@types/react-dom': 18.2.25
|
4955 |
|
4956 |
+
'@radix-ui/[email protected](@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
|
4957 |
+
dependencies:
|
4958 |
+
'@babel/runtime': 7.24.4
|
4959 |
+
'@radix-ui/number': 1.0.1
|
4960 |
+
'@radix-ui/primitive': 1.0.1
|
4961 |
+
'@radix-ui/react-collection': 1.0.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
4962 |
+
'@radix-ui/react-compose-refs': 1.0.1(@types/[email protected])([email protected])
|
4963 |
+
'@radix-ui/react-context': 1.0.1(@types/[email protected])([email protected])
|
4964 |
+
'@radix-ui/react-direction': 1.0.1(@types/[email protected])([email protected])
|
4965 |
+
'@radix-ui/react-dismissable-layer': 1.0.5(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
4966 |
+
'@radix-ui/react-focus-guards': 1.0.1(@types/[email protected])([email protected])
|
4967 |
+
'@radix-ui/react-focus-scope': 1.0.4(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
4968 |
+
'@radix-ui/react-id': 1.0.1(@types/[email protected])([email protected])
|
4969 |
+
'@radix-ui/react-popper': 1.1.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
4970 |
+
'@radix-ui/react-portal': 1.0.4(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
4971 |
+
'@radix-ui/react-primitive': 1.0.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
4972 |
+
'@radix-ui/react-slot': 1.0.2(@types/[email protected])([email protected])
|
4973 |
+
'@radix-ui/react-use-callback-ref': 1.0.1(@types/[email protected])([email protected])
|
4974 |
+
'@radix-ui/react-use-controllable-state': 1.0.1(@types/[email protected])([email protected])
|
4975 |
+
'@radix-ui/react-use-layout-effect': 1.0.1(@types/[email protected])([email protected])
|
4976 |
+
'@radix-ui/react-use-previous': 1.0.1(@types/[email protected])([email protected])
|
4977 |
+
'@radix-ui/react-visually-hidden': 1.0.3(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
4978 |
+
aria-hidden: 1.2.4
|
4979 |
+
react: 18.2.0
|
4980 |
+
react-dom: 18.2.0([email protected])
|
4981 |
+
react-remove-scroll: 2.5.5(@types/[email protected])([email protected])
|
4982 |
+
optionalDependencies:
|
4983 |
+
'@types/react': 18.2.79
|
4984 |
+
'@types/react-dom': 18.2.25
|
4985 |
+
|
4986 |
'@radix-ui/[email protected](@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
|
4987 |
dependencies:
|
4988 |
'@babel/runtime': 7.24.4
|
prisma/schema.prisma
CHANGED
@@ -24,6 +24,7 @@ model Chat {
|
|
24 |
id String @id @default(cuid())
|
25 |
createdAt DateTime @default(now()) @map("created_at")
|
26 |
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
27 |
userId String?
|
28 |
mediaUrl String
|
29 |
user User? @relation(fields: [userId], references: [id])
|
|
|
24 |
id String @id @default(cuid())
|
25 |
createdAt DateTime @default(now()) @map("created_at")
|
26 |
updatedAt DateTime @updatedAt @map("updated_at")
|
27 |
+
title String @default("(no title)")
|
28 |
userId String?
|
29 |
mediaUrl String
|
30 |
user User? @relation(fields: [userId], references: [id])
|