Spaces:
Running
Running
MingruiZhang
commited on
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 +1 -6
- app/chat/[id]/server.tsx +10 -4
- app/layout.tsx +3 -3
- auth.ts +4 -3
- components/Avatar.tsx +34 -0
- components/UserMenu.tsx +2 -13
- components/chat/LoginPrompt.tsx +0 -25
- components/chat/TopPrompt.tsx +61 -0
- lib/db/functions.ts +18 -1
- prisma/migrations/20240606082721_add_avatar/migration.sql +2 -0
- prisma/schema.prisma +1 -0
- public/landing4.png +0 -0
- public/landing5.png +0 -0
- public/loading.gif +0 -0
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 |
-
<
|
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 |
-
|
9 |
}
|
10 |
|
11 |
-
export default async function ChatServer({
|
12 |
-
const chat = await dbGetChat(
|
13 |
const { id: userId } = await sessionUser();
|
14 |
|
15 |
if (!chat) {
|
16 |
revalidatePath('/');
|
17 |
redirect('/');
|
18 |
}
|
19 |
-
return
|
|
|
|
|
|
|
|
|
|
|
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: '/
|
20 |
-
shortcut: '/
|
21 |
-
apple: '/
|
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(
|
|
|
|
|
|
|
|
|
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)
|
|