MingruiZhang commited on
Commit
c3e8f3d
β€’
1 Parent(s): 7eeb895

KV template

Browse files
app/api/chat/route.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
  ChatCompletionContentPartImage,
9
  } from 'openai/resources';
10
  import { MessageWithSelectedDataset } from '../../../lib/types';
11
- import { postAgentChat } from '@/lib/fetch';
12
 
13
  export const runtime = 'edge';
14
 
@@ -21,6 +21,7 @@ export async function POST(req: Request) {
21
  const { messages } = json as {
22
  messages: MessageWithSelectedDataset[];
23
  };
 
24
 
25
  const session = await auth();
26
  if (!session?.user?.email) {
@@ -29,14 +30,6 @@ export async function POST(req: Request) {
29
  });
30
  }
31
 
32
- // const lastMessage = messages[messages.length - 1];
33
- // const firstMessage = messages[0];
34
-
35
- // const resp = await postAgentChat({
36
- // input: lastMessage.content,
37
- // image: firstMessage.dataset?.[0]?.url,
38
- // });
39
-
40
  const formattedMessage: ChatCompletionMessageParam[] = messages.map(
41
  message => {
42
  const { dataset, ...rest } = message;
 
8
  ChatCompletionContentPartImage,
9
  } from 'openai/resources';
10
  import { MessageWithSelectedDataset } from '../../../lib/types';
11
+ // import { postAgentChat } from '@/lib/fetch';
12
 
13
  export const runtime = 'edge';
14
 
 
21
  const { messages } = json as {
22
  messages: MessageWithSelectedDataset[];
23
  };
24
+ console.log('[Ming] ~ POST ~ messages:', messages);
25
 
26
  const session = await auth();
27
  if (!session?.user?.email) {
 
30
  });
31
  }
32
 
 
 
 
 
 
 
 
 
