Spaces:
Running
Running
feat: Chat selector in Header (#59)
Browse files
- 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(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])
|