Spaces:
Sleeping
Sleeping
Save chat message to KV store and UI revalidation (#14)
Browse files- app/api/upload/route.ts +5 -15
- components/chat-sidebar/ChatCard.tsx +1 -1
- lib/hooks/useVisionAgent.tsx +21 -2
- lib/kv/chat.ts +39 -7
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
|
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 = `${
|
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
|
54 |
messages: initMessages ?? [],
|
55 |
};
|
56 |
|
57 |
-
await
|
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
|
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 |
-
|
15 |
-
}
|
16 |
-
return { email, isAdmin: email
|
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 |
}
|