Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
Larger layout / allow logout user to upload / move image to chat box (#13)
Browse files![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/ca174f10-8f7f-4619-897f-c5303a856b15)
- app/api/upload/route.ts +12 -10
- app/api/vision-agent/route.ts +7 -7
- app/chat/layout.tsx +5 -3
- app/chat/page.tsx +70 -1
- app/page.tsx +0 -5
- auth.ts +15 -8
- components/Header.tsx +4 -8
- components/LoginMenu.tsx +33 -0
- components/chat-sidebar/ChatListSidebar.tsx +5 -0
- components/chat/ChatList.tsx +1 -1
- components/chat/ChatMessage.tsx +1 -1
- components/chat/ChatPanel.tsx +4 -3
- components/chat/EmptyScreen.tsx +0 -52
- components/chat/ImageSelector.tsx +22 -62
- components/chat/PromptForm.tsx +19 -21
- components/chat/index.tsx +1 -0
- lib/hooks/useImageUpload.ts +1 -23
- lib/kv/chat.ts +2 -2
- middleware.ts → middleware_disabled.ts +0 -0
app/api/upload/route.ts
CHANGED
@@ -12,11 +12,11 @@ import { kv } from '@vercel/kv';
|
|
12 |
export async function POST(req: Request): Promise<Response> {
|
13 |
const session = await auth();
|
14 |
const email = session?.user?.email;
|
15 |
-
if (!email) {
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
}
|
20 |
|
21 |
try {
|
22 |
const { url, base64, initMessages, fileType } = (await req.json()) as {
|
@@ -50,15 +50,17 @@ 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: email,
|
54 |
messages: initMessages ?? [],
|
55 |
};
|
56 |
|
57 |
await kv.hmset(`chat:${id}`, payload);
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
62 |
await kv.zadd('user:chat:all', {
|
63 |
score: Date.now(),
|
64 |
member: `chat:${id}`,
|
|
|
12 |
export async function POST(req: Request): Promise<Response> {
|
13 |
const session = await auth();
|
14 |
const email = session?.user?.email;
|
15 |
+
// if (!email) {
|
16 |
+
// return new Response('Unauthorized', {
|
17 |
+
// status: 401,
|
18 |
+
// });
|
19 |
+
// }
|
20 |
|
21 |
try {
|
22 |
const { url, base64, initMessages, fileType } = (await req.json()) as {
|
|
|
50 |
const payload: ChatEntity = {
|
51 |
url: urlToSave!, // TODO can be uploaded as well
|
52 |
id,
|
53 |
+
user: email || 'anonymous',
|
54 |
messages: initMessages ?? [],
|
55 |
};
|
56 |
|
57 |
await kv.hmset(`chat:${id}`, payload);
|
58 |
+
if (email) {
|
59 |
+
await kv.zadd(`user:chat:${email}`, {
|
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}`,
|
app/api/vision-agent/route.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import { StreamingTextResponse } from 'ai';
|
2 |
|
3 |
-
import { auth } from '@/auth';
|
4 |
import { MessageBase } from '../../../lib/types';
|
5 |
|
6 |
export const runtime = 'edge';
|
@@ -13,12 +13,12 @@ export async function POST(req: Request) {
|
|
13 |
url: string;
|
14 |
};
|
15 |
|
16 |
-
const session = await auth();
|
17 |
-
if (!session?.user?.email) {
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
}
|
22 |
|
23 |
const formData = new FormData();
|
24 |
formData.append('input', JSON.stringify(messages));
|
|
|
1 |
import { StreamingTextResponse } from 'ai';
|
2 |
|
3 |
+
// import { auth } from '@/auth';
|
4 |
import { MessageBase } from '../../../lib/types';
|
5 |
|
6 |
export const runtime = 'edge';
|
|
|
13 |
url: string;
|
14 |
};
|
15 |
|
16 |
+
// const session = await auth();
|
17 |
+
// if (!session?.user?.email) {
|
18 |
+
// return new Response('Unauthorized', {
|
19 |
+
// status: 401,
|
20 |
+
// });
|
21 |
+
// }
|
22 |
|
23 |
const formData = new FormData();
|
24 |
formData.append('input', JSON.stringify(messages));
|
app/chat/layout.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import {
|
2 |
import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
|
3 |
import Loading from '@/components/ui/Loading';
|
4 |
import { Suspense } from 'react';
|
@@ -8,11 +8,13 @@ interface ChatLayoutProps {
|
|
8 |
}
|
9 |
|
10 |
export default async function Layout({ children }: ChatLayoutProps) {
|
|
|
|
|
11 |
return (
|
12 |
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
|
13 |
<div
|
14 |
-
data-state=
|
15 |
-
className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out translate-x-0 lg:flex lg:w-[250px] h-full flex-col overflow-auto py-2"
|
16 |
>
|
17 |
<Suspense fallback={<Loading />}>
|
18 |
<ChatSidebarList />
|
|
|
1 |
+
import { auth } from '@/auth';
|
2 |
import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
|
3 |
import Loading from '@/components/ui/Loading';
|
4 |
import { Suspense } from 'react';
|
|
|
8 |
}
|
9 |
|
10 |
export default async function Layout({ children }: ChatLayoutProps) {
|
11 |
+
const session = await auth();
|
12 |
+
const email = session?.user?.email;
|
13 |
return (
|
14 |
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
|
15 |
<div
|
16 |
+
data-state={email ? 'open' : 'closed'}
|
17 |
+
className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out -translate-x-full data-[state=open]:translate-x-0 lg:flex lg:w-[250px] h-full flex-col overflow-auto py-2"
|
18 |
>
|
19 |
<Suspense fallback={<Loading />}>
|
20 |
<ChatSidebarList />
|
app/chat/page.tsx
CHANGED
@@ -1,5 +1,74 @@
|
|
|
|
|
|
1 |
import ImageSelector from '@/components/chat/ImageSelector';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
export default function Page() {
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
}
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
import ImageSelector from '@/components/chat/ImageSelector';
|
4 |
+
import { ChatEntity, MessageBase } from '@/lib/types';
|
5 |
+
import { fetcher } from '@/lib/utils';
|
6 |
+
import Image from 'next/image';
|
7 |
+
import { useRouter } from 'next/navigation';
|
8 |
+
|
9 |
+
type Example = {
|
10 |
+
url: string;
|
11 |
+
initMessages: MessageBase[];
|
12 |
+
};
|
13 |
+
|
14 |
+
const examples: Example[] = [
|
15 |
+
{
|
16 |
+
url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
|
17 |
+
initMessages: [
|
18 |
+
{
|
19 |
+
role: 'user',
|
20 |
+
content: 'how many cereals are there in the image?',
|
21 |
+
id: 'fake-id-1',
|
22 |
+
},
|
23 |
+
],
|
24 |
+
},
|
25 |
+
// 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
|
26 |
+
// 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
|
27 |
+
// 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
|
28 |
+
];
|
29 |
|
30 |
export default function Page() {
|
31 |
+
const router = useRouter();
|
32 |
+
return (
|
33 |
+
<div className="mx-auto max-w-2xl px-4 mt-8">
|
34 |
+
<div className="rounded-lg border bg-background p-8">
|
35 |
+
<h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
|
36 |
+
<p>
|
37 |
+
Vision Agent is a library that helps you utilize agent frameworks for
|
38 |
+
your vision tasks. Vision Agent aims to provide an in-seconds
|
39 |
+
experience by allowing users to describe their problem in text and
|
40 |
+
utilizing agent frameworks to solve the task for them. Check out our
|
41 |
+
discord for updates and roadmap!
|
42 |
+
</p>
|
43 |
+
<ImageSelector />
|
44 |
+
<p className="mt-4 mb-2">
|
45 |
+
You can also choose from below examples we provided
|
46 |
+
</p>
|
47 |
+
<div className="flex">
|
48 |
+
{examples.map(({ url, initMessages }, index) => (
|
49 |
+
<Image
|
50 |
+
src={url}
|
51 |
+
key={index}
|
52 |
+
width={120}
|
53 |
+
height={120}
|
54 |
+
alt="example images"
|
55 |
+
className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
|
56 |
+
onClick={async () => {
|
57 |
+
const resp = await fetcher<ChatEntity>('/api/upload', {
|
58 |
+
method: 'POST',
|
59 |
+
headers: {
|
60 |
+
'Content-Type': 'application/json',
|
61 |
+
},
|
62 |
+
body: JSON.stringify({ url, initMessages }),
|
63 |
+
});
|
64 |
+
if (resp) {
|
65 |
+
router.push(`/chat/${resp.id}`);
|
66 |
+
}
|
67 |
+
}}
|
68 |
+
/>
|
69 |
+
))}
|
70 |
+
</div>
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
);
|
74 |
}
|
app/page.tsx
CHANGED
@@ -2,11 +2,6 @@ import { auth } from '@/auth';
|
|
2 |
import { redirect } from 'next/navigation';
|
3 |
|
4 |
export default async function Page() {
|
5 |
-
const session = await auth();
|
6 |
-
if (!session) {
|
7 |
-
return null;
|
8 |
-
}
|
9 |
-
|
10 |
redirect('/chat');
|
11 |
|
12 |
// return (
|
|
|
2 |
import { redirect } from 'next/navigation';
|
3 |
|
4 |
export default async function Page() {
|
|
|
|
|
|
|
|
|
|
|
5 |
redirect('/chat');
|
6 |
|
7 |
// return (
|
auth.ts
CHANGED
@@ -11,6 +11,8 @@ declare module 'next-auth' {
|
|
11 |
}
|
12 |
}
|
13 |
|
|
|
|
|
14 |
export const {
|
15 |
handlers: { GET, POST },
|
16 |
auth,
|
@@ -23,13 +25,13 @@ export const {
|
|
23 |
}),
|
24 |
],
|
25 |
callbacks: {
|
26 |
-
signIn({ profile }) {
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
},
|
33 |
jwt({ token, profile }) {
|
34 |
if (profile) {
|
35 |
token.id = profile.id || profile.sub;
|
@@ -44,7 +46,12 @@ export const {
|
|
44 |
return session;
|
45 |
},
|
46 |
authorized({ request, auth }) {
|
47 |
-
|
|
|
|
|
|
|
|
|
|
|
48 |
},
|
49 |
},
|
50 |
pages: {
|
|
|
11 |
}
|
12 |
}
|
13 |
|
14 |
+
const restrictedPath = ['/project'];
|
15 |
+
|
16 |
export const {
|
17 |
handlers: { GET, POST },
|
18 |
auth,
|
|
|
25 |
}),
|
26 |
],
|
27 |
callbacks: {
|
28 |
+
// signIn({ profile }) {
|
29 |
+
// if (profile?.email?.endsWith('@landing.ai')) {
|
30 |
+
// return !!profile;
|
31 |
+
// } else {
|
32 |
+
// return '/unauthorized';
|
33 |
+
// }
|
34 |
+
// },
|
35 |
jwt({ token, profile }) {
|
36 |
if (profile) {
|
37 |
token.id = profile.id || profile.sub;
|
|
|
46 |
return session;
|
47 |
},
|
48 |
authorized({ request, auth }) {
|
49 |
+
const isAdmin = !!auth?.user?.email?.endsWith('landing.ai');
|
50 |
+
return restrictedPath.find(path =>
|
51 |
+
request.nextUrl.pathname.startsWith(path),
|
52 |
+
)
|
53 |
+
? isAdmin
|
54 |
+
: true;
|
55 |
},
|
56 |
},
|
57 |
pages: {
|
components/Header.tsx
CHANGED
@@ -5,25 +5,21 @@ import { auth } from '@/auth';
|
|
5 |
import { Button } from '@/components/ui/Button';
|
6 |
import { UserMenu } from '@/components/UserMenu';
|
7 |
import { IconSeparator } from './ui/Icons';
|
|
|
8 |
|
9 |
export async function Header() {
|
10 |
const session = await auth();
|
11 |
-
|
12 |
-
if (!session?.user) {
|
13 |
-
return null;
|
14 |
-
}
|
15 |
-
|
16 |
return (
|
17 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
18 |
{/* <Button variant="link" asChild className="mr-2">
|
19 |
<Link href="/project">Projects</Link>
|
20 |
</Button> */}
|
21 |
-
<Button variant="link" asChild className="mr-2">
|
22 |
<Link href="/chat">Chat</Link>
|
23 |
-
</Button>
|
24 |
<IconSeparator className="size-6 text-muted-foreground/50" />
|
25 |
<div className="flex items-center">
|
26 |
-
<UserMenu user={session!.user} />
|
27 |
</div>
|
28 |
</header>
|
29 |
);
|
|
|
5 |
import { Button } from '@/components/ui/Button';
|
6 |
import { UserMenu } from '@/components/UserMenu';
|
7 |
import { IconSeparator } from './ui/Icons';
|
8 |
+
import { LoginMenu } from './LoginMenu';
|
9 |
|
10 |
export async function Header() {
|
11 |
const session = await auth();
|
|
|
|
|
|
|
|
|
|
|
12 |
return (
|
13 |
<header className="sticky top-0 z-50 flex items-center justify-end w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
14 |
{/* <Button variant="link" asChild className="mr-2">
|
15 |
<Link href="/project">Projects</Link>
|
16 |
</Button> */}
|
17 |
+
{/* <Button variant="link" asChild className="mr-2">
|
18 |
<Link href="/chat">Chat</Link>
|
19 |
+
</Button> */}
|
20 |
<IconSeparator className="size-6 text-muted-foreground/50" />
|
21 |
<div className="flex items-center">
|
22 |
+
{session?.user ? <UserMenu user={session!.user} /> : <LoginMenu />}
|
23 |
</div>
|
24 |
</header>
|
25 |
);
|
components/LoginMenu.tsx
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'use client';
|
2 |
+
|
3 |
+
import { Button } from '@/components/ui/Button';
|
4 |
+
import {
|
5 |
+
DropdownMenu,
|
6 |
+
DropdownMenuContent,
|
7 |
+
DropdownMenuItem,
|
8 |
+
DropdownMenuSeparator,
|
9 |
+
DropdownMenuTrigger,
|
10 |
+
} from '@/components/ui/DropdownMenu';
|
11 |
+
import { LoginButton } from './LoginButton';
|
12 |
+
|
13 |
+
export interface UserMenuProps {}
|
14 |
+
|
15 |
+
export function LoginMenu() {
|
16 |
+
return (
|
17 |
+
<div className="flex items-center justify-between">
|
18 |
+
<DropdownMenu>
|
19 |
+
<DropdownMenuTrigger asChild>
|
20 |
+
<Button variant="ghost">Sign in</Button>
|
21 |
+
</DropdownMenuTrigger>
|
22 |
+
<DropdownMenuContent sideOffset={8} align="end" className="w-[220px]">
|
23 |
+
<DropdownMenuItem className="flex-col items-center">
|
24 |
+
<LoginButton oauth="google" />
|
25 |
+
</DropdownMenuItem>
|
26 |
+
<DropdownMenuItem className="flex-col items-center">
|
27 |
+
<LoginButton oauth="github" />
|
28 |
+
</DropdownMenuItem>
|
29 |
+
</DropdownMenuContent>
|
30 |
+
</DropdownMenu>
|
31 |
+
</div>
|
32 |
+
);
|
33 |
+
}
|
components/chat-sidebar/ChatListSidebar.tsx
CHANGED
@@ -1,10 +1,15 @@
|
|
1 |
import { getKVChats } from '@/lib/kv/chat';
|
2 |
import ChatCard, { ChatCardLayout } from './ChatCard';
|
3 |
import { IconPlus } from '../ui/Icons';
|
|
|
4 |
|
5 |
export interface ChatSidebarListProps {}
|
6 |
|
7 |
export default async function ChatSidebarList({}: ChatSidebarListProps) {
|
|
|
|
|
|
|
|
|
8 |
const chats = await getKVChats();
|
9 |
return (
|
10 |
<>
|
|
|
1 |
import { getKVChats } from '@/lib/kv/chat';
|
2 |
import ChatCard, { ChatCardLayout } from './ChatCard';
|
3 |
import { IconPlus } from '../ui/Icons';
|
4 |
+
import { auth } from '@/auth';
|
5 |
|
6 |
export interface ChatSidebarListProps {}
|
7 |
|
8 |
export default async function ChatSidebarList({}: ChatSidebarListProps) {
|
9 |
+
const session = await auth();
|
10 |
+
if (!session || !session.user) {
|
11 |
+
return null;
|
12 |
+
}
|
13 |
const chats = await getKVChats();
|
14 |
return (
|
15 |
<>
|
components/chat/ChatList.tsx
CHANGED
@@ -10,7 +10,7 @@ export interface ChatList {
|
|
10 |
|
11 |
export function ChatList({ messages }: ChatList) {
|
12 |
return (
|
13 |
-
<div className="relative mx-auto max-w-
|
14 |
{messages
|
15 |
// .filter(message => message.role !== 'system')
|
16 |
.map((message, index) => (
|
|
|
10 |
|
11 |
export function ChatList({ messages }: ChatList) {
|
12 |
return (
|
13 |
+
<div className="relative mx-auto max-w-5xl px-8 pr-12 overflow-auto">
|
14 |
{messages
|
15 |
// .filter(message => message.role !== 'system')
|
16 |
.map((message, index) => (
|
components/chat/ChatMessage.tsx
CHANGED
@@ -30,7 +30,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
|
30 |
</div>
|
31 |
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
|
32 |
<MemoizedReactMarkdown
|
33 |
-
className="
|
34 |
remarkPlugins={[remarkGfm, remarkMath]}
|
35 |
components={{
|
36 |
p({ children }) {
|
|
|
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 }) {
|
components/chat/ChatPanel.tsx
CHANGED
@@ -4,7 +4,7 @@ import { type UseChatHelpers } from 'ai/react';
|
|
4 |
import { Button } from '@/components/ui/Button';
|
5 |
import { PromptForm } from '@/components/chat/PromptForm';
|
6 |
import { ButtonScrollToBottom } from '@/components/chat/ButtonScrollToBottom';
|
7 |
-
import { IconRefresh,
|
8 |
import { MessageBase } from '../../lib/types';
|
9 |
|
10 |
export interface ChatPanelProps
|
@@ -15,6 +15,7 @@ export interface ChatPanelProps
|
|
15 |
id?: string;
|
16 |
title?: string;
|
17 |
messages: MessageBase[];
|
|
|
18 |
}
|
19 |
|
20 |
export function ChatPanel({
|
@@ -27,9 +28,8 @@ export function ChatPanel({
|
|
27 |
input,
|
28 |
setInput,
|
29 |
messages,
|
|
|
30 |
}: ChatPanelProps) {
|
31 |
-
const [shareDialogOpen, setShareDialogOpen] = React.useState(false);
|
32 |
-
|
33 |
return (
|
34 |
<div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]">
|
35 |
<ButtonScrollToBottom />
|
@@ -57,6 +57,7 @@ export function ChatPanel({
|
|
57 |
</div>
|
58 |
<div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4">
|
59 |
<PromptForm
|
|
|
60 |
onSubmit={async value => {
|
61 |
await append({
|
62 |
id,
|
|
|
4 |
import { Button } from '@/components/ui/Button';
|
5 |
import { PromptForm } from '@/components/chat/PromptForm';
|
6 |
import { ButtonScrollToBottom } from '@/components/chat/ButtonScrollToBottom';
|
7 |
+
import { IconRefresh, IconStop } from '@/components/ui/Icons';
|
8 |
import { MessageBase } from '../../lib/types';
|
9 |
|
10 |
export interface ChatPanelProps
|
|
|
15 |
id?: string;
|
16 |
title?: string;
|
17 |
messages: MessageBase[];
|
18 |
+
url?: string;
|
19 |
}
|
20 |
|
21 |
export function ChatPanel({
|
|
|
28 |
input,
|
29 |
setInput,
|
30 |
messages,
|
31 |
+
url,
|
32 |
}: ChatPanelProps) {
|
|
|
|
|
33 |
return (
|
34 |
<div className="fixed inset-x-0 bottom-0 w-full animate-in duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]">
|
35 |
<ButtonScrollToBottom />
|
|
|
57 |
</div>
|
58 |
<div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4">
|
59 |
<PromptForm
|
60 |
+
url={url}
|
61 |
onSubmit={async value => {
|
62 |
await append({
|
63 |
id,
|
components/chat/EmptyScreen.tsx
DELETED
@@ -1,52 +0,0 @@
|
|
1 |
-
import { useAtom } from 'jotai';
|
2 |
-
import { useDropzone } from 'react-dropzone';
|
3 |
-
import { datasetAtom } from '../../state';
|
4 |
-
import Image from 'next/image';
|
5 |
-
import useImageUpload from '../../lib/hooks/useImageUpload';
|
6 |
-
|
7 |
-
const examples = [
|
8 |
-
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
|
9 |
-
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
|
10 |
-
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
|
11 |
-
'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
|
12 |
-
];
|
13 |
-
|
14 |
-
export function EmptyScreen() {
|
15 |
-
const [, setTarget] = useAtom(datasetAtom);
|
16 |
-
const { getRootProps, getInputProps } = useImageUpload();
|
17 |
-
return (
|
18 |
-
<div className="mx-auto max-w-2xl px-4">
|
19 |
-
<div className="rounded-lg border bg-background p-8">
|
20 |
-
<h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
|
21 |
-
<p>Lets start by choosing an image</p>
|
22 |
-
<div
|
23 |
-
{...getRootProps()}
|
24 |
-
className="dropzone border-2 border-dashed border-gray-400 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer"
|
25 |
-
>
|
26 |
-
<input {...getInputProps()} />
|
27 |
-
<p className="text-gray-400 text-lg">
|
28 |
-
Drag or drop image here, or click to select images
|
29 |
-
</p>
|
30 |
-
</div>
|
31 |
-
<p className="mt-4 mb-2">
|
32 |
-
You can also choose from below examples we provided
|
33 |
-
</p>
|
34 |
-
<div className="flex">
|
35 |
-
{examples.map((example, index) => (
|
36 |
-
<Image
|
37 |
-
src={example}
|
38 |
-
key={index}
|
39 |
-
width={120}
|
40 |
-
height={120}
|
41 |
-
alt="example images"
|
42 |
-
className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
|
43 |
-
onClick={() =>
|
44 |
-
setTarget([{ url: example, name: 'i-1', selected: false }])
|
45 |
-
}
|
46 |
-
/>
|
47 |
-
))}
|
48 |
-
</div>
|
49 |
-
</div>
|
50 |
-
</div>
|
51 |
-
);
|
52 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/chat/ImageSelector.tsx
CHANGED
@@ -1,11 +1,11 @@
|
|
1 |
'use client';
|
2 |
|
3 |
-
import React from 'react';
|
4 |
-
import Image from 'next/image';
|
5 |
import useImageUpload from '../../lib/hooks/useImageUpload';
|
6 |
-
import { fetcher } from '@/lib/utils';
|
7 |
import { ChatEntity, MessageBase } from '@/lib/types';
|
8 |
import { useRouter } from 'next/navigation';
|
|
|
9 |
|
10 |
export interface ImageSelectorProps {}
|
11 |
|
@@ -14,32 +14,17 @@ type Example = {
|
|
14 |
initMessages: MessageBase[];
|
15 |
};
|
16 |
|
17 |
-
const examples: Example[] = [
|
18 |
-
{
|
19 |
-
url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
|
20 |
-
initMessages: [
|
21 |
-
{
|
22 |
-
role: 'user',
|
23 |
-
content: 'how many cereals are there in the image?',
|
24 |
-
id: 'fake-id-1',
|
25 |
-
},
|
26 |
-
],
|
27 |
-
},
|
28 |
-
// 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
|
29 |
-
// 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
|
30 |
-
// 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
|
31 |
-
];
|
32 |
-
|
33 |
const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
34 |
const router = useRouter();
|
35 |
-
const
|
|
|
36 |
undefined,
|
37 |
async files => {
|
38 |
const formData = new FormData();
|
39 |
if (files.length !== 1) {
|
40 |
throw new Error('Only one image can be uploaded at a time');
|
41 |
}
|
42 |
-
|
43 |
const reader = new FileReader();
|
44 |
reader.readAsDataURL(files[0]);
|
45 |
reader.onload = async () => {
|
@@ -50,6 +35,7 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
|
50 |
fileType: files[0].type,
|
51 |
}),
|
52 |
});
|
|
|
53 |
if (resp) {
|
54 |
router.push(`/chat/${resp.id}`);
|
55 |
}
|
@@ -57,47 +43,21 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
|
57 |
},
|
58 |
);
|
59 |
return (
|
60 |
-
<div
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
</p>
|
76 |
-
<div className="flex">
|
77 |
-
{examples.map(({ url, initMessages }, index) => (
|
78 |
-
<Image
|
79 |
-
src={url}
|
80 |
-
key={index}
|
81 |
-
width={120}
|
82 |
-
height={120}
|
83 |
-
alt="example images"
|
84 |
-
className="object-cover rounded mr-3 shadow-md hover:scale-105 cursor-pointer transition-transform"
|
85 |
-
onClick={async () => {
|
86 |
-
const resp = await fetcher<ChatEntity>('/api/upload', {
|
87 |
-
method: 'POST',
|
88 |
-
headers: {
|
89 |
-
'Content-Type': 'application/json',
|
90 |
-
},
|
91 |
-
body: JSON.stringify({ url, initMessages }),
|
92 |
-
});
|
93 |
-
if (resp) {
|
94 |
-
router.push(`/chat/${resp.id}`);
|
95 |
-
}
|
96 |
-
}}
|
97 |
-
/>
|
98 |
-
))}
|
99 |
-
</div>
|
100 |
-
</div>
|
101 |
</div>
|
102 |
);
|
103 |
};
|
|
|
1 |
'use client';
|
2 |
|
3 |
+
import React, { useState } from 'react';
|
|
|
4 |
import useImageUpload from '../../lib/hooks/useImageUpload';
|
5 |
+
import { cn, fetcher } from '@/lib/utils';
|
6 |
import { ChatEntity, MessageBase } from '@/lib/types';
|
7 |
import { useRouter } from 'next/navigation';
|
8 |
+
import Loading from '../ui/Loading';
|
9 |
|
10 |
export interface ImageSelectorProps {}
|
11 |
|
|
|
14 |
initMessages: MessageBase[];
|
15 |
};
|
16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
18 |
const router = useRouter();
|
19 |
+
const [isUploading, setUploading] = useState(false);
|
20 |
+
const { getRootProps, getInputProps, isDragActive } = useImageUpload(
|
21 |
undefined,
|
22 |
async files => {
|
23 |
const formData = new FormData();
|
24 |
if (files.length !== 1) {
|
25 |
throw new Error('Only one image can be uploaded at a time');
|
26 |
}
|
27 |
+
setUploading(true);
|
28 |
const reader = new FileReader();
|
29 |
reader.readAsDataURL(files[0]);
|
30 |
reader.onload = async () => {
|
|
|
35 |
fileType: files[0].type,
|
36 |
}),
|
37 |
});
|
38 |
+
setUploading(false);
|
39 |
if (resp) {
|
40 |
router.push(`/chat/${resp.id}`);
|
41 |
}
|
|
|
43 |
},
|
44 |
);
|
45 |
return (
|
46 |
+
<div
|
47 |
+
{...getRootProps()}
|
48 |
+
className={cn(
|
49 |
+
'dropzone border-2 border-dashed border-gray-400 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer',
|
50 |
+
isDragActive && 'bg-gray-500/50 border-solid',
|
51 |
+
)}
|
52 |
+
>
|
53 |
+
<input {...getInputProps()} />
|
54 |
+
<p className="text-gray-400 text-lg">
|
55 |
+
{isUploading ? (
|
56 |
+
<Loading />
|
57 |
+
) : (
|
58 |
+
'Drag or drop image here, or click to select images'
|
59 |
+
)}
|
60 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
</div>
|
62 |
);
|
63 |
};
|
components/chat/PromptForm.tsx
CHANGED
@@ -11,11 +11,13 @@ import {
|
|
11 |
} from '@/components/ui/Tooltip';
|
12 |
import { IconArrowElbow, IconPlus } from '@/components/ui/Icons';
|
13 |
import { useRouter } from 'next/navigation';
|
|
|
14 |
|
15 |
export interface PromptProps
|
16 |
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
17 |
onSubmit: (value: string) => void;
|
18 |
isLoading: boolean;
|
|
|
19 |
}
|
20 |
|
21 |
export function PromptForm({
|
@@ -23,6 +25,7 @@ export function PromptForm({
|
|
23 |
input,
|
24 |
setInput,
|
25 |
isLoading,
|
|
|
26 |
}: PromptProps) {
|
27 |
const { formRef, onKeyDown } = useEnterSubmit();
|
28 |
const inputRef = React.useRef<HTMLTextAreaElement>(null);
|
@@ -45,26 +48,21 @@ export function PromptForm({
|
|
45 |
}}
|
46 |
ref={formRef}
|
47 |
>
|
48 |
-
<div className="relative flex
|
49 |
-
{
|
50 |
-
<
|
51 |
-
<
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
<span className="sr-only">New Chat</span>
|
64 |
-
</button>
|
65 |
-
</TooltipTrigger>
|
66 |
-
<TooltipContent>New Chat</TooltipContent>
|
67 |
-
</Tooltip> */}
|
68 |
<Textarea
|
69 |
ref={inputRef}
|
70 |
tabIndex={0}
|
@@ -74,7 +72,7 @@ export function PromptForm({
|
|
74 |
onChange={e => setInput(e.target.value)}
|
75 |
placeholder="Ask questions about the images."
|
76 |
spellCheck={false}
|
77 |
-
className="min-h-[60px] w-
|
78 |
/>
|
79 |
<div className="absolute right-0 top-4 sm:right-4">
|
80 |
<Tooltip>
|
|
|
11 |
} from '@/components/ui/Tooltip';
|
12 |
import { IconArrowElbow, IconPlus } from '@/components/ui/Icons';
|
13 |
import { useRouter } from 'next/navigation';
|
14 |
+
import Image from 'next/image';
|
15 |
|
16 |
export interface PromptProps
|
17 |
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
18 |
onSubmit: (value: string) => void;
|
19 |
isLoading: boolean;
|
20 |
+
url?: string;
|
21 |
}
|
22 |
|
23 |
export function PromptForm({
|
|
|
25 |
input,
|
26 |
setInput,
|
27 |
isLoading,
|
28 |
+
url,
|
29 |
}: PromptProps) {
|
30 |
const { formRef, onKeyDown } = useEnterSubmit();
|
31 |
const inputRef = React.useRef<HTMLTextAreaElement>(null);
|
|
|
48 |
}}
|
49 |
ref={formRef}
|
50 |
>
|
51 |
+
<div className="relative flex w-full px-8 pl-2 overflow-hidden max-h-60 grow bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2">
|
52 |
+
{url && (
|
53 |
+
<Tooltip>
|
54 |
+
<TooltipTrigger asChild>
|
55 |
+
<Image
|
56 |
+
src={url}
|
57 |
+
width={60}
|
58 |
+
height={60}
|
59 |
+
alt="chosen image"
|
60 |
+
className="w-1/5 my-4 mx-2 rounded-md"
|
61 |
+
/>
|
62 |
+
</TooltipTrigger>
|
63 |
+
<TooltipContent>New Chat</TooltipContent>
|
64 |
+
</Tooltip>
|
65 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
66 |
<Textarea
|
67 |
ref={inputRef}
|
68 |
tabIndex={0}
|
|
|
72 |
onChange={e => setInput(e.target.value)}
|
73 |
placeholder="Ask questions about the images."
|
74 |
spellCheck={false}
|
75 |
+
className="min-h-[60px] w-4/5 resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
|
76 |
/>
|
77 |
<div className="absolute right-0 top-4 sm:right-4">
|
78 |
<Tooltip>
|
components/chat/index.tsx
CHANGED
@@ -36,6 +36,7 @@ export function Chat({ chat }: ChatProps) {
|
|
36 |
</div>
|
37 |
<ChatPanel
|
38 |
id={id}
|
|
|
39 |
isLoading={isLoading}
|
40 |
stop={stop}
|
41 |
append={append}
|
|
|
36 |
</div>
|
37 |
<ChatPanel
|
38 |
id={id}
|
39 |
+
url={url}
|
40 |
isLoading={isLoading}
|
41 |
stop={stop}
|
42 |
append={append}
|
lib/hooks/useImageUpload.ts
CHANGED
@@ -21,29 +21,7 @@ const useImageUpload = (
|
|
21 |
acceptedFiles.forEach(file => {
|
22 |
try {
|
23 |
const reader = new FileReader();
|
24 |
-
reader.onloadend = () => {
|
25 |
-
// const newImage = reader.result as string;
|
26 |
-
// setTarget(prev => {
|
27 |
-
// // Check if the image already exists in the state
|
28 |
-
// if (
|
29 |
-
// // prev.length >= 10 ||
|
30 |
-
// prev.find(entity => entity.url === newImage)
|
31 |
-
// ) {
|
32 |
-
// // If it does, return the state unchanged
|
33 |
-
// return prev;
|
34 |
-
// } else {
|
35 |
-
// // If it doesn't, add the new image to the state
|
36 |
-
// return [
|
37 |
-
// ...prev,
|
38 |
-
// {
|
39 |
-
// url: newImage,
|
40 |
-
// selected: false,
|
41 |
-
// name: `i-${prev.length + 1}`,
|
42 |
-
// } satisfies DatasetImageEntity,
|
43 |
-
// ];
|
44 |
-
// }
|
45 |
-
// });
|
46 |
-
};
|
47 |
reader.readAsDataURL(file);
|
48 |
} catch (err) {
|
49 |
console.error(err);
|
|
|
21 |
acceptedFiles.forEach(file => {
|
22 |
try {
|
23 |
const reader = new FileReader();
|
24 |
+
reader.onloadend = () => {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
reader.readAsDataURL(file);
|
26 |
} catch (err) {
|
27 |
console.error(err);
|
lib/kv/chat.ts
CHANGED
@@ -38,10 +38,10 @@ export async function getKVChats() {
|
|
38 |
}
|
39 |
|
40 |
export async function getKVChat(id: string) {
|
41 |
-
const { email, isAdmin } = await authCheck();
|
42 |
const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
|
43 |
|
44 |
-
if (chat
|
45 |
redirect('/');
|
46 |
}
|
47 |
|
|
|
38 |
}
|
39 |
|
40 |
export async function getKVChat(id: string) {
|
41 |
+
// const { email, isAdmin } = await authCheck();
|
42 |
const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
|
43 |
|
44 |
+
if (!chat) {
|
45 |
redirect('/');
|
46 |
}
|
47 |
|
middleware.ts → middleware_disabled.ts
RENAMED
File without changes
|