Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
Commit
•
71679ee
1
Parent(s):
c792d09
feat: revalidate path / remove create chat api (use server action) / code block style (#53)
Browse files<img width="934" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/5669963/6fd046ea-e9b5-45e2-bbef-bff0f1e42ac3">
- app/api/chat/create/route.ts +0 -36
- app/chat/page.tsx +6 -11
- components/chat-sidebar/ChatCard.tsx +1 -0
- components/chat/ImageSelector.tsx +4 -9
- components/ui/CodeBlock.tsx +3 -4
- lib/db/functions.ts +12 -2
- lib/hooks/useChatWithMedia.ts +0 -87
- lib/types.ts +0 -31
- next.config.js +1 -0
- prisma/migrations/20240524012008_init/migration.sql +49 -0
- prisma/migrations/migration_lock.toml +3 -0
app/api/chat/create/route.ts
DELETED
@@ -1,36 +0,0 @@
|
|
1 |
-
import { dbPostCreateChat } from '@/lib/db/functions';
|
2 |
-
import { MessageRaw } from '@/lib/db/types';
|
3 |
-
import { withLogging } from '@/lib/logger';
|
4 |
-
import { revalidatePath } from 'next/cache';
|
5 |
-
|
6 |
-
/**
|
7 |
-
* @param req
|
8 |
-
* @returns
|
9 |
-
*/
|
10 |
-
export const POST = withLogging(
|
11 |
-
async (
|
12 |
-
_session,
|
13 |
-
json: {
|
14 |
-
id?: string;
|
15 |
-
url: string;
|
16 |
-
initMessages?: MessageRaw[];
|
17 |
-
},
|
18 |
-
): Promise<Response> => {
|
19 |
-
try {
|
20 |
-
const { url, id, initMessages } = json;
|
21 |
-
|
22 |
-
const response = await dbPostCreateChat({
|
23 |
-
id,
|
24 |
-
mediaUrl: url,
|
25 |
-
initMessages,
|
26 |
-
});
|
27 |
-
|
28 |
-
revalidatePath('/chat', 'layout');
|
29 |
-
return Response.json(response);
|
30 |
-
} catch (error) {
|
31 |
-
return new Response((error as Error).message, {
|
32 |
-
status: 400,
|
33 |
-
});
|
34 |
-
}
|
35 |
-
},
|
36 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/chat/page.tsx
CHANGED
@@ -14,7 +14,8 @@ import { IconDiscord, IconGitHub } from '@/components/ui/Icons';
|
|
14 |
import Link from 'next/link';
|
15 |
import { Button } from '@/components/ui/Button';
|
16 |
import Img from '@/components/ui/Img';
|
17 |
-
import {
|
|
|
18 |
|
19 |
// const EXAMPLE_URL = 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg';
|
20 |
const EXAMPLE_URL =
|
@@ -32,7 +33,7 @@ const exampleMessages = [
|
|
32 |
url: EXAMPLE_URL,
|
33 |
initMessages: [
|
34 |
{
|
35 |
-
role: 'user',
|
36 |
content:
|
37 |
EXAMPLE_PROMPT + '\n\n' + generateInputImageMarkdown(EXAMPLE_URL),
|
38 |
},
|
@@ -93,15 +94,9 @@ export default function Page() {
|
|
93 |
index > 1 && 'hidden md:block'
|
94 |
}`}
|
95 |
onClick={async () => {
|
96 |
-
const resp = await
|
97 |
-
|
98 |
-
|
99 |
-
'Content-Type': 'application/json',
|
100 |
-
},
|
101 |
-
body: JSON.stringify({
|
102 |
-
url: example.url,
|
103 |
-
initMessages: example.initMessages,
|
104 |
-
}),
|
105 |
});
|
106 |
if (resp) {
|
107 |
router.push(`/chat/${resp.id}`);
|
|
|
14 |
import Link from 'next/link';
|
15 |
import { Button } from '@/components/ui/Button';
|
16 |
import Img from '@/components/ui/Img';
|
17 |
+
import { MessageRaw } from '@/lib/db/types';
|
18 |
+
import { dbPostCreateChat } from '@/lib/db/functions';
|
19 |
|
20 |
// const EXAMPLE_URL = 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg';
|
21 |
const EXAMPLE_URL =
|
|
|
33 |
url: EXAMPLE_URL,
|
34 |
initMessages: [
|
35 |
{
|
36 |
+
role: 'user' as MessageRaw['role'],
|
37 |
content:
|
38 |
EXAMPLE_PROMPT + '\n\n' + generateInputImageMarkdown(EXAMPLE_URL),
|
39 |
},
|
|
|
94 |
index > 1 && 'hidden md:block'
|
95 |
}`}
|
96 |
onClick={async () => {
|
97 |
+
const resp = await dbPostCreateChat({
|
98 |
+
mediaUrl: example.url,
|
99 |
+
initMessages: example.initMessages,
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
});
|
101 |
if (resp) {
|
102 |
router.push(`/chat/${resp.id}`);
|
components/chat-sidebar/ChatCard.tsx
CHANGED
@@ -36,6 +36,7 @@ export const ChatCardLayout: React.FC<
|
|
36 |
|
37 |
const ChatCard: React.FC<ChatCardProps> = ({ chat, isAdminView }) => {
|
38 |
const { id: chatIdFromParam } = useParams();
|
|
|
39 |
const { id, mediaUrl, messages, userId, updatedAt } = chat;
|
40 |
if (!id) {
|
41 |
return null;
|
|
|
36 |
|
37 |
const ChatCard: React.FC<ChatCardProps> = ({ chat, isAdminView }) => {
|
38 |
const { id: chatIdFromParam } = useParams();
|
39 |
+
const router = useRouter();
|
40 |
const { id, mediaUrl, messages, userId, updatedAt } = chat;
|
41 |
if (!id) {
|
42 |
return null;
|
components/chat/ImageSelector.tsx
CHANGED
@@ -12,6 +12,7 @@ import {
|
|
12 |
getVideoCover,
|
13 |
} from '@rajesh896/video-thumbnails-generator';
|
14 |
import { ChatWithMessages } from '@/lib/db/types';
|
|
|
15 |
|
16 |
export interface ImageSelectorProps {}
|
17 |
|
@@ -87,15 +88,9 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
|
87 |
return upload(thumbnailFile, resp.id);
|
88 |
});
|
89 |
}
|
90 |
-
await
|
91 |
-
|
92 |
-
|
93 |
-
'Content-Type': 'application/json',
|
94 |
-
},
|
95 |
-
body: JSON.stringify({
|
96 |
-
id: resp.id,
|
97 |
-
url: resp.publicUrl,
|
98 |
-
}),
|
99 |
});
|
100 |
setUploading(false);
|
101 |
router.push(`/chat/${resp.id}`);
|
|
|
12 |
getVideoCover,
|
13 |
} from '@rajesh896/video-thumbnails-generator';
|
14 |
import { ChatWithMessages } from '@/lib/db/types';
|
15 |
+
import { dbPostCreateChat } from '@/lib/db/functions';
|
16 |
|
17 |
export interface ImageSelectorProps {}
|
18 |
|
|
|
88 |
return upload(thumbnailFile, resp.id);
|
89 |
});
|
90 |
}
|
91 |
+
await dbPostCreateChat({
|
92 |
+
id: resp.id,
|
93 |
+
mediaUrl: resp.publicUrl,
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
});
|
95 |
setUploading(false);
|
96 |
router.push(`/chat/${resp.id}`);
|
components/ui/CodeBlock.tsx
CHANGED
@@ -91,10 +91,9 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
|
91 |
if (isCopied) return;
|
92 |
copyToClipboard(value);
|
93 |
};
|
94 |
-
|
95 |
return (
|
96 |
-
<div className="relative w-full font-sans codeblock bg-zinc-950">
|
97 |
-
<div className="flex items-center justify-between w-full
|
98 |
<span className="text-xs lowercase">{language}</span>
|
99 |
<div className="flex items-center space-x-1">
|
100 |
<Button
|
@@ -126,7 +125,7 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
|
126 |
margin: 0,
|
127 |
width: '100%',
|
128 |
background: 'transparent',
|
129 |
-
padding: '1.5rem 1rem',
|
130 |
}}
|
131 |
lineNumberStyle={{
|
132 |
userSelect: 'none',
|
|
|
91 |
if (isCopied) return;
|
92 |
copyToClipboard(value);
|
93 |
};
|
|
|
94 |
return (
|
95 |
+
<div className="relative w-full font-sans codeblock bg-zinc-950 rounded-lg overflow-hidden">
|
96 |
+
<div className="flex items-center justify-between w-full pl-8 pr-4 pt-2 text-zinc-100">
|
97 |
<span className="text-xs lowercase">{language}</span>
|
98 |
<div className="flex items-center space-x-1">
|
99 |
<Button
|
|
|
125 |
margin: 0,
|
126 |
width: '100%',
|
127 |
background: 'transparent',
|
128 |
+
padding: '0.5rem 1rem 1.5rem 1rem',
|
129 |
}}
|
130 |
lineNumberStyle={{
|
131 |
userSelect: 'none',
|
lib/db/functions.ts
CHANGED
@@ -3,6 +3,8 @@
|
|
3 |
import { sessionUser } from '@/auth';
|
4 |
import prisma from './prisma';
|
5 |
import { ChatWithMessages, MessageRaw } from './types';
|
|
|
|
|
6 |
|
7 |
/**
|
8 |
* Finds or creates a user in the database based on the provided email and name.
|
@@ -45,6 +47,7 @@ export async function dbGetAllChat(): Promise<ChatWithMessages[]> {
|
|
45 |
where: { userId },
|
46 |
include: {
|
47 |
messages: true,
|
|
|
48 |
},
|
49 |
});
|
50 |
}
|
@@ -89,7 +92,7 @@ export async function dbPostCreateChat({
|
|
89 |
}
|
90 |
: {};
|
91 |
try {
|
92 |
-
|
93 |
data: {
|
94 |
id,
|
95 |
mediaUrl: mediaUrl,
|
@@ -105,6 +108,9 @@ export async function dbPostCreateChat({
|
|
105 |
messages: true,
|
106 |
},
|
107 |
});
|
|
|
|
|
|
|
108 |
} catch (error) {
|
109 |
console.error(error);
|
110 |
}
|
@@ -139,7 +145,11 @@ export async function dbPostCreateMessage(chatId: string, message: MessageRaw) {
|
|
139 |
}
|
140 |
|
141 |
export async function dbDeleteChat(chatId: string) {
|
142 |
-
|
143 |
where: { id: chatId },
|
144 |
});
|
|
|
|
|
|
|
|
|
145 |
}
|
|
|
3 |
import { sessionUser } from '@/auth';
|
4 |
import prisma from './prisma';
|
5 |
import { ChatWithMessages, MessageRaw } from './types';
|
6 |
+
import { revalidatePath } from 'next/cache';
|
7 |
+
import { redirect } from 'next/navigation';
|
8 |
|
9 |
/**
|
10 |
* Finds or creates a user in the database based on the provided email and name.
|
|
|
47 |
where: { userId },
|
48 |
include: {
|
49 |
messages: true,
|
50 |
+
user: true,
|
51 |
},
|
52 |
});
|
53 |
}
|
|
|
92 |
}
|
93 |
: {};
|
94 |
try {
|
95 |
+
const response = await prisma.chat.create({
|
96 |
data: {
|
97 |
id,
|
98 |
mediaUrl: mediaUrl,
|
|
|
108 |
messages: true,
|
109 |
},
|
110 |
});
|
111 |
+
|
112 |
+
revalidatePath('/chat', 'layout');
|
113 |
+
return response;
|
114 |
} catch (error) {
|
115 |
console.error(error);
|
116 |
}
|
|
|
145 |
}
|
146 |
|
147 |
export async function dbDeleteChat(chatId: string) {
|
148 |
+
await prisma.chat.delete({
|
149 |
where: { id: chatId },
|
150 |
});
|
151 |
+
|
152 |
+
revalidatePath('/chat', 'layout');
|
153 |
+
|
154 |
+
return;
|
155 |
}
|
lib/hooks/useChatWithMedia.ts
DELETED
@@ -1,87 +0,0 @@
|
|
1 |
-
import { useChat, type Message } from 'ai/react';
|
2 |
-
import { toast } from 'react-hot-toast';
|
3 |
-
import { useEffect, useState } from 'react';
|
4 |
-
import { MediaDetails } from '../fetch';
|
5 |
-
import { MessageWithSelectedDataset } from '../types';
|
6 |
-
|
7 |
-
const useChatWithMedia = (mediaList: MediaDetails[]) => {
|
8 |
-
const {
|
9 |
-
messages,
|
10 |
-
append,
|
11 |
-
reload,
|
12 |
-
stop,
|
13 |
-
isLoading,
|
14 |
-
input,
|
15 |
-
setInput,
|
16 |
-
setMessages,
|
17 |
-
} = useChat({
|
18 |
-
sendExtraMessageFields: true,
|
19 |
-
onResponse(response) {
|
20 |
-
if (response.status !== 200) {
|
21 |
-
toast.error(response.statusText);
|
22 |
-
}
|
23 |
-
},
|
24 |
-
initialMessages: [
|
25 |
-
{
|
26 |
-
id: 'system',
|
27 |
-
content: `For the full conversation, user have provided the following images: ${mediaList.map(media => media.name)}. Please help reply to user regarding these images`,
|
28 |
-
dataset: mediaList,
|
29 |
-
role: 'system',
|
30 |
-
},
|
31 |
-
] as MessageWithSelectedDataset[],
|
32 |
-
});
|
33 |
-
|
34 |
-
const [loadingDots, setLoadingDots] = useState('');
|
35 |
-
|
36 |
-
useEffect(() => {
|
37 |
-
let loadingInterval: NodeJS.Timeout;
|
38 |
-
|
39 |
-
if (isLoading) {
|
40 |
-
loadingInterval = setInterval(() => {
|
41 |
-
setLoadingDots(prevMessage => {
|
42 |
-
switch (prevMessage) {
|
43 |
-
case '':
|
44 |
-
return '.';
|
45 |
-
case '.':
|
46 |
-
return '..';
|
47 |
-
case '..':
|
48 |
-
return '...';
|
49 |
-
case '...':
|
50 |
-
return '';
|
51 |
-
default:
|
52 |
-
return '';
|
53 |
-
}
|
54 |
-
});
|
55 |
-
}, 500);
|
56 |
-
}
|
57 |
-
|
58 |
-
return () => {
|
59 |
-
clearInterval(loadingInterval);
|
60 |
-
};
|
61 |
-
}, [isLoading]);
|
62 |
-
|
63 |
-
const assistantLoadingMessage = {
|
64 |
-
id: 'loading',
|
65 |
-
content: loadingDots,
|
66 |
-
role: 'assistant',
|
67 |
-
};
|
68 |
-
|
69 |
-
const messageWithLoading =
|
70 |
-
isLoading &&
|
71 |
-
messages.length &&
|
72 |
-
messages[messages.length - 1].role !== 'assistant'
|
73 |
-
? [...messages, assistantLoadingMessage]
|
74 |
-
: messages;
|
75 |
-
|
76 |
-
return {
|
77 |
-
messages: messageWithLoading as MessageWithSelectedDataset[],
|
78 |
-
append,
|
79 |
-
reload,
|
80 |
-
stop,
|
81 |
-
isLoading,
|
82 |
-
input,
|
83 |
-
setInput,
|
84 |
-
};
|
85 |
-
};
|
86 |
-
|
87 |
-
export default useChatWithMedia;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lib/types.ts
CHANGED
@@ -1,42 +1,11 @@
|
|
1 |
import { type Message } from 'ai';
|
2 |
|
3 |
-
export type ServerActionResult<Result> = Promise<
|
4 |
-
| Result
|
5 |
-
| {
|
6 |
-
error: string;
|
7 |
-
}
|
8 |
-
>;
|
9 |
-
|
10 |
-
/**
|
11 |
-
* @deprecated
|
12 |
-
*/
|
13 |
-
export type DatasetImageEntity = {
|
14 |
-
url: string;
|
15 |
-
selected?: boolean;
|
16 |
-
name: string;
|
17 |
-
};
|
18 |
-
|
19 |
-
/**
|
20 |
-
* @deprecated
|
21 |
-
*/
|
22 |
-
export type MessageWithSelectedDataset = Message & {
|
23 |
-
dataset: DatasetImageEntity[];
|
24 |
-
};
|
25 |
-
|
26 |
export type MessageBase = {
|
27 |
role: Message['role'];
|
28 |
content: string;
|
29 |
id: string;
|
30 |
};
|
31 |
|
32 |
-
export type ChatEntity = {
|
33 |
-
url: string;
|
34 |
-
id?: string; // a chat without id is not to be saved
|
35 |
-
user: string; // email
|
36 |
-
messages: MessageBase[];
|
37 |
-
updatedAt: number;
|
38 |
-
};
|
39 |
-
|
40 |
export interface SignedPayload {
|
41 |
id: string;
|
42 |
publicUrl: string;
|
|
|
1 |
import { type Message } from 'ai';
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
export type MessageBase = {
|
4 |
role: Message['role'];
|
5 |
content: string;
|
6 |
id: string;
|
7 |
};
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
export interface SignedPayload {
|
10 |
id: string;
|
11 |
publicUrl: string;
|
next.config.js
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
|
3 |
module.exports = {
|
|
|
4 |
images: {
|
5 |
remotePatterns: [
|
6 |
{
|
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
|
3 |
module.exports = {
|
4 |
+
// reactStrictMode: false,
|
5 |
images: {
|
6 |
remotePatterns: [
|
7 |
{
|
prisma/migrations/20240524012008_init/migration.sql
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
-- CreateEnum
|
2 |
+
CREATE TYPE "MessageRole" AS ENUM ('system', 'user', 'assistant');
|
3 |
+
|
4 |
+
-- CreateTable
|
5 |
+
CREATE TABLE "user" (
|
6 |
+
"id" TEXT NOT NULL,
|
7 |
+
"name" TEXT NOT NULL,
|
8 |
+
"email" TEXT NOT NULL,
|
9 |
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
10 |
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
11 |
+
|
12 |
+
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
13 |
+
);
|
14 |
+
|
15 |
+
-- CreateTable
|
16 |
+
CREATE TABLE "chat" (
|
17 |
+
"id" TEXT NOT NULL,
|
18 |
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
19 |
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
20 |
+
"userId" TEXT,
|
21 |
+
"mediaUrl" TEXT NOT NULL,
|
22 |
+
|
23 |
+
CONSTRAINT "chat_pkey" PRIMARY KEY ("id")
|
24 |
+
);
|
25 |
+
|
26 |
+
-- CreateTable
|
27 |
+
CREATE TABLE "message" (
|
28 |
+
"id" TEXT NOT NULL,
|
29 |
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
30 |
+
"updated_at" TIMESTAMP(3) NOT NULL,
|
31 |
+
"userId" TEXT,
|
32 |
+
"chatId" TEXT NOT NULL,
|
33 |
+
"content" TEXT NOT NULL,
|
34 |
+
"role" "MessageRole" NOT NULL,
|
35 |
+
|
36 |
+
CONSTRAINT "message_pkey" PRIMARY KEY ("id")
|
37 |
+
);
|
38 |
+
|
39 |
+
-- CreateIndex
|
40 |
+
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
41 |
+
|
42 |
+
-- AddForeignKey
|
43 |
+
ALTER TABLE "chat" ADD CONSTRAINT "chat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
44 |
+
|
45 |
+
-- AddForeignKey
|
46 |
+
ALTER TABLE "message" ADD CONSTRAINT "message_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "chat"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
47 |
+
|
48 |
+
-- AddForeignKey
|
49 |
+
ALTER TABLE "message" ADD CONSTRAINT "message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
prisma/migrations/migration_lock.toml
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# Please do not edit this file manually
|
2 |
+
# It should be added in your version-control system (i.e. Git)
|
3 |
+
provider = "postgresql"
|