MingruiZhang commited on
Commit
04735a9
1 Parent(s): 2e16ef9

feat: Author avatar (#85)

Browse files

![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/6c6991e8-33dd-46f0-ae39-b67fde81a132)

app/chat/[id]/page.tsx CHANGED
@@ -2,7 +2,6 @@ import { Suspense } from 'react';
2
  import ChatServer from './server';
3
  import Loading from '@/components/ui/Loading';
4
  import { auth } from '@/auth';
5
- import { LoginPrompt } from '@/components/chat/LoginPrompt';
6
 
7
  interface PageProps {
8
  params: {
@@ -12,7 +11,6 @@ interface PageProps {
12
 
13
  export default async function Page({ params }: PageProps) {
14
  const { id: chatId } = params;
15
- const session = await auth();
16
  return (
17
  <Suspense
18
  fallback={
@@ -21,10 +19,7 @@ export default async function Page({ params }: PageProps) {
21
  </div>
22
  }
23
  >
24
- <div className="w-[1600px] max-w-full mx-auto flex flex-col space-y-4 items-center">
25
- {!session && <LoginPrompt />}
26
- <ChatServer id={chatId} />
27
- </div>
28
  </Suspense>
29
  );
30
  }
 
2
  import ChatServer from './server';
3
  import Loading from '@/components/ui/Loading';
4
  import { auth } from '@/auth';
 
5
 
6
  interface PageProps {
7
  params: {
 
11
 
12
  export default async function Page({ params }: PageProps) {
13
  const { id: chatId } = params;
 
14
  return (
15
  <Suspense
16
  fallback={
 
19
  </div>
20
  }
21
  >
22
+ <ChatServer chatId={chatId} />
 
 
 
23
  </Suspense>
24
  );
25
  }
app/chat/[id]/server.tsx CHANGED
@@ -3,18 +3,24 @@ import { auth, sessionUser } from '@/auth';
3
  import { dbGetChat } from '@/lib/db/functions';
4
  import { redirect } from 'next/navigation';
5
  import { revalidatePath } from 'next/cache';
 
6
 
7
  interface ChatServerProps {
8
- id: string;
9
  }
10
 
11
- export default async function ChatServer({ id }: ChatServerProps) {
12
- const chat = await dbGetChat(id);
13
  const { id: userId } = await sessionUser();
14
 
15
  if (!chat) {
16
  revalidatePath('/');
17
  redirect('/');
18
  }
19
- return <ChatInterface chat={chat} userId={userId} />;
 
 
 
 
 
20
  }
 
3
  import { dbGetChat } from '@/lib/db/functions';
4
  import { redirect } from 'next/navigation';
5
  import { revalidatePath } from 'next/cache';
6
+ import TopPrompt from '@/components/chat/TopPrompt';
7
 
8
  interface ChatServerProps {
9
+ chatId: string;
10
  }
11
 
12
+ export default async function ChatServer({ chatId }: ChatServerProps) {
13
+ const chat = await dbGetChat(chatId);
14
  const { id: userId } = await sessionUser();
15
 
16
  if (!chat) {
17
  revalidatePath('/');
18
  redirect('/');
19
  }
20
+ return (
21
+ <div className="w-[1600px] max-w-full mx-auto flex flex-col space-y-4 items-center">
22
+ <TopPrompt chat={chat} userId={userId} />
23
+ <ChatInterface chat={chat} userId={userId} />
24
+ </div>
25
+ );
26
  }
app/layout.tsx CHANGED
@@ -16,9 +16,9 @@ export const metadata = {
16
  },
17
  description: 'By Landing AI',
18
  icons: {
19
- icon: '/landing.png',
20
- shortcut: '/landing.png',
21
- apple: '/landing.png',
22
  },
23
  };
24
 
 
16
  },
17
  description: 'By Landing AI',
18
  icons: {
19
+ icon: '/landing4.png',
20
+ shortcut: '/landing4.png',
21
+ apple: '/landing4.png',
22
  },
23
  };
24
 
auth.ts CHANGED
@@ -31,13 +31,13 @@ export const {
31
  if (!profile) {
32
  return false;
33
  }
34
- const { email, name } = profile;
35
 
36
  if (!email || !name) {
37
  return false;
38
  }
39
 
40
- const dbUser = await dbFindOrCreateUser(email, name);
41
 
42
  if (dbUser) {
43
  user.id = dbUser.id;
@@ -57,8 +57,9 @@ export const {
57
  // so also UI might still have session, DB might already have cleaned up
58
  const email = session?.user?.email;
59
  const name = session?.user?.name;
 
60
  if (email && name) {
61
- const dbUser = await dbFindOrCreateUser(email, name);
62
  // put db user id into session
63
  session.user.id = dbUser.id;
64
  }
 
31
  if (!profile) {
32
  return false;
33
  }
34
+ const { email, name, picture } = profile;
35
 
36
  if (!email || !name) {
37
  return false;
38
  }
39
 
40
+ const dbUser = await dbFindOrCreateUser(email, name, picture);
41
 
42
  if (dbUser) {
43
  user.id = dbUser.id;
 
57
  // so also UI might still have session, DB might already have cleaned up
58
  const email = session?.user?.email;
59
  const name = session?.user?.name;
60
+ const avatar = session?.user?.image;
61
  if (email && name) {
62
+ const dbUser = await dbFindOrCreateUser(email, name, avatar);
63
  // put db user id into session
64
  session.user.id = dbUser.id;
65
  }
components/Avatar.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Image from 'next/image';
2
+ import React from 'react';
3
+
4
+ export interface AvatarProps {
5
+ name?: string | null;
6
+ avatar?: string | null;
7
+ }
8
+
9
+ function getUserInitials(name: string) {
10
+ const [firstName, lastName] = name.split(' ');
11
+ return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2);
12
+ }
13
+
14
+ const Avatar: React.FC<AvatarProps> = ({ name, avatar }) => {
15
+ return (
16
+ <>
17
+ {avatar ? (
18
+ <Image
19
+ className="size-6 transition-opacity duration-300 rounded-full select-none ring-1 ring-zinc-100/10 hover:opacity-80"
20
+ src={avatar ?? ''}
21
+ alt={name ?? 'Avatar'}
22
+ height={48}
23
+ width={48}
24
+ />
25
+ ) : (
26
+ <div className="flex items-center justify-center text-xs font-medium uppercase rounded-full select-none size-7 shrink-0 bg-muted/50 text-muted-foreground">
27
+ {name ? getUserInitials(name) : 'VA'}
28
+ </div>
29
+ )}
30
+ </>
31
+ );
32
+ };
33
+
34
+ export default Avatar;
components/UserMenu.tsx CHANGED
@@ -13,6 +13,7 @@ import {
13
  DropdownMenuTrigger,
14
  } from '@/components/ui/DropdownMenu';
15
  import { IconExternalLink } from '@/components/ui/Icons';
 
16
 
17
  export interface UserMenuProps {
18
  user: Session['user'];
@@ -29,19 +30,7 @@ export function UserMenu({ user }: UserMenuProps) {
29
  <DropdownMenu>
30
  <DropdownMenuTrigger asChild>
31
  <Button variant="ghost">
32
- {user?.image ? (
33
- <Image
34
- className="size-6 transition-opacity duration-300 rounded-full select-none ring-1 ring-zinc-100/10 hover:opacity-80"
35
- src={user?.image ?? ''}
36
- alt={user.name ?? 'Avatar'}
37
- height={48}
38
- width={48}
39
- />
40
- ) : (
41
- <div className="flex items-center justify-center text-xs font-medium uppercase rounded-full select-none size-7 shrink-0 bg-muted/50 text-muted-foreground">
42
- {user?.name ? getUserInitials(user?.name) : null}
43
- </div>
44
- )}
45
  <span className="ml-2">{user?.name}</span>
46
  </Button>
47
  </DropdownMenuTrigger>
 
13
  DropdownMenuTrigger,
14
  } from '@/components/ui/DropdownMenu';
15
  import { IconExternalLink } from '@/components/ui/Icons';
16
+ import Avatar from './Avatar';
17
 
18
  export interface UserMenuProps {
19
  user: Session['user'];
 
30
  <DropdownMenu>
31
  <DropdownMenuTrigger asChild>
32
  <Button variant="ghost">
33
+ <Avatar name={user?.name} avatar={user?.image} />
 
 
 
 
 
 
 
 
 
 
 
 
34
  <span className="ml-2">{user?.name}</span>
35
  </Button>
36
  </DropdownMenuTrigger>
components/chat/LoginPrompt.tsx DELETED
@@ -1,25 +0,0 @@
1
- 'use client';
2
-
3
- import { Card } from '../ui/Card';
4
- import { IconExclamationTriangle } from '../ui/Icons';
5
- import Link from 'next/link';
6
-
7
- export interface LoginPrompt {}
8
-
9
- export function LoginPrompt() {
10
- return (
11
- <Card className="group py-2 px-4 flex items-center">
12
- <div className="bg-background flex size-8 shrink-0 select-none items-center justify-center rounded-md">
13
- <IconExclamationTriangle className="font-medium" />
14
- </div>
15
- <div className="flex-1 px-1 ml-2 overflow-hidden">
16
- <p className="leading-normal font-medium">
17
- <Link href="/sign-in" className="underline">
18
- Sign in
19
- </Link>{' '}
20
- to save and revisit your chat history!
21
- </p>
22
- </div>
23
- </Card>
24
- );
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/chat/TopPrompt.tsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { sessionUser } from '@/auth';
2
+ import { Card } from '../ui/Card';
3
+ import { IconExclamationTriangle } from '../ui/Icons';
4
+ import Link from 'next/link';
5
+ import { ChatWithMessages } from '@/lib/types';
6
+ import { dbGetUser } from '@/lib/db/functions';
7
+ import Avatar from '../Avatar';
8
+
9
+ export interface TopPrompt {
10
+ chat: ChatWithMessages;
11
+ userId?: string | null;
12
+ }
13
+
14
+ export default async function TopPrompt({ chat, userId }: TopPrompt) {
15
+ const authorId = chat.userId;
16
+ console.log('[Ming] ~ TopPrompt ~ authorId:', authorId);
17
+ // 1. Viewer logged in, Viewer = Author
18
+ if (userId && authorId === userId) {
19
+ return null;
20
+ }
21
+ // 2. Viewer logged in, No Author
22
+ if (userId && !authorId) {
23
+ return null;
24
+ }
25
+ // 3. Author, but is not Viewer
26
+ if (authorId && authorId !== userId) {
27
+ const chatAuthor = authorId ? await dbGetUser(authorId) : null;
28
+ return (
29
+ <Card className="group py-2 px-4 flex items-center">
30
+ <div className="bg-background flex size-8 shrink-0 select-none items-center justify-center rounded-md">
31
+ <Avatar name={chatAuthor?.name} avatar={chatAuthor?.avatar} />
32
+ </div>
33
+ <div className="flex-1 px-1 ml-2 overflow-hidden">
34
+ <p className="leading-normal">
35
+ Code author:{' '}
36
+ <span className="font-medium">{chatAuthor?.name ?? 'Unknown'}</span>
37
+ </p>
38
+ </div>
39
+ </Card>
40
+ );
41
+ }
42
+ // 4. No author, Viewer not logged in
43
+ if (!userId && !authorId) {
44
+ return (
45
+ <Card className="group py-2 px-4 flex items-center">
46
+ <div className="bg-background flex size-8 shrink-0 select-none items-center justify-center rounded-md">
47
+ <IconExclamationTriangle className="font-medium" />
48
+ </div>
49
+ <div className="flex-1 px-1 ml-2 overflow-hidden">
50
+ <p className="leading-normal font-medium">
51
+ <Link href="/sign-in" className="underline">
52
+ Sign in
53
+ </Link>{' '}
54
+ to save and revisit your chat history!
55
+ </p>
56
+ </div>
57
+ </Card>
58
+ );
59
+ }
60
+ return null;
61
+ }
lib/db/functions.ts CHANGED
@@ -30,7 +30,11 @@ async function getUserConnect() {
30
  * @param name - The name of the user.
31
  * @returns A promise that resolves to the user object.
32
  */
33
- export async function dbFindOrCreateUser(email: string, name: string) {
 
 
 
 
34
  // Try to find the user by email
35
  const user = await prisma.user.findUnique({
36
  where: { email: email },
@@ -44,11 +48,24 @@ export async function dbFindOrCreateUser(email: string, name: string) {
44
  data: {
45
  email: email,
46
  name: name,
 
47
  },
48
  });
49
  }
50
  }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  /**
53
  * Retrieves all chat records from the database for the current user.
54
  * @returns A promise that resolves to an array of Chat objects.
 
30
  * @param name - The name of the user.
31
  * @returns A promise that resolves to the user object.
32
  */
33
+ export async function dbFindOrCreateUser(
34
+ email: string,
35
+ name: string,
36
+ avatar?: string | null,
37
+ ) {
38
  // Try to find the user by email
39
  const user = await prisma.user.findUnique({
40
  where: { email: email },
 
48
  data: {
49
  email: email,
50
  name: name,
51
+ avatar: avatar,
52
  },
53
  });
54
  }
55
  }
56
 
57
+ /**
58
+ * Retrieves a user from the database based on the provided ID.
59
+ * @param id - The ID of the user to retrieve.
60
+ * @returns A Promise that resolves to the user object if found, or null if not found.
61
+ */
62
+ export async function dbGetUser(id: string) {
63
+ // Try to find the user by email
64
+ return await prisma.user.findUnique({
65
+ where: { id },
66
+ });
67
+ }
68
+
69
  /**
70
  * Retrieves all chat records from the database for the current user.
71
  * @returns A promise that resolves to an array of Chat objects.
prisma/migrations/20240606082721_add_avatar/migration.sql ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ -- AlterTable
2
+ ALTER TABLE "user" ADD COLUMN "avatar" TEXT;
prisma/schema.prisma CHANGED
@@ -18,6 +18,7 @@ model User {
18
  email String @unique
19
  createdAt DateTime @default(now()) @map("created_at")
20
  updatedAt DateTime @updatedAt @map("updated_at")
 
21
  chats Chat[]
22
  message Message[]
23
 
 
18
  email String @unique
19
  createdAt DateTime @default(now()) @map("created_at")
20
  updatedAt DateTime @updatedAt @map("updated_at")
21
+ avatar String?
22
  chats Chat[]
23
  message Message[]
24
 
public/landing4.png ADDED
public/landing5.png ADDED
public/loading.gif DELETED
Binary file (8.94 kB)