MingruiZhang commited on
Commit
38448fc
β€’
1 Parent(s): 97e41aa

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 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
- return new Response('Unauthorized', {
17
- status: 401,
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
- await kv.zadd(`user:chat:${email}`, {
59
- score: Date.now(),
60
- member: `chat:${id}`,
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
- return new Response('Unauthorized', {
19
- status: 401,
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 { ThemeToggle } from '@/components/ThemeToggle';
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="open"
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
- return <ImageSelector />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if (profile?.email?.endsWith('@landing.ai')) {
28
- return !!profile;
29
- } else {
30
- return '/unauthorized';
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
- return !!auth?.user || request.nextUrl.pathname === '/unauthorized'; // this ensures there is a logged in user for -every- request
 
 
 
 
 
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-3xl px-8 pr-12">
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="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0"
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, IconShare, IconStop } from '@/components/ui/Icons';
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 { getRootProps, getInputProps } = useImageUpload(
 
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
- console.log();
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 className="mx-auto max-w-2xl px-4 mt-8">
61
- <div className="rounded-lg border bg-background p-8">
62
- <h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
63
- <p>Lets start by choosing an image</p>
64
- <div
65
- {...getRootProps()}
66
- className="dropzone border-2 border-dashed border-gray-400 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer"
67
- >
68
- <input {...getInputProps()} />
69
- <p className="text-gray-400 text-lg">
70
- Drag or drop image here, or click to select images
71
- </p>
72
- </div>
73
- <p className="mt-4 mb-2">
74
- You can also choose from below examples we provided
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 flex-col 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">
49
- {/* <Tooltip>
50
- <TooltipTrigger asChild>
51
- <button
52
- onClick={e => {
53
- e.preventDefault()
54
- router.refresh()
55
- router.push('/')
56
- }}
57
- className={cn(
58
- buttonVariants({ size: 'sm', variant: 'outline' }),
59
- 'absolute left-0 top-4 size-8 rounded-full bg-background p-0 sm:left-4'
60
- )}
61
- >
62
- <IconPlus />
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-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
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?.user !== email || !isAdmin) {
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