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 |
}
|