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 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 { ChatWithMessages } from '@/lib/db/types';
 
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 fetcher<ChatWithMessages>('/api/chat/create', {
97
- method: 'POST',
98
- headers: {
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 fetcher<ChatWithMessages>('/api/chat/create', {
91
- method: 'POST',
92
- headers: {
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 px-6 py-2 pr-4 bg-zinc-800 text-zinc-100">
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
- return await prisma.chat.create({
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
- return prisma.chat.delete({
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"