33
  const formattedMessage: ChatCompletionMessageParam[] = messages.map(
34
  message => {
35
  const { dataset, ...rest } = message;
app/api/upload/route.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/auth';
2
+ import { nanoid } from '@/lib/utils';
3
+ import { kv } from '@vercel/kv';
4
+ import { format } from 'date-fns';
5
+
6
+ export async function POST(req: Request) {
7
+ const session = await auth();
8
+ console.log('[Ming] ~ POST ~ session:', session);
9
+ const email = session?.user?.email;
10
+ if (!email) {
11
+ return new Response('Unauthorized', {
12
+ status: 401,
13
+ });
14
+ }
15
+
16
+ const json = await req.json();
17
+ console.log('[Ming] ~ POST ~ json:', json);
18
+
19
+ const id = nanoid();
20
+
21
+ await kv.hmset(`chat:${id}`, json);
22
+ await kv.zadd(`user:chat:${email}`, {
23
+ score: Date.now(),
24
+ member: `chat:${id}`,
25
+ });
26
+ await kv.zadd('user:chat:all', {
27
+ score: Date.now(),
28
+ member: `chat:${id}`,
29
+ });
30
+
31
+ return 'success';
32
+ }
app/chat/layout.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
2
+ import Loading from '@/components/ui/Loading';
3
+ import { Suspense } from 'react';
4
+
5
+ interface ChatLayoutProps {
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ export default async function Layout({ children }: ChatLayoutProps) {
10
+ return (
11
+ <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
12
+ <div
13
+ data-state="open"
14
+ 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"
15
+ >
16
+ <Suspense fallback={<Loading />}>
17
+ <ChatSidebarList />
18
+ </Suspense>
19
+ </div>
20
+ <Suspense fallback={<Loading />}>
21
+ <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]">
22
+ {children}
23
+ </div>
24
+ </Suspense>
25
+ </div>
26
+ );
27
+ }
app/chat/page.tsx CHANGED
@@ -1,27 +1,7 @@
1
- 'use client';
2
-
3
  import { nanoid } from '@/lib/utils';
4
  import { Chat } from '@/components/chat';
5
- import { ThemeToggle } from '../../components/ThemeToggle';
6
- import { useAtomValue } from 'jotai';
7
- import { datasetAtom } from '../../state';
8
- import { EmptyScreen } from '../../components/chat/EmptyScreen';
9
 
10
  export default function Page() {
11
  const id = nanoid();
12
- const dataset = useAtomValue(datasetAtom);
13
-
14
- if (!dataset.length)
15
- return (
16
- <div className="pb-[150px] pt-4 md:pt-10 h-full">
17
- <EmptyScreen />
18
- </div>
19
- );
20
-
21
- return (
22
- <>
23
- <Chat id={id} />
24
- <ThemeToggle />
25
- </>
26
- );
27
  }
 
 
 
1
  import { nanoid } from '@/lib/utils';
2
  import { Chat } from '@/components/chat';
 
 
 
 
3
 
4
  export default function Page() {
5
  const id = nanoid();
6
+ return <Chat id={id} />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  }
app/layout.tsx CHANGED
@@ -7,6 +7,7 @@ import { cn } from '@/lib/utils';
7
  import { TailwindIndicator } from '@/components/TailwindIndicator';
8
  import { Providers } from '@/components/Providers';
9
  import { Header } from '@/components/Header';
 
10
 
11
  export const metadata = {
12
  metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
 
7
  import { TailwindIndicator } from '@/components/TailwindIndicator';
8
  import { Providers } from '@/components/Providers';
9
  import { Header } from '@/components/Header';
10
+ import { ThemeToggle } from '@/components/ThemeToggle';
11
 
12
  export const metadata = {
13
  metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
app/page.tsx CHANGED
@@ -1,9 +1,17 @@
1
- 'use client';
 
2
 
3
- export default function Page() {
4
- return (
5
- <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
6
- Welcome to Insight Playground
7
- </div>
8
- );
 
 
 
 
 
 
 
9
  }
 
1
+ import { auth } from '@/auth';
2
+ import { redirect } from 'next/navigation';
3
 
4
+ export default async function Page() {
5
+ const session = await auth();
6
+ if (!session) {
7
+ return null;
8
+ }
9
+
10
+ redirect('/chat');
11
+
12
+ // return (
13
+ // <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
14
+ // Welcome to Insight Playground
15
+ // </div>
16
+ // );
17
  }
app/project/[projectId]/page.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import MediaGrid from '@/components/project/MediaGrid';
2
  import { fetchProjectMedia } from '@/lib/fetch';
3
  import { Suspense } from 'react';
4
- import Loading from '../loading';
5
  import Chat from '@/components/project/Chat';
6
 
7
  interface PageProps {
 
1
  import MediaGrid from '@/components/project/MediaGrid';
2
  import { fetchProjectMedia } from '@/lib/fetch';
3
  import { Suspense } from 'react';
4
+ import Loading from '../../../components/ui/Loading';
5
  import Chat from '@/components/project/Chat';
6
 
7
  interface PageProps {
app/project/layout.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import ProjectListSideBar from '@/components/sidebar/ProjectListSideBar';
2
  import { Suspense } from 'react';
3
- import Loading from './loading';
4
 
5
  interface ChatLayoutProps {
6
  children: React.ReactNode;
 
1
+ import ProjectListSideBar from '@/components/project-sidebar/ProjectListSideBar';
2
  import { Suspense } from 'react';
3
+ import Loading from '@/components/ui/Loading';
4
 
5
  interface ChatLayoutProps {
6
  children: React.ReactNode;
components/Header.tsx CHANGED
@@ -15,9 +15,9 @@ export async function Header() {
15
 
16
  return (
17
  <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">
18
- <Button variant="link" asChild className="mr-2">
19
  <Link href="/project">Projects</Link>
20
- </Button>
21
  <Button variant="link" asChild className="mr-2">
22
  <Link href="/chat">Chat</Link>
23
  </Button>
 
15
 
16
  return (
17
  <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">
18
+ {/* <Button variant="link" asChild className="mr-2">
19
  <Link href="/project">Projects</Link>
20
+ </Button> */}
21
  <Button variant="link" asChild className="mr-2">
22
  <Link href="/chat">Chat</Link>
23
  </Button>
components/chat-sidebar/ChatCard.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useParams } from 'next/navigation';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ export interface ChatCardProps {
8
+ id: string;
9
+ title: string;
10
+ }
11
+
12
+ const ChatCard: React.FC<ChatCardProps> = ({ id, title }) => {
13
+ const { chatId: chatIdFromParam } = useParams();
14
+ return (
15
+ <Link
16
+ className={cn(
17
+ 'p-4 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
18
+ chatIdFromParam === id && 'border-gray-500',
19
+ )}
20
+ href={`/chat/${id}`}
21
+ >
22
+ <div className="overflow-hidden">
23
+ <p className="text-sm font-medium text-black mb-1">{title}</p>
24
+ </div>
25
+ </Link>
26
+ );
27
+ };
28
+
29
+ export default ChatCard;
components/chat-sidebar/ChatListSidebar.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getKVChats } from '@/lib/kv/chat';
2
+ import ChatCard from './ChatCard';
3
+
4
+ export interface ChatSidebarListProps {}
5
+
6
+ export default async function ChatSidebarList({}: ChatSidebarListProps) {
7
+ const chats = await getKVChats();
8
+ console.log('[Ming] ~ ChatSidebarList ~ chats:', chats);
9
+ return (
10
+ <>
11
+ {chats.map(chat => (
12
+ <ChatCard key={chat.id} id={chat.id} title={chat.title} />
13
+ ))}
14
+ </>
15
+ );
16
+ }
components/chat/ImageSelector.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import Image from 'next/image';
3
+ import useImageUpload from '../../lib/hooks/useImageUpload';
4
+ import { fetcher } from '@/lib/utils';
5
+
6
+ export interface ImageSelectorProps {}
7
+
8
+ const examples = [
9
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
10
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
11
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
12
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
13
+ ];
14
+
15
+ const ImageSelector: React.FC<ImageSelectorProps> = () => {
16
+ const { getRootProps, getInputProps } = useImageUpload();
17
+ return (
18
+ <div className="mx-auto max-w-2xl px-4">
19
+ <div className="rounded-lg border bg-background p-8">
20
+ <h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
21
+ <p>Lets start by choosing an image</p>
22
+ <div
23
+ {...getRootProps()}
24
+ className="dropzone border-2 border-dashed border-gray-400 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer"
25
+ >
26
+ <input {...getInputProps()} />
27
+ <p className="text-gray-400 text-lg">
28
+ Drag or drop image here, or click to select images
29
+ </p>
30
+ </div>
31
+ <p className="mt-4 mb-2">
32
+ You can also choose from below examples we provided
33
+ </p>
34
+ <div className="flex">
35
+ {examples.map((example, index) => (
36
+ <Image
37
+ src={example}
38
+ key={index}
39
+ width={120}
40
+ height={120}
41
+ alt="example images"
42
+ className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
43
+ onClick={() =>
44
+ fetcher('/api/upload', {
45
+ method: 'POST',
46
+ body: JSON.stringify({ url: example }),
47
+ })
48
+ }
49
+ />
50
+ ))}
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export default ImageSelector;
components/chat/index.tsx CHANGED
@@ -5,21 +5,25 @@ import { ChatPanel } from '@/components/chat/ChatPanel';
5
  import { ChatScrollAnchor } from '@/components/chat/ChatScrollAnchor';
6
  import ImageList from './ImageList';
7
  import useChatWithDataset from '../../lib/hooks/useChatWithDataset';
 
 
 
8
 
9
  export interface ChatProps extends React.ComponentProps<'div'> {
10
- id?: string;
11
  }
12
 
13
  export function Chat({ id, className }: ChatProps) {
14
  const { messages, append, reload, stop, isLoading, input, setInput } =
15
- useChatWithDataset();
16
 
17
  return (
18
  <>
19
  <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
20
  <div className="flex h-full">
21
- <div className="w-1/2 relative border-r-2 border-gray-300 overflow-auto">
22
- <ImageList />
 
23
  </div>
24
  <div className="w-1/2 relative overflow-auto">
25
  <ChatList messages={messages} />
 
5
  import { ChatScrollAnchor } from '@/components/chat/ChatScrollAnchor';
6
  import ImageList from './ImageList';
7
  import useChatWithDataset from '../../lib/hooks/useChatWithDataset';
8
+ import { useChat } from 'ai/react';
9
+ import { Button } from '../ui/Button';
10
+ import ImageSelector from './ImageSelector';
11
 
12
  export interface ChatProps extends React.ComponentProps<'div'> {
13
+ id: string;
14
  }
15
 
16
  export function Chat({ id, className }: ChatProps) {
17
  const { messages, append, reload, stop, isLoading, input, setInput } =
18
+ useChat();
19
 
20
  return (
21
  <>
22
  <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
23
  <div className="flex h-full">
24
+ <div className="w-1/2 relative border-r border-gray-400 overflow-auto">
25
+ {/* <ImageList /> */}
26
+ <ImageSelector />
27
  </div>
28
  <div className="w-1/2 relative overflow-auto">
29
  <ChatList messages={messages} />
components/{sidebar β†’ project-sidebar}/ProjectCard.tsx RENAMED
File without changes
components/{sidebar β†’ project-sidebar}/ProjectListSideBar.tsx RENAMED
File without changes
components/ui/Icons.tsx CHANGED
@@ -511,7 +511,7 @@ function IconLoading({ className, ...props }: React.ComponentProps<'svg'>) {
511
  return (
512
  <svg
513
  aria-hidden="true"
514
- className="size-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
515
  viewBox="0 0 100 101"
516
  fill="none"
517
  xmlns="http://www.w3.org/2000/svg"
 
511
  return (
512
  <svg
513
  aria-hidden="true"
514
+ className="size-8 text-gray-200 animate-spin dark:text-gray-600 fill-gray-600"
515
  viewBox="0 0 100 101"
516
  fill="none"
517
  xmlns="http://www.w3.org/2000/svg"
app/project/loading.tsx β†’ components/ui/Loading.tsx RENAMED
@@ -1,6 +1,6 @@
1
  import { IconLoading } from '@/components/ui/Icons';
2
 
3
- export default async function Loading() {
4
  return (
5
  <div className="flex justify-center items-center size-full text-sm">
6
  <IconLoading />
 
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 />
lib/fetch/index.ts CHANGED
@@ -7,7 +7,7 @@ interface ApiResponse<T> {
7
  data: T;
8
  }
9
 
10
- const apiBuilder = <Params extends object | void, Resp>(
11
  path: string,
12
  options?: {
13
  // default to GET
@@ -18,18 +18,19 @@ const apiBuilder = <Params extends object | void, Resp>(
18
  ) => {
19
  return async (params: Params): Promise<Resp> => {
20
  const session = await auth();
21
- if (!session?.user?.email) {
 
 
22
  throw new Response('Unauthorized', {
23
  status: 401,
24
  });
25
  }
26
 
27
- const adminEmail = session.user.email;
28
  const sessionUser = {
29
- id: uuidV5(adminEmail, uuidV5.URL),
30
  orgId: '-1024',
31
- email: adminEmail,
32
- username: adminEmail.split('@')[0],
33
  userRole: 'adminPortal',
34
  bucket: 'fake_bucket',
35
  };
@@ -63,7 +64,6 @@ const apiBuilder = <Params extends object | void, Resp>(
63
 
64
  fetchParams.body = formData;
65
  }
66
- console.log('[Ming] ~ return ~ fetchParams:', fetchParams, url.toString());
67
 
68
  const res = await fetch(url.toString(), fetchParams);
69
 
@@ -94,7 +94,7 @@ export type ProjectBaseInfo = {
94
  * 3. projects not containing media or only contain sample media
95
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
96
  */
97
- export const fetchRecentProjectList = apiBuilder<void, ProjectBaseInfo[]>(
98
  'api/admin/projects/recent',
99
  );
100
 
@@ -117,19 +117,7 @@ export type MediaDetails = {
117
  * Randomly fetch 10 media from a given project
118
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
119
  */
120
- export const fetchProjectMedia = apiBuilder<
121
  { projectId: number },
122
  MediaDetails[]
123
  >('api/admin/project/media');
124
-
125
- /**
126
- * Call vision agent
127
- * @author https://github.com/landing-ai/public-rest-api/pull/36
128
- */
129
- export const postAgentChat = apiBuilder<
130
- { input: string; image: string },
131
- MediaDetails[]
132
- >('v1/agent/chat?agent_class=vision_agent', {
133
- method: 'POST',
134
- prefix: 'api.dev',
135
- });
 
7
  data: T;
8
  }
9
 
10
+ const clefApiBuilder = <Params extends object | void, Resp>(
11
  path: string,
12
  options?: {
13
  // default to GET
 
18
  ) => {
19
  return async (params: Params): Promise<Resp> => {
20
  const session = await auth();
21
+ const email = session?.user?.email;
22
+
23
+ if (!email || !email.endsWith('@landing.ai')) {
24
  throw new Response('Unauthorized', {
25
  status: 401,
26
  });
27
  }
28
 
 
29
  const sessionUser = {
30
+ id: uuidV5(email, uuidV5.URL),
31
  orgId: '-1024',
32
+ email: email,
33
+ username: email.split('@')[0],
34
  userRole: 'adminPortal',
35
  bucket: 'fake_bucket',
36
  };
 
64
 
65
  fetchParams.body = formData;
66
  }
 
67
 
68
  const res = await fetch(url.toString(), fetchParams);
69
 
 
94
  * 3. projects not containing media or only contain sample media
95
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
96
  */
97
+ export const fetchRecentProjectList = clefApiBuilder<void, ProjectBaseInfo[]>(
98
  'api/admin/projects/recent',
99
  );
100
 
 
117
  * Randomly fetch 10 media from a given project
118
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
119
  */
120
+ export const fetchProjectMedia = clefApiBuilder<
121
  { projectId: number },
122
  MediaDetails[]
123
  >('api/admin/project/media');
 
 
 
 
 
 
 
 
 
 
 
 
lib/kv/chat.ts ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server';
2
+
3
+ import { revalidatePath } from 'next/cache';
4
+ import { redirect } from 'next/navigation';
5
+ import { kv } from '@vercel/kv';
6
+
7
+ import { auth } from '@/auth';
8
+ import { type Chat } from '@/lib/types';
9
+
10
+ export async function getKVChats() {
11
+ const session = await auth();
12
+ const email = session?.user?.email;
13
+
14
+ if (!email) {
15
+ return [];
16
+ }
17
+
18
+ try {
19
+ const pipeline = kv.pipeline();
20
+ const chats: string[] = await kv.zrange(`user:chat:${email}`, 0, -1, {
21
+ rev: true,
22
+ });
23
+
24
+ for (const chat of chats) {
25
+ pipeline.hgetall(chat);
26
+ }
27
+
28
+ const results = await pipeline.exec();
29
+
30
+ return results as Chat[];
31
+ } catch (error) {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ export async function getKVChat(id: string, userId: string) {
37
+ const chat = await kv.hgetall<Chat>(`chat:${id}`);
38
+
39
+ if (!chat || (userId && chat.userId !== userId)) {
40
+ return null;
41
+ }
42
+
43
+ return chat;
44
+ }
45
+
46
+ export async function removeKVChat({ id, path }: { id: string; path: string }) {
47
+ const session = await auth();
48
+
49
+ if (!session) {
50
+ return {
51
+ error: 'Unauthorized',
52
+ };
53
+ }
54
+
55
+ //Convert uid to string for consistent comparison with session.user.id
56
+ const uid = String(await kv.hget(`chat:${id}`, 'userId'));
57
+
58
+ if (uid !== session?.user?.id) {
59
+ return {
60
+ error: 'Unauthorized',
61
+ };
62
+ }
63
+
64
+ await kv.del(`chat:${id}`);
65
+ await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`);
66
+
67
+ revalidatePath('/');
68
+ return revalidatePath(path);
69
+ }
lib/types.ts CHANGED
@@ -1,5 +1,4 @@
1
  import { type Message } from 'ai';
2
- import { CreateMessage } from 'ai/react/dist';
3
 
4
  export interface Chat extends Record<string, any> {
5
  id: string;
@@ -8,7 +7,6 @@ export interface Chat extends Record<string, any> {
8
  userId: string;
9
  path: string;
10
  messages: Message[];
11
- sharePath?: string;
12
  }
13
 
14
  export type ServerActionResult<Result> = Promise<
 
1
  import { type Message } from 'ai';
 
2
 
3
  export interface Chat extends Record<string, any> {
4
  id: string;
 
7
  userId: string;
8
  path: string;
9
  messages: Message[];
 
10
  }
11
 
12
  export type ServerActionResult<Result> = Promise<
lib/utils.ts CHANGED
@@ -1,43 +1,44 @@
1
- import { clsx, type ClassValue } from 'clsx'
2
- import { customAlphabet } from 'nanoid'
3
- import { twMerge } from 'tailwind-merge'
4
 
5
  export function cn(...inputs: ClassValue[]) {
6
- return twMerge(clsx(inputs))
7
  }
8
 
9
  export const nanoid = customAlphabet(
10
- '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
11
- 7
12
- ) // 7-character random string
13
 
14
  export async function fetcher<JSON = any>(
15
- input: RequestInfo,
16
- init?: RequestInit
17
  ): Promise<JSON> {
18
- const res = await fetch(input, init)
 
19
 
20
- if (!res.ok) {
21
- const json = await res.json()
22
- if (json.error) {
23
- const error = new Error(json.error) as Error & {
24
- status: number
25
- }
26
- error.status = res.status
27
- throw error
28
- } else {
29
- throw new Error('An unexpected error occurred')
30
- }
31
- }
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
  }
 
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { customAlphabet } from 'nanoid';
3
+ import { twMerge } from 'tailwind-merge';
4
 
5
  export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
  }
8
 
9
  export const nanoid = customAlphabet(
10
+ '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
11
+ 7,
12
+ ); // 7-character random string
13
 
14
  export async function fetcher<JSON = any>(
15
+ input: RequestInfo,
16
+ init?: RequestInit,
17
  ): Promise<JSON> {
18
+ const res = await fetch(input, init);
19
+ console.log('[Ming] ~ res:', res);
20
 
21
+ if (!res.ok) {
22
+ const json = await res.json();
23
+ if (json.error) {
24
+ const error = new Error(json.error) as Error & {
25
+ status: number;
26
+ };
27
+ error.status = res.status;
28
+ throw error;
29
+ } else {
30
+ throw new Error('An unexpected error occurred');
31
+ }
32
+ }
33
 
34
+ return res.json();
35
  }
36
 
37
  export function formatDate(input: string | number | Date): string {
38
+ const date = new Date(input);
39
+ return date.toLocaleDateString('en-US', {
40
+ month: 'long',
41
+ day: 'numeric',
42
+ year: 'numeric',
43
+ });
44
  }
package.json CHANGED
@@ -20,6 +20,7 @@
20
  "@radix-ui/react-slot": "^1.0.2",
21
  "@radix-ui/react-switch": "^1.0.3",
22
  "@radix-ui/react-tooltip": "^1.0.7",
 
23
  "ai": "^2.2.31",
24
  "class-variance-authority": "^0.7.0",
25
  "clsx": "^2.1.0",
@@ -66,5 +67,5 @@
66
  "tailwindcss-animate": "^1.0.7",
67
  "typescript": "^5.3.3"
68
  },
69
- "packageManager": "pnpm@8.6.3"
70
  }
 
20
  "@radix-ui/react-slot": "^1.0.2",
21
  "@radix-ui/react-switch": "^1.0.3",
22
  "@radix-ui/react-tooltip": "^1.0.7",
23
+ "@vercel/kv": "^1.0.1",
24
  "ai": "^2.2.31",
25
  "class-variance-authority": "^0.7.0",
26
  "clsx": "^2.1.0",
 
67
  "tailwindcss-animate": "^1.0.7",
68
  "typescript": "^5.3.3"
69
  },
70
+ "packageManager": "pnpm@9.0.1"
71
  }
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff