Spaces:
Running
Running
Merge branch 'main' of github.com:landing-ai/vision-agent-ui
Browse files- app/api/upload/route.ts +8 -15
- app/chat/[id]/page.tsx +8 -6
- app/globals.css +51 -0
- components/chat-sidebar/ChatCard.tsx +10 -5
- components/chat/ChatDataLoad.tsx +11 -0
- components/chat/ChatMessage.tsx +89 -3
- components/chat/ImageSelector.tsx +2 -2
- components/chat/PromptForm.tsx +2 -2
- components/ui/ImageLoader.tsx +11 -0
- lib/hooks/useVisionAgent.tsx +21 -2
- lib/kv/chat.ts +39 -7
app/api/upload/route.ts
CHANGED
@@ -1,8 +1,9 @@
|
|
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 {
|
6 |
|
7 |
/**
|
8 |
* TODO: this should be replaced with actual upload to S3
|
@@ -11,7 +12,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 +38,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 +51,13 @@ 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 |
-
|
59 |
-
|
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 |
+
import { revalidatePath } from 'next/cache';
|
7 |
|
8 |
/**
|
9 |
* TODO: this should be replaced with actual upload to S3
|
|
|
12 |
*/
|
13 |
export async function POST(req: Request): Promise<Response> {
|
14 |
const session = await auth();
|
15 |
+
const user = session?.user?.email ?? 'anonymous';
|
16 |
// if (!email) {
|
17 |
// return new Response('Unauthorized', {
|
18 |
// status: 401,
|
|
|
38 |
|
39 |
let urlToSave = url;
|
40 |
if (base64) {
|
41 |
+
const fileName = `${user}/${id}/${Date.now()}-image.jpg`;
|
42 |
const res = await upload(base64, fileName, fileType ?? 'image/png');
|
43 |
if (res.ok) {
|
44 |
console.log('Image uploaded successfully');
|
|
|
51 |
const payload: ChatEntity = {
|
52 |
url: urlToSave!, // TODO can be uploaded as well
|
53 |
id,
|
54 |
+
user,
|
55 |
messages: initMessages ?? [],
|
56 |
};
|
57 |
|
58 |
+
await createKVChat(payload);
|
59 |
+
|
60 |
+
revalidatePath('/chat', 'layout');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
return Response.json(payload);
|
63 |
} catch (error) {
|
app/chat/[id]/page.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
-
import
|
2 |
-
import {
|
3 |
-
import
|
4 |
|
5 |
interface PageProps {
|
6 |
params: {
|
@@ -11,7 +11,9 @@ interface PageProps {
|
|
11 |
export default async function Page({ params }: PageProps) {
|
12 |
const { id: chatId } = params;
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
|
|
17 |
}
|
|
|
1 |
+
import ChatDataLoad from '@/components/chat/ChatDataLoad';
|
2 |
+
import { Suspense } from 'react';
|
3 |
+
import Loading from '@/components/ui/Loading';
|
4 |
|
5 |
interface PageProps {
|
6 |
params: {
|
|
|
11 |
export default async function Page({ params }: PageProps) {
|
12 |
const { id: chatId } = params;
|
13 |
|
14 |
+
return (
|
15 |
+
<Suspense fallback={<Loading />}>
|
16 |
+
<ChatDataLoad chatId={chatId} />
|
17 |
+
</Suspense>
|
18 |
+
);
|
19 |
}
|
app/globals.css
CHANGED
@@ -110,3 +110,54 @@
|
|
110 |
pointer-events: none;
|
111 |
}
|
112 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
pointer-events: none;
|
111 |
}
|
112 |
}
|
113 |
+
|
114 |
+
/* Light theme. */
|
115 |
+
:root {
|
116 |
+
--color-canvas-default: #ffffff;
|
117 |
+
--color-canvas-subtle: #f6f8fa;
|
118 |
+
--color-border-default: #d0d7de;
|
119 |
+
--color-border-muted: hsla(210, 18%, 87%, 1);
|
120 |
+
}
|
121 |
+
|
122 |
+
/* Dark theme. */
|
123 |
+
@media (prefers-color-scheme: dark) {
|
124 |
+
:root {
|
125 |
+
--color-canvas-default: #0d1117;
|
126 |
+
--color-canvas-subtle: #161b22;
|
127 |
+
--color-border-default: #30363d;
|
128 |
+
--color-border-muted: #21262d;
|
129 |
+
}
|
130 |
+
}
|
131 |
+
|
132 |
+
table {
|
133 |
+
border-spacing: 0;
|
134 |
+
border-collapse: collapse;
|
135 |
+
display: block;
|
136 |
+
margin-top: 0;
|
137 |
+
margin-bottom: 16px;
|
138 |
+
width: max-content;
|
139 |
+
max-width: 100%;
|
140 |
+
overflow: auto;
|
141 |
+
}
|
142 |
+
|
143 |
+
tr {
|
144 |
+
border-top: 1px solid var(--color-border-muted);
|
145 |
+
}
|
146 |
+
|
147 |
+
tr:nth-child(2n) {
|
148 |
+
background-color: var(--color-canvas-subtle);
|
149 |
+
}
|
150 |
+
|
151 |
+
td,
|
152 |
+
th {
|
153 |
+
padding: 6px 13px;
|
154 |
+
border: 1px solid var(--color-border-default);
|
155 |
+
}
|
156 |
+
|
157 |
+
th {
|
158 |
+
font-weight: 600;
|
159 |
+
}
|
160 |
+
|
161 |
+
table img {
|
162 |
+
background-color: transparent;
|
163 |
+
}
|
components/chat-sidebar/ChatCard.tsx
CHANGED
@@ -18,7 +18,7 @@ export const ChatCardLayout: React.FC<
|
|
18 |
return (
|
19 |
<Link
|
20 |
className={cn(
|
21 |
-
'p-2 m-2 bg-background
|
22 |
classNames,
|
23 |
)}
|
24 |
href={link}
|
@@ -31,7 +31,12 @@ export const ChatCardLayout: React.FC<
|
|
31 |
const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
32 |
const { id: chatIdFromParam } = useParams();
|
33 |
const { id, url, messages, user } = chat;
|
34 |
-
const firstMessage = messages?.[0]?.content
|
|
|
|
|
|
|
|
|
|
|
35 |
return (
|
36 |
<ChatCardLayout
|
37 |
link={`/chat/${id}`}
|
@@ -45,9 +50,9 @@ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
|
45 |
height={50}
|
46 |
className="rounded w-1/4 "
|
47 |
/>
|
48 |
-
<
|
49 |
-
|
50 |
-
</
|
51 |
</div>
|
52 |
</ChatCardLayout>
|
53 |
);
|
|
|
18 |
return (
|
19 |
<Link
|
20 |
className={cn(
|
21 |
+
'p-2 m-2 bg-background max-h-[100px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
|
22 |
classNames,
|
23 |
)}
|
24 |
href={link}
|
|
|
31 |
const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
|
32 |
const { id: chatIdFromParam } = useParams();
|
33 |
const { id, url, messages, user } = chat;
|
34 |
+
const firstMessage = messages?.[0]?.content;
|
35 |
+
const title = firstMessage
|
36 |
+
? firstMessage.length > 50
|
37 |
+
? firstMessage.slice(0, 50) + '...'
|
38 |
+
: firstMessage
|
39 |
+
: '(No messages yet)';
|
40 |
return (
|
41 |
<ChatCardLayout
|
42 |
link={`/chat/${id}`}
|
|
|
50 |
height={50}
|
51 |
className="rounded w-1/4 "
|
52 |
/>
|
53 |
+
<div className="flex items-start h-full">
|
54 |
+
<p className="text-sm w-3/4 ml-3">{title}</p>
|
55 |
+
</div>
|
56 |
</div>
|
57 |
</ChatCardLayout>
|
58 |
);
|
components/chat/ChatDataLoad.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { getKVChat } from '@/lib/kv/chat';
|
2 |
+
import { Chat } from '.';
|
3 |
+
|
4 |
+
export interface ChatDataLoadProps {
|
5 |
+
chatId: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
export default async function ChatDataLoad({ chatId }: ChatDataLoadProps) {
|
9 |
+
const chat = await getKVChat(chatId);
|
10 |
+
return <Chat chat={chat} />;
|
11 |
+
}
|
components/chat/ChatMessage.tsx
CHANGED
@@ -15,7 +15,67 @@ export interface ChatMessageProps {
|
|
15 |
message: MessageBase;
|
16 |
}
|
17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
return (
|
20 |
<div className={cn('group relative mb-4 flex items-start')} {...props}>
|
21 |
<div
|
@@ -29,12 +89,39 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
29 |
{message.role === 'user' ? <IconUser /> : <IconOpenAI />}
|
30 |
</div>
|
31 |
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
<MemoizedReactMarkdown
|
33 |
className="break-words"
|
34 |
remarkPlugins={[remarkGfm, remarkMath]}
|
35 |
components={{
|
36 |
p({ children }) {
|
37 |
-
return
|
|
|
|
|
38 |
},
|
39 |
code({ node, inline, className, children, ...props }) {
|
40 |
if (children.length) {
|
@@ -48,7 +135,6 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
48 |
}
|
49 |
|
50 |
const match = /language-(\w+)/.exec(className || '');
|
51 |
-
|
52 |
if (inline) {
|
53 |
return (
|
54 |
<code className={className} {...props}>
|
@@ -68,7 +154,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
68 |
},
|
69 |
}}
|
70 |
>
|
71 |
-
{
|
72 |
</MemoizedReactMarkdown>
|
73 |
<ChatMessageActions message={message} />
|
74 |
</div>
|
|
|
15 |
message: MessageBase;
|
16 |
}
|
17 |
|
18 |
+
const PAIRS: Record<string, string> = {
|
19 |
+
'┍': '┑',
|
20 |
+
'┝': '┥',
|
21 |
+
'├': '┤',
|
22 |
+
'┕': '┙',
|
23 |
+
};
|
24 |
+
|
25 |
+
const MIDDLE_STARTER = '┝';
|
26 |
+
const MIDDLE_SEPARATOR = '┿';
|
27 |
+
|
28 |
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
29 |
+
const cleanupMessage = ({ content, role }: MessageBase) => {
|
30 |
+
if (role === 'user') {
|
31 |
+
return {
|
32 |
+
content,
|
33 |
+
};
|
34 |
+
}
|
35 |
+
const [logs = '', answer = ''] = content.split('<ANSWER>');
|
36 |
+
const cleanedLogs = [];
|
37 |
+
let left = 0;
|
38 |
+
let right = 0;
|
39 |
+
while (right < logs.length) {
|
40 |
+
if (Object.keys(PAIRS).includes(content[right])) {
|
41 |
+
cleanedLogs.push(content.substring(left, right));
|
42 |
+
left = right++;
|
43 |
+
while (
|
44 |
+
right < content.length &&
|
45 |
+
PAIRS[content[left]] !== content[right]
|
46 |
+
) {
|
47 |
+
right++;
|
48 |
+
}
|
49 |
+
if (content[left] === MIDDLE_STARTER) {
|
50 |
+
// add the text alignment so it can be shown as a table
|
51 |
+
const separators = logs
|
52 |
+
.substring(left, right)
|
53 |
+
.split(MIDDLE_SEPARATOR).length;
|
54 |
+
if (separators > 0) {
|
55 |
+
cleanedLogs.push(
|
56 |
+
Array(separators + 1)
|
57 |
+
.fill('|')
|
58 |
+
.join(' :- '),
|
59 |
+
);
|
60 |
+
}
|
61 |
+
}
|
62 |
+
left = ++right;
|
63 |
+
} else {
|
64 |
+
right++;
|
65 |
+
}
|
66 |
+
}
|
67 |
+
cleanedLogs.push(content.substring(left, right));
|
68 |
+
return {
|
69 |
+
logs: cleanedLogs
|
70 |
+
.join('')
|
71 |
+
.replace(/│/g, '|')
|
72 |
+
.split('|\n\n|')
|
73 |
+
.join('|\n|'),
|
74 |
+
content: answer.replace('</</ANSWER>', '').replace('</ANSWER>', ''),
|
75 |
+
};
|
76 |
+
};
|
77 |
+
|
78 |
+
const { logs, content } = cleanupMessage(message);
|
79 |
return (
|
80 |
<div className={cn('group relative mb-4 flex items-start')} {...props}>
|
81 |
<div
|
|
|
89 |
{message.role === 'user' ? <IconUser /> : <IconOpenAI />}
|
90 |
</div>
|
91 |
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
92 |
+
{logs && message.role !== 'user' && (
|
93 |
+
<div className="bg-slate-100 dark:bg-slate-900 mb-4 p-4 max-h-[400px] overflow-auto">
|
94 |
+
<div className="text-xl font-bold">Thinking Process</div>
|
95 |
+
<MemoizedReactMarkdown
|
96 |
+
className="break-words text-sm"
|
97 |
+
remarkPlugins={[remarkGfm, remarkMath]}
|
98 |
+
components={{
|
99 |
+
p({ children }) {
|
100 |
+
return (
|
101 |
+
<p className="my-2 last:mb-0 whitespace-pre-line">
|
102 |
+
{children}
|
103 |
+
</p>
|
104 |
+
);
|
105 |
+
},
|
106 |
+
code({ children, ...props }) {
|
107 |
+
return (
|
108 |
+
<code className="whitespace-pre-line">{children}</code>
|
109 |
+
);
|
110 |
+
},
|
111 |
+
}}
|
112 |
+
>
|
113 |
+
{logs}
|
114 |
+
</MemoizedReactMarkdown>
|
115 |
+
</div>
|
116 |
+
)}
|
117 |
<MemoizedReactMarkdown
|
118 |
className="break-words"
|
119 |
remarkPlugins={[remarkGfm, remarkMath]}
|
120 |
components={{
|
121 |
p({ children }) {
|
122 |
+
return (
|
123 |
+
<p className="my-2 last:mb-0 whitespace-pre-line">{children}</p>
|
124 |
+
);
|
125 |
},
|
126 |
code({ node, inline, className, children, ...props }) {
|
127 |
if (children.length) {
|
|
|
135 |
}
|
136 |
|
137 |
const match = /language-(\w+)/.exec(className || '');
|
|
|
138 |
if (inline) {
|
139 |
return (
|
140 |
<code className={className} {...props}>
|
|
|
154 |
},
|
155 |
}}
|
156 |
>
|
157 |
+
{content}
|
158 |
</MemoizedReactMarkdown>
|
159 |
<ChatMessageActions message={message} />
|
160 |
</div>
|
components/chat/ImageSelector.tsx
CHANGED
@@ -51,13 +51,13 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
|
51 |
)}
|
52 |
>
|
53 |
<input {...getInputProps()} />
|
54 |
-
<
|
55 |
{isUploading ? (
|
56 |
<Loading />
|
57 |
) : (
|
58 |
'Drag or drop image here, or click to select images'
|
59 |
)}
|
60 |
-
</
|
61 |
</div>
|
62 |
);
|
63 |
};
|
|
|
51 |
)}
|
52 |
>
|
53 |
<input {...getInputProps()} />
|
54 |
+
<div className="text-gray-400 text-lg">
|
55 |
{isUploading ? (
|
56 |
<Loading />
|
57 |
) : (
|
58 |
'Drag or drop image here, or click to select images'
|
59 |
)}
|
60 |
+
</div>
|
61 |
</div>
|
62 |
);
|
63 |
};
|
components/chat/PromptForm.tsx
CHANGED
@@ -54,8 +54,8 @@ export function PromptForm({
|
|
54 |
<TooltipTrigger asChild>
|
55 |
<Image
|
56 |
src={url}
|
57 |
-
width={
|
58 |
-
height={
|
59 |
alt="chosen image"
|
60 |
className="w-1/5 my-4 mx-2 rounded-md"
|
61 |
/>
|
|
|
54 |
<TooltipTrigger asChild>
|
55 |
<Image
|
56 |
src={url}
|
57 |
+
width={250}
|
58 |
+
height={250}
|
59 |
alt="chosen image"
|
60 |
className="w-1/5 my-4 mx-2 rounded-md"
|
61 |
/>
|
components/ui/ImageLoader.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// import Image from 'next/image';
|
2 |
+
// import React from 'react';
|
3 |
+
|
4 |
+
// type ImageLoaderProps = typeof Image;
|
5 |
+
|
6 |
+
// const ImageLoader: React.FC<ImageLoaderProps> = props => {
|
7 |
+
// const { alt, width, height } = props;
|
8 |
+
// return <Image alt={alt} />;
|
9 |
+
// };
|
10 |
+
|
11 |
+
// export default ImageLoader;
|
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');
|
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');
|
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 |
}
|