MingruiZhang commited on
Commit
84c9f51
1 Parent(s): 76a5b74

Save chat message to KV store and UI revalidation (#14)

Browse files
app/api/upload/route.ts CHANGED
@@ -1,8 +1,8 @@
1
  import { auth } from '@/auth';
2
  import { upload } from '@/lib/aws';
 
3
  import { ChatEntity, MessageBase } from '@/lib/types';
4
  import { nanoid } from '@/lib/utils';
5
- import { kv } from '@vercel/kv';
6
 
7
  /**
8
  * TODO: this should be replaced with actual upload to S3
@@ -11,7 +11,7 @@ import { kv } from '@vercel/kv';
11
  */
12
  export async function POST(req: Request): Promise<Response> {
13
  const session = await auth();
14
- const email = session?.user?.email;
15
  // if (!email) {
16
  // return new Response('Unauthorized', {
17
  // status: 401,
@@ -37,7 +37,7 @@ export async function POST(req: Request): Promise<Response> {
37
 
38
  let urlToSave = url;
39
  if (base64) {
40
- const fileName = `${email}/${id}/${Date.now()}-image.jpg`;
41
  const res = await upload(base64, fileName, fileType ?? 'image/png');
42
  if (res.ok) {
43
  console.log('Image uploaded successfully');
@@ -50,21 +50,11 @@ export async function POST(req: Request): Promise<Response> {
50
  const payload: ChatEntity = {
51
  url: urlToSave!, // TODO can be uploaded as well
52
  id,
53
- user: email || 'anonymous',
54
  messages: initMessages ?? [],
55
  };
56
 
57
- await kv.hmset(`chat:${id}`, payload);
58
- if (email) {
59
- await kv.zadd(`user:chat:${email}`, {
60
- score: Date.now(),
61
- member: `chat:${id}`,
62
- });
63
- }
64
- await kv.zadd('user:chat:all', {
65
- score: Date.now(),
66
- member: `chat:${id}`,
67
- });
68
 
69
  return Response.json(payload);
70
  } catch (error) {
 
1
  import { auth } from '@/auth';
2
  import { upload } from '@/lib/aws';
3
+ import { createKVChat } from '@/lib/kv/chat';
4
  import { ChatEntity, MessageBase } from '@/lib/types';
5
  import { nanoid } from '@/lib/utils';
 
6
 
7
  /**
8
  * TODO: this should be replaced with actual upload to S3
 
11
  */
12
  export async function POST(req: Request): Promise<Response> {
13
  const session = await auth();
14
+ const user = session?.user?.email ?? 'anonymous';
15
  // if (!email) {
16
  // return new Response('Unauthorized', {
17
  // status: 401,
 
37
 
38
  let urlToSave = url;
39
  if (base64) {
40
+ const fileName = `${user}/${id}/${Date.now()}-image.jpg`;
41
  const res = await upload(base64, fileName, fileType ?? 'image/png');
42
  if (res.ok) {
43
  console.log('Image uploaded successfully');
 
50
  const payload: ChatEntity = {
51
  url: urlToSave!, // TODO can be uploaded as well
52
  id,
53
+ user,
54
  messages: initMessages ?? [],
55
  };
56
 
57
+ await createKVChat(payload);
 
 
 
 
 
 
 
 
 
 
58
 
59
  return Response.json(payload);
60
  } catch (error) {
components/chat-sidebar/ChatCard.tsx CHANGED
@@ -46,7 +46,7 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
46
  className="rounded w-1/4 "
47
  />
48
  <p className="text-sm w-3/4 ml-3">
49
- {firstMessage ? firstMessage + ' ...' : '(No messages yet)'}
50
  </p>
51
  </div>
52
  </ChatCardLayout>
 
46
  className="rounded w-1/4 "
47
  />
48
  <p className="text-sm w-3/4 ml-3">
49
+ {firstMessage ?? '(No messages yet)'}
50
  </p>
51
  </div>
52
  </ChatCardLayout>
lib/hooks/useVisionAgent.tsx CHANGED
@@ -1,13 +1,14 @@
1
- import { useChat, type Message } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
  import { useEffect, useState } from 'react';
4
  import { ChatEntity, MessageBase } from '../types';
 
5
 
6
  const useVisionAgent = (chat: ChatEntity) => {
7
  const { messages: initialMessages, id, url } = chat;
8
  const {
9
  messages,
10
- append,
11
  reload,
12
  stop,
13
  isLoading,
@@ -22,6 +23,9 @@ const useVisionAgent = (chat: ChatEntity) => {
22
  toast.error(response.statusText);
23
  }
24
  },
 
 
 
25
  initialMessages: initialMessages,
26
  body: {
27
  url,
@@ -58,6 +62,16 @@ const useVisionAgent = (chat: ChatEntity) => {
58
  };
59
  }, [isLoading]);
60
 
 
 
 
 
 
 
 
 
 
 
61
  const assistantLoadingMessage = {
62
  id: 'loading',
63
  content: loadingDots,
@@ -71,6 +85,11 @@ const useVisionAgent = (chat: ChatEntity) => {
71
  ? [...messages, assistantLoadingMessage]
72
  : messages;
73
 
 
 
 
 
 
74
  return {
75
  messages: messageWithLoading as MessageBase[],
76
  append,
 
1
+ import { useChat, type Message, UseChatHelpers } from 'ai/react';
2
  import { toast } from 'react-hot-toast';
3
  import { useEffect, useState } from 'react';
4
  import { ChatEntity, MessageBase } from '../types';
5
+ import { saveKVChatMessage } from '../kv/chat';
6
 
7
  const useVisionAgent = (chat: ChatEntity) => {
8
  const { messages: initialMessages, id, url } = chat;
9
  const {
10
  messages,
11
+ append: appendRaw,
12
  reload,
13
  stop,
14
  isLoading,
 
23
  toast.error(response.statusText);
24
  }
25
  },
26
+ onFinish(message) {
27
+ saveKVChatMessage(id, message);
28
+ },
29
  initialMessages: initialMessages,
30
  body: {
31
  url,
 
62
  };
63
  }, [isLoading]);
64
 
65
+ useEffect(() => {
66
+ if (
67
+ !isLoading &&
68
+ messages.length &&
69
+ messages[messages.length - 1].role === 'user'
70
+ ) {
71
+ reload();
72
+ }
73
+ }, [isLoading, messages, reload]);
74
+
75
  const assistantLoadingMessage = {
76
  id: 'loading',
77
  content: loadingDots,
 
85
  ? [...messages, assistantLoadingMessage]
86
  : messages;
87
 
88
+ const append: UseChatHelpers['append'] = async message => {
89
+ await saveKVChatMessage(id, message as MessageBase);
90
+ return appendRaw(message);
91
+ };
92
+
93
  return {
94
  messages: messageWithLoading as MessageBase[],
95
  append,
lib/kv/chat.ts CHANGED
@@ -4,16 +4,17 @@ import { revalidatePath } from 'next/cache';
4
  import { kv } from '@vercel/kv';
5
 
6
  import { auth } from '@/auth';
7
- import { ChatEntity } from '@/lib/types';
8
- import { redirect } from 'next/navigation';
 
9
 
10
  async function authCheck() {
11
  const session = await auth();
12
  const email = session?.user?.email;
13
- if (!email) {
14
- redirect('/');
15
- }
16
- return { email, isAdmin: email.endsWith('landing.ai') };
17
  }
18
 
19
  export async function getKVChats() {
@@ -48,6 +49,37 @@ export async function getKVChat(id: string) {
48
  return chat;
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  export async function removeKVChat({ id, path }: { id: string; path: string }) {
52
  const session = await auth();
53
 
@@ -69,6 +101,6 @@ export async function removeKVChat({ id, path }: { id: string; path: string }) {
69
  await kv.del(`chat:${id}`);
70
  await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`);
71
 
72
- revalidatePath('/');
73
  return revalidatePath(path);
74
  }
 
4
  import { kv } from '@vercel/kv';
5
 
6
  import { auth } from '@/auth';
7
+ import { ChatEntity, MessageBase } from '@/lib/types';
8
+ import { notFound, redirect } from 'next/navigation';
9
+ import { nanoid } from '../utils';
10
 
11
  async function authCheck() {
12
  const session = await auth();
13
  const email = session?.user?.email;
14
+ // if (!email) {
15
+ // redirect('/');
16
+ // }
17
+ return { email, isAdmin: !!email?.endsWith('landing.ai') };
18
  }
19
 
20
  export async function getKVChats() {
 
49
  return chat;
50
  }
51
 
52
+ export async function createKVChat(chat: ChatEntity) {
53
+ // const { email, isAdmin } = await authCheck();
54
+ const { email } = await authCheck();
55
+
56
+ await kv.hmset(`chat:${chat.id}`, chat);
57
+ if (email) {
58
+ await kv.zadd(`user:chat:${email}`, {
59
+ score: Date.now(),
60
+ member: `chat:${chat.id}`,
61
+ });
62
+ }
63
+ await kv.zadd('user:chat:all', {
64
+ score: Date.now(),
65
+ member: `chat:${chat.id}`,
66
+ });
67
+ revalidatePath('/chat/layout', 'layout');
68
+ }
69
+
70
+ export async function saveKVChatMessage(id: string, message: MessageBase) {
71
+ const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
72
+ if (!chat) {
73
+ notFound();
74
+ }
75
+ const { messages } = chat;
76
+ await kv.hmset(`chat:${id}`, {
77
+ ...chat,
78
+ messages: [...messages, message],
79
+ });
80
+ revalidatePath('/chat/layout', 'layout');
81
+ }
82
+
83
  export async function removeKVChat({ id, path }: { id: string; path: string }) {
84
  const session = await auth();
85
 
 
101
  await kv.del(`chat:${id}`);
102
  await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`);
103
 
104
+ revalidatePath('/chat/layout', 'layout');
105
  return revalidatePath(path);
106
  }