MingruiZhang commited on
Commit
009c95b
β€’
1 Parent(s): 63d0776

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 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 { Chat } from '@/components/chat';
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 PageProps {
8
  params: {
@@ -12,12 +10,15 @@ interface PageProps {
12
 
13
  export default async function Page({ params }: PageProps) {
14
  const { id: chatId } = params;
15
- const chat = await dbGetChat(chatId);
16
-
17
- if (!chat) {
18
- revalidatePath('/');
19
- redirect('/');
20
- }
21
- const session = await auth();
22
- return <Chat chat={chat} session={session} />;
 
 
 
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 { dbGetAllChat } 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 dbGetAllChat();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  }
 
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({ children }: RootLayoutProps) {
 
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 * as React from 'react';
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 Chat</Link>
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
- <div className="overflow-hidden w-[150px] h-[45px] shrink-0 grow-0 relative">
 
 
 
29
  <Image src={LandingLogo} alt="Landing AI" fill />
30
- </div>
 
 
 
 
 
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">Chat</Link>
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: width ? Number(width) / aspectRatio : img.naturalHeight,
 
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 className="flex justify-center items-center size-full text-sm">
 
 
 
 
 
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 "@/lib/utils"
2
 
3
  function Skeleton({
4
  className,
@@ -6,10 +6,10 @@ function Skeleton({
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 }
 
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 dbGetAllChat(): Promise<ChatWithMessages[]> {
 
 
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', 'layout');
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', 'layout');
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(react@18.2.0)
 
 
 
32
  '@radix-ui/react-separator':
33
  specifier: ^1.0.3
34
  version: 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -732,6 +735,9 @@ packages:
732
  '@prisma/get-platform@5.14.0':
733
  resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==}
734
 
 
 
 
735
  '@radix-ui/primitive@1.0.1':
736
  resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
737
 
@@ -941,6 +947,19 @@ packages:
941
  '@types/react-dom':
942
  optional: true
943
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
  '@radix-ui/react-separator@1.0.3':
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/primitive@1.0.1':
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/react-separator@1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
4934
  dependencies:
4935
  '@babel/runtime': 7.24.4
 
29
  '@radix-ui/react-icons':
30
  specifier: ^1.3.0
31
  version: 1.3.0(react@18.2.0)
32
+ '@radix-ui/react-select':
33
+ specifier: ^2.0.0
34
+ version: 2.0.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
35
  '@radix-ui/react-separator':
36
  specifier: ^1.0.3
37
  version: 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
 
735
  '@prisma/get-platform@5.14.0':
736
  resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==}
737
 
738
+ '@radix-ui/number@1.0.1':
739
+ resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
740
+
741
  '@radix-ui/primitive@1.0.1':
742
  resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
743
 
 
947
  '@types/react-dom':
948
  optional: true
949
 
950
+ '@radix-ui/react-select@2.0.0':
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/react-separator@1.0.3':
964
  resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
965
  peerDependencies:
 
4722
  dependencies:
4723
  '@prisma/debug': 5.14.0
4724
 
4725
+ '@radix-ui/number@1.0.1':
4726
+ dependencies:
4727
+ '@babel/runtime': 7.24.4
4728
+
4729
  '@radix-ui/primitive@1.0.1':
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/react-select@2.0.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
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/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
4962
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4963
+ '@radix-ui/react-context': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4964
+ '@radix-ui/react-direction': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4965
+ '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
4966
+ '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4967
+ '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
4968
+ '@radix-ui/react-id': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4969
+ '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
4970
+ '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
4971
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
4972
+ '@radix-ui/react-slot': 1.0.2(@types/react@18.2.79)(react@18.2.0)
4973
+ '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4974
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4975
+ '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4976
+ '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.79)(react@18.2.0)
4977
+ '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
4978
+ aria-hidden: 1.2.4
4979
+ react: 18.2.0
4980
+ react-dom: 18.2.0(react@18.2.0)
4981
+ react-remove-scroll: 2.5.5(@types/react@18.2.79)(react@18.2.0)
4982
+ optionalDependencies:
4983
+ '@types/react': 18.2.79
4984
+ '@types/react-dom': 18.2.25
4985
+
4986
  '@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
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])