MingruiZhang commited on
Commit
f80b091
1 Parent(s): c3e8f3d
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app/(logout)/sign-in/page.tsx +11 -11
  2. app/(logout)/unauthorized/page.tsx +16 -16
  3. app/api/auth/[...nextauth]/route.ts +2 -2
  4. app/api/chat/route.ts +46 -46
  5. app/api/upload/route.ts +21 -21
  6. app/chat/layout.tsx +18 -18
  7. app/chat/page.tsx +2 -2
  8. app/globals.css +80 -80
  9. app/layout.tsx +41 -41
  10. app/page.tsx +10 -10
  11. app/project/[projectId]/page.tsx +17 -17
  12. app/project/layout.tsx +18 -18
  13. app/project/page.tsx +6 -6
  14. auth.ts +43 -43
  15. components/Header.tsx +16 -16
  16. components/LoginButton.tsx +23 -23
  17. components/Providers.tsx +5 -5
  18. components/TailwindIndicator.tsx +11 -11
  19. components/ThemeToggle.tsx +21 -21
  20. components/UserMenu.tsx +49 -49
  21. components/chat-sidebar/ChatCard.tsx +16 -16
  22. components/chat-sidebar/ChatListSidebar.tsx +9 -9
  23. components/chat/ButtonScrollToBottom.tsx +22 -22
  24. components/chat/ChatList.tsx +15 -15
  25. components/chat/ChatMessage.tsx +55 -55
  26. components/chat/ChatMessageActions.tsx +23 -23
  27. components/chat/ChatPanel.tsx +59 -59
  28. components/chat/ChatScrollAnchor.tsx +15 -15
  29. components/chat/EmptyScreen.tsx +41 -41
  30. components/chat/ImageList.tsx +53 -53
  31. components/chat/ImageSelector.tsx +43 -43
  32. components/chat/MemoizedReactMarkdown.tsx +4 -4
  33. components/chat/PromptForm.tsx +61 -61
  34. components/chat/index.tsx +29 -29
  35. components/project-sidebar/ProjectCard.tsx +25 -25
  36. components/project-sidebar/ProjectListSideBar.tsx +8 -8
  37. components/project/Chat.tsx +18 -18
  38. components/project/MediaGrid.tsx +11 -11
  39. components/project/MediaTile.tsx +31 -31
  40. components/ui/Button.tsx +40 -40
  41. components/ui/Chip.tsx +19 -19
  42. components/ui/CodeBlock.tsx +113 -113
  43. components/ui/DropdownMenu.tsx +77 -77
  44. components/ui/Icons.tsx +463 -463
  45. components/ui/Input.tsx +14 -14
  46. components/ui/Loading.tsx +5 -5
  47. components/ui/Separator.tsx +18 -18
  48. components/ui/Textarea.tsx +8 -8
  49. components/ui/Tooltip.tsx +11 -11
  50. lib/fetch/index.ts +83 -83
app/(logout)/sign-in/page.tsx CHANGED
@@ -3,16 +3,16 @@ import { LoginButton } from '@/components/LoginButton';
3
  import { redirect } from 'next/navigation';
4
 
5
  export default async function SignInPage() {
6
- const session = await auth();
7
- // redirect to home if user is already logged in
8
- if (session?.user) {
9
- redirect('/');
10
- }
11
 
12
- return (
13
- <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
14
- <LoginButton oauth="google" />
15
- <LoginButton oauth="github" />
16
- </div>
17
- );
18
  }
 
3
  import { redirect } from 'next/navigation';
4
 
5
  export default async function SignInPage() {
6
+ const session = await auth();
7
+ // redirect to home if user is already logged in
8
+ if (session?.user) {
9
+ redirect('/');
10
+ }
11
 
12
+ return (
13
+ <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
14
+ <LoginButton oauth="google" />
15
+ <LoginButton oauth="github" />
16
+ </div>
17
+ );
18
  }
app/(logout)/unauthorized/page.tsx CHANGED
@@ -4,21 +4,21 @@ import { Button } from '@/components/ui/Button';
4
  import Link from 'next/link';
5
 
6
  export default async function Unauthorized() {
7
- const session = await auth();
8
- // redirect to home if user is already logged in
9
- if (session?.user) {
10
- redirect('/');
11
- }
12
 
13
- return (
14
- <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
15
- <div>
16
- You are not authorized to view this page. Please sign in with Landing
17
- account to continue.
18
- </div>
19
- <Button asChild className="mt-16">
20
- <Link href="/sign-in">Sign in</Link>
21
- </Button>
22
- </div>
23
- );
24
  }
 
4
  import Link from 'next/link';
5
 
6
  export default async function Unauthorized() {
7
+ const session = await auth();
8
+ // redirect to home if user is already logged in
9
+ if (session?.user) {
10
+ redirect('/');
11
+ }
12
 
13
+ return (
14
+ <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
15
+ <div>
16
+ You are not authorized to view this page. Please sign in with Landing
17
+ account to continue.
18
+ </div>
19
+ <Button asChild className="mt-16">
20
+ <Link href="/sign-in">Sign in</Link>
21
+ </Button>
22
+ </div>
23
+ );
24
  }
app/api/auth/[...nextauth]/route.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { GET, POST } from '@/auth'
2
- export const runtime = 'edge'
 
1
+ export { GET, POST } from '@/auth';
2
+ export const runtime = 'edge';
app/api/chat/route.ts CHANGED
@@ -3,9 +3,9 @@ import OpenAI from 'openai';
3
 
4
  import { auth } from '@/auth';
5
  import {
6
- ChatCompletionMessageParam,
7
- ChatCompletionContentPart,
8
- ChatCompletionContentPartImage,
9
  } from 'openai/resources';
10
  import { MessageWithSelectedDataset } from '../../../lib/types';
11
  // import { postAgentChat } from '@/lib/fetch';
@@ -13,56 +13,56 @@ import { MessageWithSelectedDataset } from '../../../lib/types';
13
  export const runtime = 'edge';
14
 
15
  const openai = new OpenAI({
16
- apiKey: process.env.OPENAI_API_KEY,
17
  });
18
 
19
  export async function POST(req: Request) {
20
- const json = await req.json();
21
- const { messages } = json as {
22
- messages: MessageWithSelectedDataset[];
23
- };
24
- console.log('[Ming] ~ POST ~ messages:', messages);
25
 
26
- const session = await auth();
27
- if (!session?.user?.email) {
28
- return new Response('Unauthorized', {
29
- status: 401,
30
- });
31
- }
32
 
33
- const formattedMessage: ChatCompletionMessageParam[] = messages.map(
34
- message => {
35
- const { dataset, ...rest } = message;
36
 
37
- const contentWithImage: ChatCompletionContentPart[] = [
38
- {
39
- type: 'text',
40
- text: message.content as string,
41
- },
42
- ...(dataset ?? []).map(
43
- entity =>
44
- ({
45
- type: 'image_url',
46
- image_url: { url: entity.url },
47
- }) satisfies ChatCompletionContentPartImage,
48
- ),
49
- ];
50
- return {
51
- role: 'user',
52
- content: contentWithImage,
53
- };
54
- },
55
- );
56
 
57
- const res = await openai.chat.completions.create({
58
- model: 'gpt-4-vision-preview',
59
- messages: formattedMessage,
60
- temperature: 0.3,
61
- stream: true,
62
- max_tokens: 300,
63
- });
64
 
65
- const stream = OpenAIStream(res);
66
 
67
- return new StreamingTextResponse(stream);
68
  }
 
3
 
4
  import { auth } from '@/auth';
5
  import {
6
+ ChatCompletionMessageParam,
7
+ ChatCompletionContentPart,
8
+ ChatCompletionContentPartImage,
9
  } from 'openai/resources';
10
  import { MessageWithSelectedDataset } from '../../../lib/types';
11
  // import { postAgentChat } from '@/lib/fetch';
 
13
  export const runtime = 'edge';
14
 
15
  const openai = new OpenAI({
16
+ apiKey: process.env.OPENAI_API_KEY,
17
  });
18
 
19
  export async function POST(req: Request) {
20
+ const json = await req.json();
21
+ const { messages } = json as {
22
+ messages: MessageWithSelectedDataset[];
23
+ };
24
+ console.log('[Ming] ~ POST ~ messages:', messages);
25
 
26
+ const session = await auth();
27
+ if (!session?.user?.email) {
28
+ return new Response('Unauthorized', {
29
+ status: 401,
30
+ });
31
+ }
32
 
33
+ const formattedMessage: ChatCompletionMessageParam[] = messages.map(
34
+ message => {
35
+ const { dataset, ...rest } = message;
36
 
37
+ const contentWithImage: ChatCompletionContentPart[] = [
38
+ {
39
+ type: 'text',
40
+ text: message.content as string,
41
+ },
42
+ ...(dataset ?? []).map(
43
+ entity =>
44
+ ({
45
+ type: 'image_url',
46
+ image_url: { url: entity.url },
47
+ }) satisfies ChatCompletionContentPartImage,
48
+ ),
49
+ ];
50
+ return {
51
+ role: 'user',
52
+ content: contentWithImage,
53
+ };
54
+ },
55
+ );
56
 
57
+ const res = await openai.chat.completions.create({
58
+ model: 'gpt-4-vision-preview',
59
+ messages: formattedMessage,
60
+ temperature: 0.3,
61
+ stream: true,
62
+ max_tokens: 300,
63
+ });
64
 
65
+ const stream = OpenAIStream(res);
66
 
67
+ return new StreamingTextResponse(stream);
68
  }
app/api/upload/route.ts CHANGED
@@ -4,29 +4,29 @@ import { kv } from '@vercel/kv';
4
  import { format } from 'date-fns';
5
 
6
  export async function POST(req: Request) {
7
- const session = await auth();
8
- console.log('[Ming] ~ POST ~ session:', session);
9
- const email = session?.user?.email;
10
- if (!email) {
11
- return new Response('Unauthorized', {
12
- status: 401,
13
- });
14
- }
15
 
16
- const json = await req.json();
17
- console.log('[Ming] ~ POST ~ json:', json);
18
 
19
- const id = nanoid();
20
 
21
- await kv.hmset(`chat:${id}`, json);
22
- await kv.zadd(`user:chat:${email}`, {
23
- score: Date.now(),
24
- member: `chat:${id}`,
25
- });
26
- await kv.zadd('user:chat:all', {
27
- score: Date.now(),
28
- member: `chat:${id}`,
29
- });
30
 
31
- return 'success';
32
  }
 
4
  import { format } from 'date-fns';
5
 
6
  export async function POST(req: Request) {
7
+ const session = await auth();
8
+ console.log('[Ming] ~ POST ~ session:', session);
9
+ const email = session?.user?.email;
10
+ if (!email) {
11
+ return new Response('Unauthorized', {
12
+ status: 401,
13
+ });
14
+ }
15
 
16
+ const json = await req.json();
17
+ console.log('[Ming] ~ POST ~ json:', json);
18
 
19
+ const id = nanoid();
20
 
21
+ await kv.hmset(`chat:${id}`, json);
22
+ await kv.zadd(`user:chat:${email}`, {
23
+ score: Date.now(),
24
+ member: `chat:${id}`,
25
+ });
26
+ await kv.zadd('user:chat:all', {
27
+ score: Date.now(),
28
+ member: `chat:${id}`,
29
+ });
30
 
31
+ return 'success';
32
  }
app/chat/layout.tsx CHANGED
@@ -3,25 +3,25 @@ import Loading from '@/components/ui/Loading';
3
  import { Suspense } from 'react';
4
 
5
  interface ChatLayoutProps {
6
- children: React.ReactNode;
7
  }
8
 
9
  export default async function Layout({ children }: ChatLayoutProps) {
10
- return (
11
- <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
12
- <div
13
- data-state="open"
14
- 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] xl:w-[300px] h-full flex-col dark:bg-zinc-950 overflow-auto py-2"
15
- >
16
- <Suspense fallback={<Loading />}>
17
- <ChatSidebarList />
18
- </Suspense>
19
- </div>
20
- <Suspense fallback={<Loading />}>
21
- <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
22
- {children}
23
- </div>
24
- </Suspense>
25
- </div>
26
- );
27
  }
 
3
  import { Suspense } from 'react';
4
 
5
  interface ChatLayoutProps {
6
+ children: React.ReactNode;
7
  }
8
 
9
  export default async function Layout({ children }: ChatLayoutProps) {
10
+ return (
11
+ <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
12
+ <div
13
+ data-state="open"
14
+ 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] xl:w-[300px] h-full flex-col dark:bg-zinc-950 overflow-auto py-2"
15
+ >
16
+ <Suspense fallback={<Loading />}>
17
+ <ChatSidebarList />
18
+ </Suspense>
19
+ </div>
20
+ <Suspense fallback={<Loading />}>
21
+ <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
22
+ {children}
23
+ </div>
24
+ </Suspense>
25
+ </div>
26
+ );
27
  }
app/chat/page.tsx CHANGED
@@ -2,6 +2,6 @@ import { nanoid } from '@/lib/utils';
2
  import { Chat } from '@/components/chat';
3
 
4
  export default function Page() {
5
- const id = nanoid();
6
- return <Chat id={id} />;
7
  }
 
2
  import { Chat } from '@/components/chat';
3
 
4
  export default function Page() {
5
+ const id = nanoid();
6
+ return <Chat id={id} />;
7
  }
app/globals.css CHANGED
@@ -3,110 +3,110 @@
3
  @tailwind utilities;
4
 
5
  @layer base {
6
- :root {
7
- --background: 0 0% 100%;
8
- --foreground: 240 10% 3.9%;
9
 
10
- --muted: 240 4.8% 95.9%;
11
- --muted-foreground: 240 3.8% 46.1%;
12
 
13
- --popover: 0 0% 100%;
14
- --popover-foreground: 240 10% 3.9%;
15
 
16
- --card: 0 0% 100%;
17
- --card-foreground: 240 10% 3.9%;
18
 
19
- --border: 240 5.9% 90%;
20
- --input: 240 5.9% 90%;
21
 
22
- --primary: 240 5.9% 10%;
23
- --primary-foreground: 0 0% 98%;
24
 
25
- --secondary: 240 4.8% 95.9%;
26
- --secondary-foreground: 240 5.9% 10%;
27
 
28
- --accent: 240 4.8% 95.9%;
29
- --accent-foreground: ;
30
 
31
- --destructive: 0 84.2% 60.2%;
32
- --destructive-foreground: 0 0% 98%;
33
 
34
- --ring: 240 5% 64.9%;
35
 
36
- --radius: 0.5rem;
37
- }
38
 
39
- .dark {
40
- --background: 240 10% 3.9%;
41
- --foreground: 0 0% 98%;
42
 
43
- --muted: 240 3.7% 15.9%;
44
- --muted-foreground: 240 5% 64.9%;
45
 
46
- --popover: 240 10% 3.9%;
47
- --popover-foreground: 0 0% 98%;
48
 
49
- --card: 240 10% 3.9%;
50
- --card-foreground: 0 0% 98%;
51
 
52
- --border: 240 3.7% 15.9%;
53
- --input: 240 3.7% 15.9%;
54
 
55
- --primary: 0 0% 98%;
56
- --primary-foreground: 240 5.9% 10%;
57
 
58
- --secondary: 240 3.7% 15.9%;
59
- --secondary-foreground: 0 0% 98%;
60
 
61
- --accent: 240 3.7% 15.9%;
62
- --accent-foreground: ;
63
 
64
- --destructive: 0 62.8% 30.6%;
65
- --destructive-foreground: 0 85.7% 97.3%;
66
 
67
- --ring: 240 3.7% 15.9%;
68
- }
69
  }
70
 
71
  @layer base {
72
- * {
73
- @apply border-border;
74
- }
75
- body {
76
- @apply bg-background text-foreground;
77
- }
78
  }
79
 
80
  @layer components {
81
- .scroll-fade::after {
82
- content: '';
83
- position: absolute;
84
- bottom: 0;
85
- left: 0;
86
- right: 0;
87
- height: 50px;
88
- background: linear-gradient(
89
- to bottom,
90
- rgba(255, 255, 255, 1),
91
- rgba(255, 255, 255, 0)
92
- );
93
- pointer-events: none;
94
- }
95
- .scroll-fade:active::after,
96
- .scroll-fade:hover::after {
97
- background: none;
98
- }
99
- .image-shadow::after {
100
- content: '';
101
- position: absolute;
102
- top: 0;
103
- right: 0;
104
- bottom: 0;
105
- left: 0;
106
- box-shadow:
107
- 0 10px 15px -3px rgba(0, 0, 0, 0.1),
108
- 0 4px 6px -2px rgba(0, 0, 0, 0.05);
109
- border-radius: 0.5rem;
110
- pointer-events: none;
111
- }
112
  }
 
3
  @tailwind utilities;
4
 
5
  @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 240 10% 3.9%;
9
 
10
+ --muted: 240 4.8% 95.9%;
11
+ --muted-foreground: 240 3.8% 46.1%;
12
 
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 240 10% 3.9%;
15
 
16
+ --card: 0 0% 100%;
17
+ --card-foreground: 240 10% 3.9%;
18
 
19
+ --border: 240 5.9% 90%;
20
+ --input: 240 5.9% 90%;
21
 
22
+ --primary: 240 5.9% 10%;
23
+ --primary-foreground: 0 0% 98%;
24
 
25
+ --secondary: 240 4.8% 95.9%;
26
+ --secondary-foreground: 240 5.9% 10%;
27
 
28
+ --accent: 240 4.8% 95.9%;
29
+ --accent-foreground: ;
30
 
31
+ --destructive: 0 84.2% 60.2%;
32
+ --destructive-foreground: 0 0% 98%;
33
 
34
+ --ring: 240 5% 64.9%;
35
 
36
+ --radius: 0.5rem;
37
+ }
38
 
39
+ .dark {
40
+ --background: 240 10% 3.9%;
41
+ --foreground: 0 0% 98%;
42
 
43
+ --muted: 240 3.7% 15.9%;
44
+ --muted-foreground: 240 5% 64.9%;
45
 
46
+ --popover: 240 10% 3.9%;
47
+ --popover-foreground: 0 0% 98%;
48
 
49
+ --card: 240 10% 3.9%;
50
+ --card-foreground: 0 0% 98%;
51
 
52
+ --border: 240 3.7% 15.9%;
53
+ --input: 240 3.7% 15.9%;
54
 
55
+ --primary: 0 0% 98%;
56
+ --primary-foreground: 240 5.9% 10%;
57
 
58
+ --secondary: 240 3.7% 15.9%;
59
+ --secondary-foreground: 0 0% 98%;
60
 
61
+ --accent: 240 3.7% 15.9%;
62
+ --accent-foreground: ;
63
 
64
+ --destructive: 0 62.8% 30.6%;
65
+ --destructive-foreground: 0 85.7% 97.3%;
66
 
67
+ --ring: 240 3.7% 15.9%;
68
+ }
69
  }
70
 
71
  @layer base {
72
+ * {
73
+ @apply border-border;
74
+ }
75
+ body {
76
+ @apply bg-background text-foreground;
77
+ }
78
  }
79
 
80
  @layer components {
81
+ .scroll-fade::after {
82
+ content: '';
83
+ position: absolute;
84
+ bottom: 0;
85
+ left: 0;
86
+ right: 0;
87
+ height: 50px;
88
+ background: linear-gradient(
89
+ to bottom,
90
+ rgba(255, 255, 255, 1),
91
+ rgba(255, 255, 255, 0)
92
+ );
93
+ pointer-events: none;
94
+ }
95
+ .scroll-fade:active::after,
96
+ .scroll-fade:hover::after {
97
+ background: none;
98
+ }
99
+ .image-shadow::after {
100
+ content: '';
101
+ position: absolute;
102
+ top: 0;
103
+ right: 0;
104
+ bottom: 0;
105
+ left: 0;
106
+ box-shadow:
107
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
108
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
109
+ border-radius: 0.5rem;
110
+ pointer-events: none;
111
+ }
112
  }
app/layout.tsx CHANGED
@@ -10,54 +10,54 @@ import { Header } from '@/components/Header';
10
  import { ThemeToggle } from '@/components/ThemeToggle';
11
 
12
  export const metadata = {
13
- metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
14
- title: {
15
- default: 'Insight Playground',
16
- template: `%s - Insight Playground`,
17
- },
18
- description: 'By Landing AI',
19
- icons: {
20
- icon: '/landing.png',
21
- shortcut: '/landing.png',
22
- apple: '/landing.png',
23
- },
24
  };
25
 
26
  export const viewport = {
27
- themeColor: [
28
- { media: '(prefers-color-scheme: light)', color: 'white' },
29
- { media: '(prefers-color-scheme: dark)', color: 'black' },
30
- ],
31
  };
32
 
33
  interface RootLayoutProps {
34
- children: React.ReactNode;
35
  }
36
 
37
  export default function RootLayout({ children }: RootLayoutProps) {
38
- return (
39
- <html lang="en" suppressHydrationWarning>
40
- <body
41
- className={cn(
42
- 'font-sans antialiased',
43
- GeistSans.variable,
44
- GeistMono.variable,
45
- )}
46
- >
47
- <Toaster />
48
- <Providers
49
- attribute="class"
50
- defaultTheme="system"
51
- enableSystem
52
- disableTransitionOnChange
53
- >
54
- <div className="flex flex-col min-h-screen">
55
- <Header />
56
- <main className="flex flex-col flex-1 bg-muted/50">{children}</main>
57
- </div>
58
- <TailwindIndicator />
59
- </Providers>
60
- </body>
61
- </html>
62
- );
63
  }
 
10
  import { ThemeToggle } from '@/components/ThemeToggle';
11
 
12
  export const metadata = {
13
+ metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
14
+ title: {
15
+ default: 'Insight Playground',
16
+ template: `%s - Insight Playground`,
17
+ },
18
+ description: 'By Landing AI',
19
+ icons: {
20
+ icon: '/landing.png',
21
+ shortcut: '/landing.png',
22
+ apple: '/landing.png',
23
+ },
24
  };
25
 
26
  export const viewport = {
27
+ themeColor: [
28
+ { media: '(prefers-color-scheme: light)', color: 'white' },
29
+ { media: '(prefers-color-scheme: dark)', color: 'black' },
30
+ ],
31
  };
32
 
33
  interface RootLayoutProps {
34
+ children: React.ReactNode;
35
  }
36
 
37
  export default function RootLayout({ children }: RootLayoutProps) {
38
+ return (
39
+ <html lang="en" suppressHydrationWarning>
40
+ <body
41
+ className={cn(
42
+ 'font-sans antialiased',
43
+ GeistSans.variable,
44
+ GeistMono.variable,
45
+ )}
46
+ >
47
+ <Toaster />
48
+ <Providers
49
+ attribute="class"
50
+ defaultTheme="system"
51
+ enableSystem
52
+ disableTransitionOnChange
53
+ >
54
+ <div className="flex flex-col min-h-screen">
55
+ <Header />
56
+ <main className="flex flex-col flex-1 bg-muted/50">{children}</main>
57
+ </div>
58
+ <TailwindIndicator />
59
+ </Providers>
60
+ </body>
61
+ </html>
62
+ );
63
  }
app/page.tsx CHANGED
@@ -2,16 +2,16 @@ 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 (
13
- // <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
14
- // Welcome to Insight Playground
15
- // </div>
16
- // );
17
  }
 
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 (
13
+ // <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
14
+ // Welcome to Insight Playground
15
+ // </div>
16
+ // );
17
  }
app/project/[projectId]/page.tsx CHANGED
@@ -5,26 +5,26 @@ import Loading from '../../../components/ui/Loading';
5
  import Chat from '@/components/project/Chat';
6
 
7
  interface PageProps {
8
- params: {
9
- projectId: string;
10
- };
11
  }
12
 
13
  export default async function Page({ params }: PageProps) {
14
- const { projectId } = params;
15
 
16
- const mediaList = await fetchProjectMedia({ projectId: Number(projectId) });
17
 
18
- return (
19
- <div className="pb-[150px] pt-4 md:pt-10 h-full">
20
- <div className="flex h-full">
21
- <div className="w-1/2 relative border-r border-gray-300 overflow-auto">
22
- <MediaGrid mediaList={mediaList} />
23
- </div>
24
- <div className="w-1/2 relative overflow-auto">
25
- <Chat mediaList={mediaList} />
26
- </div>
27
- </div>
28
- </div>
29
- );
30
  }
 
5
  import Chat from '@/components/project/Chat';
6
 
7
  interface PageProps {
8
+ params: {
9
+ projectId: string;
10
+ };
11
  }
12
 
13
  export default async function Page({ params }: PageProps) {
14
+ const { projectId } = params;
15
 
16
+ const mediaList = await fetchProjectMedia({ projectId: Number(projectId) });
17
 
18
+ return (
19
+ <div className="pb-[150px] pt-4 md:pt-10 h-full">
20
+ <div className="flex h-full">
21
+ <div className="w-1/2 relative border-r border-gray-300 overflow-auto">
22
+ <MediaGrid mediaList={mediaList} />
23
+ </div>
24
+ <div className="w-1/2 relative overflow-auto">
25
+ <Chat mediaList={mediaList} />
26
+ </div>
27
+ </div>
28
+ </div>
29
+ );
30
  }
app/project/layout.tsx CHANGED
@@ -3,25 +3,25 @@ import { Suspense } from 'react';
3
  import Loading from '@/components/ui/Loading';
4
 
5
  interface ChatLayoutProps {
6
- children: React.ReactNode;
7
  }
8
 
9
  export default async function Layout({ children }: ChatLayoutProps) {
10
- return (
11
- <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
12
- <div
13
- data-state="open"
14
- 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] xl:w-[300px] h-full flex-col dark:bg-zinc-950 overflow-auto py-2"
15
- >
16
- <Suspense fallback={<Loading />}>
17
- <ProjectListSideBar />
18
- </Suspense>
19
- </div>
20
- <Suspense fallback={<Loading />}>
21
- <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
22
- {children}
23
- </div>
24
- </Suspense>
25
- </div>
26
- );
27
  }
 
3
  import Loading from '@/components/ui/Loading';
4
 
5
  interface ChatLayoutProps {
6
+ children: React.ReactNode;
7
  }
8
 
9
  export default async function Layout({ children }: ChatLayoutProps) {
10
+ return (
11
+ <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
12
+ <div
13
+ data-state="open"
14
+ 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] xl:w-[300px] h-full flex-col dark:bg-zinc-950 overflow-auto py-2"
15
+ >
16
+ <Suspense fallback={<Loading />}>
17
+ <ProjectListSideBar />
18
+ </Suspense>
19
+ </div>
20
+ <Suspense fallback={<Loading />}>
21
+ <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
22
+ {children}
23
+ </div>
24
+ </Suspense>
25
+ </div>
26
+ );
27
  }
app/project/page.tsx CHANGED
@@ -1,8 +1,8 @@
1
  export default function Page() {
2
- const content = '<- Select a project from sidebar';
3
- return (
4
- <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2 font-medium">
5
- {content}
6
- </div>
7
- );
8
  }
 
1
  export default function Page() {
2
+ const content = '<- Select a project from sidebar';
3
+ return (
4
+ <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2 font-medium">
5
+ {content}
6
+ </div>
7
+ );
8
  }
auth.ts CHANGED
@@ -3,51 +3,51 @@ import GitHub from 'next-auth/providers/github';
3
  import Google from 'next-auth/providers/google';
4
 
5
  declare module 'next-auth' {
6
- interface Session {
7
- user: {
8
- /** The user's id. */
9
- id: string;
10
- } & DefaultSession['user'];
11
- }
12
  }
13
 
14
  export const {
15
- handlers: { GET, POST },
16
- auth,
17
  } = NextAuth({
18
- providers: [
19
- GitHub,
20
- Google({
21
- clientId: process.env.GOOGLE_CLIENT_ID!,
22
- clientSecret: process.env.GOOGLE_SECRET!,
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;
36
- token.image = profile.avatar_url || profile.picture;
37
- }
38
- return token;
39
- },
40
- session: ({ session, token }) => {
41
- if (session?.user && token?.id) {
42
- session.user.id = String(token.id);
43
- }
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: {
51
- signIn: '/sign-in', // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
52
- },
53
  });
 
3
  import Google from 'next-auth/providers/google';
4
 
5
  declare module 'next-auth' {
6
+ interface Session {
7
+ user: {
8
+ /** The user's id. */
9
+ id: string;
10
+ } & DefaultSession['user'];
11
+ }
12
  }
13
 
14
  export const {
15
+ handlers: { GET, POST },
16
+ auth,
17
  } = NextAuth({
18
+ providers: [
19
+ GitHub,
20
+ Google({
21
+ clientId: process.env.GOOGLE_CLIENT_ID!,
22
+ clientSecret: process.env.GOOGLE_SECRET!,
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;
36
+ token.image = profile.avatar_url || profile.picture;
37
+ }
38
+ return token;
39
+ },
40
+ session: ({ session, token }) => {
41
+ if (session?.user && token?.id) {
42
+ session.user.id = String(token.id);
43
+ }
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: {
51
+ signIn: '/sign-in', // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
52
+ },
53
  });
components/Header.tsx CHANGED
@@ -7,24 +7,24 @@ 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
- );
30
  }
 
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
+ );
30
  }
components/LoginButton.tsx CHANGED
@@ -8,31 +8,31 @@ import { Button, type ButtonProps } from '@/components/ui/Button';
8
  import { IconGitHub, IconSpinner, IconGoogle } from '@/components/ui/Icons';
9
 
10
  interface LoginButtonProps extends ButtonProps {
11
- oauth: 'github' | 'google';
12
  }
13
 
14
  export function LoginButton({ oauth, ...props }: LoginButtonProps) {
15
- const [isLoading, setIsLoading] = React.useState(false);
16
 
17
- const icon =
18
- oauth === 'github' ? (
19
- <IconGitHub className="mr-2" />
20
- ) : (
21
- <IconGoogle className="mr-2" />
22
- );
23
- return (
24
- <Button
25
- variant="outline"
26
- onClick={() => {
27
- setIsLoading(true);
28
- // next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
29
- signIn(oauth, { callbackUrl: `/` });
30
- }}
31
- disabled={isLoading}
32
- {...props}
33
- >
34
- {isLoading ? <IconSpinner className="mr-2 animate-spin" /> : icon}
35
- Sign in with {oauth.charAt(0).toUpperCase() + oauth.slice(1)}
36
- </Button>
37
- );
38
  }
 
8
  import { IconGitHub, IconSpinner, IconGoogle } from '@/components/ui/Icons';
9
 
10
  interface LoginButtonProps extends ButtonProps {
11
+ oauth: 'github' | 'google';
12
  }
13
 
14
  export function LoginButton({ oauth, ...props }: LoginButtonProps) {
15
+ const [isLoading, setIsLoading] = React.useState(false);
16
 
17
+ const icon =
18
+ oauth === 'github' ? (
19
+ <IconGitHub className="mr-2" />
20
+ ) : (
21
+ <IconGoogle className="mr-2" />
22
+ );
23
+ return (
24
+ <Button
25
+ variant="outline"
26
+ onClick={() => {
27
+ setIsLoading(true);
28
+ // next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
29
+ signIn(oauth, { callbackUrl: `/` });
30
+ }}
31
+ disabled={isLoading}
32
+ {...props}
33
+ >
34
+ {isLoading ? <IconSpinner className="mr-2 animate-spin" /> : icon}
35
+ Sign in with {oauth.charAt(0).toUpperCase() + oauth.slice(1)}
36
+ </Button>
37
+ );
38
  }
components/Providers.tsx CHANGED
@@ -6,9 +6,9 @@ import { ThemeProviderProps } from 'next-themes/dist/types';
6
  import { TooltipProvider } from '@/components/ui/Tooltip';
7
 
8
  export function Providers({ children, ...props }: ThemeProviderProps) {
9
- return (
10
- <NextThemesProvider {...props}>
11
- <TooltipProvider>{children}</TooltipProvider>
12
- </NextThemesProvider>
13
- );
14
  }
 
6
  import { TooltipProvider } from '@/components/ui/Tooltip';
7
 
8
  export function Providers({ children, ...props }: ThemeProviderProps) {
9
+ return (
10
+ <NextThemesProvider {...props}>
11
+ <TooltipProvider>{children}</TooltipProvider>
12
+ </NextThemesProvider>
13
+ );
14
  }
components/TailwindIndicator.tsx CHANGED
@@ -1,14 +1,14 @@
1
  export function TailwindIndicator() {
2
- if (process.env.NODE_ENV === 'production') return null;
3
 
4
- return (
5
- <div className="fixed bottom-1 left-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
6
- <div className="block sm:hidden">xs</div>
7
- <div className="hidden sm:block md:hidden">sm</div>
8
- <div className="hidden md:block lg:hidden">md</div>
9
- <div className="hidden lg:block xl:hidden">lg</div>
10
- <div className="hidden xl:block 2xl:hidden">xl</div>
11
- <div className="hidden 2xl:block">2xl</div>
12
- </div>
13
- );
14
  }
 
1
  export function TailwindIndicator() {
2
+ if (process.env.NODE_ENV === 'production') return null;
3
 
4
+ return (
5
+ <div className="fixed bottom-1 left-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
6
+ <div className="block sm:hidden">xs</div>
7
+ <div className="hidden sm:block md:hidden">sm</div>
8
+ <div className="hidden md:block lg:hidden">md</div>
9
+ <div className="hidden lg:block xl:hidden">lg</div>
10
+ <div className="hidden xl:block 2xl:hidden">xl</div>
11
+ <div className="hidden 2xl:block">2xl</div>
12
+ </div>
13
+ );
14
  }
components/ThemeToggle.tsx CHANGED
@@ -7,26 +7,26 @@ import { Button } from '@/components/ui/Button';
7
  import { IconMoon, IconSun } from '@/components/ui/Icons';
8
 
9
  export function ThemeToggle() {
10
- const { setTheme, theme } = useTheme();
11
- const [_, startTransition] = React.useTransition();
12
 
13
- return (
14
- <Button
15
- variant="ghost"
16
- size="icon"
17
- className="fixed bottom-4 right-4 z-50 dark:bg-zinc-950 dark:text-white transition-all p-2 rounded-full shadow-md"
18
- onClick={() => {
19
- startTransition(() => {
20
- setTheme(theme === 'light' ? 'dark' : 'light');
21
- });
22
- }}
23
- >
24
- {!theme ? null : theme === 'dark' ? (
25
- <IconMoon className="transition-all" />
26
- ) : (
27
- <IconSun className="transition-all" />
28
- )}
29
- <span className="sr-only">Toggle theme</span>
30
- </Button>
31
- );
32
  }
 
7
  import { IconMoon, IconSun } from '@/components/ui/Icons';
8
 
9
  export function ThemeToggle() {
10
+ const { setTheme, theme } = useTheme();
11
+ const [_, startTransition] = React.useTransition();
12
 
13
+ return (
14
+ <Button
15
+ variant="ghost"
16
+ size="icon"
17
+ className="fixed bottom-4 right-4 z-50 dark:bg-zinc-950 dark:text-white transition-all p-2 rounded-full shadow-md"
18
+ onClick={() => {
19
+ startTransition(() => {
20
+ setTheme(theme === 'light' ? 'dark' : 'light');
21
+ });
22
+ }}
23
+ >
24
+ {!theme ? null : theme === 'dark' ? (
25
+ <IconMoon className="transition-all" />
26
+ ) : (
27
+ <IconSun className="transition-all" />
28
+ )}
29
+ <span className="sr-only">Toggle theme</span>
30
+ </Button>
31
+ );
32
  }
components/UserMenu.tsx CHANGED
@@ -6,63 +6,63 @@ import { signOut } from 'next-auth/react';
6
 
7
  import { Button } from '@/components/ui/Button';
8
  import {
9
- DropdownMenu,
10
- DropdownMenuContent,
11
- DropdownMenuItem,
12
- DropdownMenuSeparator,
13
- DropdownMenuTrigger,
14
  } from '@/components/ui/DropdownMenu';
15
  import { IconExternalLink } from '@/components/ui/Icons';
16
 
17
  export interface UserMenuProps {
18
- user: Session['user'];
19
  }
20
 
21
  function getUserInitials(name: string) {
22
- const [firstName, lastName] = name.split(' ');
23
- return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2);
24
  }
25
 
26
  export function UserMenu({ user }: UserMenuProps) {
27
- return (
28
- <div className="flex items-center justify-between">
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>
48
- <DropdownMenuContent sideOffset={8} align="start" className="w-[180px]">
49
- <DropdownMenuItem className="flex-col items-start">
50
- <div className="text-xs font-medium">{user?.name}</div>
51
- <div className="text-xs text-zinc-500">{user?.email}</div>
52
- </DropdownMenuItem>
53
- <DropdownMenuSeparator />
54
- <DropdownMenuItem
55
- onClick={() =>
56
- signOut({
57
- callbackUrl: '/',
58
- })
59
- }
60
- className="text-xs"
61
- >
62
- Log Out
63
- </DropdownMenuItem>
64
- </DropdownMenuContent>
65
- </DropdownMenu>
66
- </div>
67
- );
68
  }
 
6
 
7
  import { Button } from '@/components/ui/Button';
8
  import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
  } from '@/components/ui/DropdownMenu';
15
  import { IconExternalLink } from '@/components/ui/Icons';
16
 
17
  export interface UserMenuProps {
18
+ user: Session['user'];
19
  }
20
 
21
  function getUserInitials(name: string) {
22
+ const [firstName, lastName] = name.split(' ');
23
+ return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2);
24
  }
25
 
26
  export function UserMenu({ user }: UserMenuProps) {
27
+ return (
28
+ <div className="flex items-center justify-between">
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>
48
+ <DropdownMenuContent sideOffset={8} align="start" className="w-[180px]">
49
+ <DropdownMenuItem className="flex-col items-start">
50
+ <div className="text-xs font-medium">{user?.name}</div>
51
+ <div className="text-xs text-zinc-500">{user?.email}</div>
52
+ </DropdownMenuItem>
53
+ <DropdownMenuSeparator />
54
+ <DropdownMenuItem
55
+ onClick={() =>
56
+ signOut({
57
+ callbackUrl: '/',
58
+ })
59
+ }
60
+ className="text-xs"
61
+ >
62
+ Log Out
63
+ </DropdownMenuItem>
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ </div>
67
+ );
68
  }
components/chat-sidebar/ChatCard.tsx CHANGED
@@ -5,25 +5,25 @@ import { useParams } from 'next/navigation';
5
  import { cn } from '@/lib/utils';
6
 
7
  export interface ChatCardProps {
8
- id: string;
9
- title: string;
10
  }
11
 
12
  const ChatCard: React.FC<ChatCardProps> = ({ id, title }) => {
13
- const { chatId: chatIdFromParam } = useParams();
14
- return (
15
- <Link
16
- className={cn(
17
- 'p-4 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
18
- chatIdFromParam === id && 'border-gray-500',
19
- )}
20
- href={`/chat/${id}`}
21
- >
22
- <div className="overflow-hidden">
23
- <p className="text-sm font-medium text-black mb-1">{title}</p>
24
- </div>
25
- </Link>
26
- );
27
  };
28
 
29
  export default ChatCard;
 
5
  import { cn } from '@/lib/utils';
6
 
7
  export interface ChatCardProps {
8
+ id: string;
9
+ title: string;
10
  }
11
 
12
  const ChatCard: React.FC<ChatCardProps> = ({ id, title }) => {
13
+ const { chatId: chatIdFromParam } = useParams();
14
+ return (
15
+ <Link
16
+ className={cn(
17
+ 'p-4 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
18
+ chatIdFromParam === id && 'border-gray-500',
19
+ )}
20
+ href={`/chat/${id}`}
21
+ >
22
+ <div className="overflow-hidden">
23
+ <p className="text-sm font-medium text-black mb-1">{title}</p>
24
+ </div>
25
+ </Link>
26
+ );
27
  };
28
 
29
  export default ChatCard;
components/chat-sidebar/ChatListSidebar.tsx CHANGED
@@ -4,13 +4,13 @@ import ChatCard from './ChatCard';
4
  export interface ChatSidebarListProps {}
5
 
6
  export default async function ChatSidebarList({}: ChatSidebarListProps) {
7
- const chats = await getKVChats();
8
- console.log('[Ming] ~ ChatSidebarList ~ chats:', chats);
9
- return (
10
- <>
11
- {chats.map(chat => (
12
- <ChatCard key={chat.id} id={chat.id} title={chat.title} />
13
- ))}
14
- </>
15
- );
16
  }
 
4
  export interface ChatSidebarListProps {}
5
 
6
  export default async function ChatSidebarList({}: ChatSidebarListProps) {
7
+ const chats = await getKVChats();
8
+ console.log('[Ming] ~ ChatSidebarList ~ chats:', chats);
9
+ return (
10
+ <>
11
+ {chats.map(chat => (
12
+ <ChatCard key={chat.id} id={chat.id} title={chat.title} />
13
+ ))}
14
+ </>
15
+ );
16
  }
components/chat/ButtonScrollToBottom.tsx CHANGED
@@ -8,27 +8,27 @@ import { Button, type ButtonProps } from '@/components/ui/Button';
8
  import { IconArrowDown } from '@/components/ui/Icons';
9
 
10
  export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
11
- const isAtBottom = useAtBottom();
12
 
13
- return (
14
- <Button
15
- variant="outline"
16
- size="icon"
17
- className={cn(
18
- 'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
19
- isAtBottom ? 'opacity-0' : 'opacity-100',
20
- className,
21
- )}
22
- onClick={() =>
23
- window.scrollTo({
24
- top: document.body.offsetHeight,
25
- behavior: 'smooth',
26
- })
27
- }
28
- {...props}
29
- >
30
- <IconArrowDown />
31
- <span className="sr-only">Scroll to bottom</span>
32
- </Button>
33
- );
34
  }
 
8
  import { IconArrowDown } from '@/components/ui/Icons';
9
 
10
  export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
11
+ const isAtBottom = useAtBottom();
12
 
13
+ return (
14
+ <Button
15
+ variant="outline"
16
+ size="icon"
17
+ className={cn(
18
+ 'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
19
+ isAtBottom ? 'opacity-0' : 'opacity-100',
20
+ className,
21
+ )}
22
+ onClick={() =>
23
+ window.scrollTo({
24
+ top: document.body.offsetHeight,
25
+ behavior: 'smooth',
26
+ })
27
+ }
28
+ {...props}
29
+ >
30
+ <IconArrowDown />
31
+ <span className="sr-only">Scroll to bottom</span>
32
+ </Button>
33
+ );
34
  }
components/chat/ChatList.tsx CHANGED
@@ -5,22 +5,22 @@ import { ChatMessage } from '@/components/chat/ChatMessage';
5
  import { MessageWithSelectedDataset } from '../../lib/types';
6
 
7
  export interface ChatList {
8
- messages: MessageWithSelectedDataset[];
9
  }
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) => (
17
- <div key={index}>
18
- <ChatMessage message={message} />
19
- {index < messages.length - 1 && (
20
- <Separator className="my-4 md:my-8" />
21
- )}
22
- </div>
23
- ))}
24
- </div>
25
- );
26
  }
 
5
  import { MessageWithSelectedDataset } from '../../lib/types';
6
 
7
  export interface ChatList {
8
+ messages: MessageWithSelectedDataset[];
9
  }
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) => (
17
+ <div key={index}>
18
+ <ChatMessage message={message} />
19
+ {index < messages.length - 1 && (
20
+ <Separator className="my-4 md:my-8" />
21
+ )}
22
+ </div>
23
+ ))}
24
+ </div>
25
+ );
26
  }
components/chat/ChatMessage.tsx CHANGED
@@ -12,66 +12,66 @@ import { ChatMessageActions } from '@/components/chat/ChatMessageActions';
12
  import { MessageWithSelectedDataset } from '../../lib/types';
13
 
14
  export interface ChatMessageProps {
15
- message: MessageWithSelectedDataset;
16
  }
17
 
18
  export function ChatMessage({ message, ...props }: ChatMessageProps) {
19
- return (
20
- <div className={cn('group relative mb-4 flex items-start')} {...props}>
21
- <div
22
- className={cn(
23
- 'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
24
- message.role === 'user'
25
- ? 'bg-background'
26
- : 'bg-primary text-primary-foreground',
27
- )}
28
- >
29
- {message.role === 'user' ? <IconUser /> : <IconOpenAI />}
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 }) {
37
- return <p className="mb-2 last:mb-0">{children}</p>;
38
- },
39
- code({ node, inline, className, children, ...props }) {
40
- if (children.length) {
41
- if (children[0] == '▍') {
42
- return (
43
- <span className="mt-1 cursor-default animate-pulse">▍</span>
44
- );
45
- }
46
 
47
- children[0] = (children[0] as string).replace('`▍`', '▍');
48
- }
49
 
50
- const match = /language-(\w+)/.exec(className || '');
51
 
52
- if (inline) {
53
- return (
54
- <code className={className} {...props}>
55
- {children}
56
- </code>
57
- );
58
- }
59
 
60
- return (
61
- <CodeBlock
62
- key={Math.random()}
63
- language={(match && match[1]) || ''}
64
- value={String(children).replace(/\n$/, '')}
65
- {...props}
66
- />
67
- );
68
- },
69
- }}
70
- >
71
- {message.content}
72
- </MemoizedReactMarkdown>
73
- <ChatMessageActions message={message} />
74
- </div>
75
- </div>
76
- );
77
  }
 
12
  import { MessageWithSelectedDataset } from '../../lib/types';
13
 
14
  export interface ChatMessageProps {
15
+ message: MessageWithSelectedDataset;
16
  }
17
 
18
  export function ChatMessage({ message, ...props }: ChatMessageProps) {
19
+ return (
20
+ <div className={cn('group relative mb-4 flex items-start')} {...props}>
21
+ <div
22
+ className={cn(
23
+ 'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
24
+ message.role === 'user'
25
+ ? 'bg-background'
26
+ : 'bg-primary text-primary-foreground',
27
+ )}
28
+ >
29
+ {message.role === 'user' ? <IconUser /> : <IconOpenAI />}
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 }) {
37
+ return <p className="mb-2 last:mb-0">{children}</p>;
38
+ },
39
+ code({ node, inline, className, children, ...props }) {
40
+ if (children.length) {
41
+ if (children[0] == '▍') {
42
+ return (
43
+ <span className="mt-1 cursor-default animate-pulse">▍</span>
44
+ );
45
+ }
46
 
47
+ children[0] = (children[0] as string).replace('`▍`', '▍');
48
+ }
49
 
50
+ const match = /language-(\w+)/.exec(className || '');
51
 
52
+ if (inline) {
53
+ return (
54
+ <code className={className} {...props}>
55
+ {children}
56
+ </code>
57
+ );
58
+ }
59
 
60
+ return (
61
+ <CodeBlock
62
+ key={Math.random()}
63
+ language={(match && match[1]) || ''}
64
+ value={String(children).replace(/\n$/, '')}
65
+ {...props}
66
+ />
67
+ );
68
+ },
69
+ }}
70
+ >
71
+ {message.content}
72
+ </MemoizedReactMarkdown>
73
+ <ChatMessageActions message={message} />
74
+ </div>
75
+ </div>
76
+ );
77
  }
components/chat/ChatMessageActions.tsx CHANGED
@@ -9,33 +9,33 @@ import { cn } from '@/lib/utils';
9
  import { MessageWithSelectedDataset } from '../../lib/types';
10
 
11
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
- message: MessageWithSelectedDataset;
13
  }
14
 
15
  export function ChatMessageActions({
16
- message,
17
- className,
18
- ...props
19
  }: ChatMessageActionsProps) {
20
- const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
21
 
22
- const onCopy = () => {
23
- if (isCopied) return;
24
- copyToClipboard(message.content);
25
- };
26
 
27
- return (
28
- <div
29
- className={cn(
30
- 'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
31
- className,
32
- )}
33
- {...props}
34
- >
35
- <Button variant="ghost" size="icon" onClick={onCopy}>
36
- {isCopied ? <IconCheck /> : <IconCopy />}
37
- <span className="sr-only">Copy message</span>
38
- </Button>
39
- </div>
40
- );
41
  }
 
9
  import { MessageWithSelectedDataset } from '../../lib/types';
10
 
11
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
+ message: MessageWithSelectedDataset;
13
  }
14
 
15
  export function ChatMessageActions({
16
+ message,
17
+ className,
18
+ ...props
19
  }: ChatMessageActionsProps) {
20
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
21
 
22
+ const onCopy = () => {
23
+ if (isCopied) return;
24
+ copyToClipboard(message.content);
25
+ };
26
 
27
+ return (
28
+ <div
29
+ className={cn(
30
+ 'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
31
+ className,
32
+ )}
33
+ {...props}
34
+ >
35
+ <Button variant="ghost" size="icon" onClick={onCopy}>
36
+ {isCopied ? <IconCheck /> : <IconCopy />}
37
+ <span className="sr-only">Copy message</span>
38
+ </Button>
39
+ </div>
40
+ );
41
  }
components/chat/ChatPanel.tsx CHANGED
@@ -8,68 +8,68 @@ import { IconRefresh, IconShare, IconStop } from '@/components/ui/Icons';
8
  import { MessageWithSelectedDataset } from '../../lib/types';
9
 
10
  export interface ChatPanelProps
11
- extends Pick<
12
- UseChatHelpers,
13
- 'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput'
14
- > {
15
- id?: string;
16
- title?: string;
17
- messages: MessageWithSelectedDataset[];
18
  }
19
 
20
  export function ChatPanel({
21
- id,
22
- title,
23
- isLoading,
24
- stop,
25
- append,
26
- reload,
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 />
36
- <div className="mx-auto sm:max-w-3xl sm:px-4">
37
- <div className="flex items-center justify-center h-12">
38
- {isLoading ? (
39
- <Button
40
- variant="outline"
41
- onClick={() => stop()}
42
- className="bg-background"
43
- >
44
- <IconStop className="mr-2" />
45
- Stop generating
46
- </Button>
47
- ) : (
48
- messages?.length >= 2 && (
49
- <div className="flex space-x-2">
50
- <Button variant="outline" onClick={() => reload()}>
51
- <IconRefresh className="mr-2" />
52
- Regenerate response
53
- </Button>
54
- </div>
55
- )
56
- )}
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,
63
- content: value,
64
- role: 'user',
65
- });
66
- }}
67
- input={input}
68
- setInput={setInput}
69
- isLoading={isLoading}
70
- />
71
- </div>
72
- </div>
73
- </div>
74
- );
75
  }
 
8
  import { MessageWithSelectedDataset } from '../../lib/types';
9
 
10
  export interface ChatPanelProps
11
+ extends Pick<
12
+ UseChatHelpers,
13
+ 'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput'
14
+ > {
15
+ id?: string;
16
+ title?: string;
17
+ messages: MessageWithSelectedDataset[];
18
  }
19
 
20
  export function ChatPanel({
21
+ id,
22
+ title,
23
+ isLoading,
24
+ stop,
25
+ append,
26
+ reload,
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 />
36
+ <div className="mx-auto sm:max-w-3xl sm:px-4">
37
+ <div className="flex items-center justify-center h-12">
38
+ {isLoading ? (
39
+ <Button
40
+ variant="outline"
41
+ onClick={() => stop()}
42
+ className="bg-background"
43
+ >
44
+ <IconStop className="mr-2" />
45
+ Stop generating
46
+ </Button>
47
+ ) : (
48
+ messages?.length >= 2 && (
49
+ <div className="flex space-x-2">
50
+ <Button variant="outline" onClick={() => reload()}>
51
+ <IconRefresh className="mr-2" />
52
+ Regenerate response
53
+ </Button>
54
+ </div>
55
+ )
56
+ )}
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,
63
+ content: value,
64
+ role: 'user',
65
+ });
66
+ }}
67
+ input={input}
68
+ setInput={setInput}
69
+ isLoading={isLoading}
70
+ />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ );
75
  }
components/chat/ChatScrollAnchor.tsx CHANGED
@@ -6,24 +6,24 @@ import { useInView } from 'react-intersection-observer';
6
  import { useAtBottom } from '@/lib/hooks/useAtBottom';
7
 
8
  interface ChatScrollAnchorProps {
9
- trackVisibility?: boolean;
10
  }
11
 
12
  export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
13
- const isAtBottom = useAtBottom();
14
- const { ref, entry, inView } = useInView({
15
- trackVisibility,
16
- delay: 100,
17
- rootMargin: '0px 0px -150px 0px',
18
- });
19
 
20
- React.useEffect(() => {
21
- if (isAtBottom && trackVisibility && !inView) {
22
- entry?.target.scrollIntoView({
23
- block: 'start',
24
- });
25
- }
26
- }, [inView, entry, isAtBottom, trackVisibility]);
27
 
28
- return <div ref={ref} className="h-px w-full" />;
29
  }
 
6
  import { useAtBottom } from '@/lib/hooks/useAtBottom';
7
 
8
  interface ChatScrollAnchorProps {
9
+ trackVisibility?: boolean;
10
  }
11
 
12
  export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
13
+ const isAtBottom = useAtBottom();
14
+ const { ref, entry, inView } = useInView({
15
+ trackVisibility,
16
+ delay: 100,
17
+ rootMargin: '0px 0px -150px 0px',
18
+ });
19
 
20
+ React.useEffect(() => {
21
+ if (isAtBottom && trackVisibility && !inView) {
22
+ entry?.target.scrollIntoView({
23
+ block: 'start',
24
+ });
25
+ }
26
+ }, [inView, entry, isAtBottom, trackVisibility]);
27
 
28
+ return <div ref={ref} className="h-px w-full" />;
29
  }
components/chat/EmptyScreen.tsx CHANGED
@@ -5,48 +5,48 @@ 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
  }
 
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/ImageList.tsx CHANGED
@@ -8,14 +8,14 @@ import { produce } from 'immer';
8
  export interface ImageListProps {}
9
 
10
  const ImageList: React.FC<ImageListProps> = () => {
11
- const { getRootProps, getInputProps, isDragActive } = useImageUpload({
12
- noClick: true,
13
- });
14
 
15
- const [dataset, setDataset] = useAtom(datasetAtom);
16
- return (
17
- <div className="relative size-full px-12 max-w-3xl mx-auto">
18
- {/* {dataset.length < 10 ? (
19
  <div className="col-span-full px-8 py-4 rounded-xl bg-blue-100 text-blue-400 mb-8">
20
  You can upload up to 10 images max by dragging image.
21
  </div>
@@ -24,53 +24,53 @@ const ImageList: React.FC<ImageListProps> = () => {
24
  You have reached the maximum limit of 10 images.
25
  </div>
26
  )} */}
27
- <div
28
- {...getRootProps()}
29
- className="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-4"
30
- >
31
- {dataset.map(entity => {
32
- const { url: imageSrc, name, selected } = entity;
33
- return (
34
- <div
35
- key={name}
36
- onClick={() =>
37
- setDataset(prev =>
38
- produce(prev, draft => {
39
- const index = draft.findIndex(d => d.name === name);
40
- draft[index].selected = !selected;
41
- }),
42
- )
43
- }
44
- className={`relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content ${selected ? 'border-4 border-blue-500' : ''}`}
45
- >
46
- <Image
47
- src={imageSrc}
48
- draggable={false}
49
- alt="dataset images"
50
- width={500}
51
- height={500}
52
- objectFit="cover"
53
- className="rounded-xl"
54
- />
55
- <div className="absolute bottom-0 left-0 bg-gray-800/50 text-white px-3 py-1 rounded-tr-lg">
56
- <p className="text-xs font-bold">{name}</p>
57
- </div>
58
- </div>
59
- );
60
- })}
61
- </div>
62
 
63
- {isDragActive && (
64
- <div
65
- {...getRootProps()}
66
- className="dropzone border-2 border-dashed border-gray-400 size-full absolute top-0 left-0 flex items-center justify-center rounded-lg cursor-pointer bg-gray-500/50"
67
- >
68
- <input {...getInputProps()} />
69
- <p className="text-white">Drop the files here ...</p>
70
- </div>
71
- )}
72
- </div>
73
- );
74
  };
75
 
76
  export default ImageList;
 
8
  export interface ImageListProps {}
9
 
10
  const ImageList: React.FC<ImageListProps> = () => {
11
+ const { getRootProps, getInputProps, isDragActive } = useImageUpload({
12
+ noClick: true,
13
+ });
14
 
15
+ const [dataset, setDataset] = useAtom(datasetAtom);
16
+ return (
17
+ <div className="relative size-full px-12 max-w-3xl mx-auto">
18
+ {/* {dataset.length < 10 ? (
19
  <div className="col-span-full px-8 py-4 rounded-xl bg-blue-100 text-blue-400 mb-8">
20
  You can upload up to 10 images max by dragging image.
21
  </div>
 
24
  You have reached the maximum limit of 10 images.
25
  </div>
26
  )} */}
27
+ <div
28
+ {...getRootProps()}
29
+ className="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-4"
30
+ >
31
+ {dataset.map(entity => {
32
+ const { url: imageSrc, name, selected } = entity;
33
+ return (
34
+ <div
35
+ key={name}
36
+ onClick={() =>
37
+ setDataset(prev =>
38
+ produce(prev, draft => {
39
+ const index = draft.findIndex(d => d.name === name);
40
+ draft[index].selected = !selected;
41
+ }),
42
+ )
43
+ }
44
+ className={`relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content ${selected ? 'border-4 border-blue-500' : ''}`}
45
+ >
46
+ <Image
47
+ src={imageSrc}
48
+ draggable={false}
49
+ alt="dataset images"
50
+ width={500}
51
+ height={500}
52
+ objectFit="cover"
53
+ className="rounded-xl"
54
+ />
55
+ <div className="absolute bottom-0 left-0 bg-gray-800/50 text-white px-3 py-1 rounded-tr-lg">
56
+ <p className="text-xs font-bold">{name}</p>
57
+ </div>
58
+ </div>
59
+ );
60
+ })}
61
+ </div>
62
 
63
+ {isDragActive && (
64
+ <div
65
+ {...getRootProps()}
66
+ className="dropzone border-2 border-dashed border-gray-400 size-full absolute top-0 left-0 flex items-center justify-center rounded-lg cursor-pointer bg-gray-500/50"
67
+ >
68
+ <input {...getInputProps()} />
69
+ <p className="text-white">Drop the files here ...</p>
70
+ </div>
71
+ )}
72
+ </div>
73
+ );
74
  };
75
 
76
  export default ImageList;
components/chat/ImageSelector.tsx CHANGED
@@ -6,52 +6,52 @@ import { fetcher } from '@/lib/utils';
6
  export interface ImageSelectorProps {}
7
 
8
  const examples = [
9
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
10
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
11
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
12
- 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
13
  ];
14
 
15
  const ImageSelector: React.FC<ImageSelectorProps> = () => {
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
- fetcher('/api/upload', {
45
- method: 'POST',
46
- body: JSON.stringify({ url: example }),
47
- })
48
- }
49
- />
50
- ))}
51
- </div>
52
- </div>
53
- </div>
54
- );
55
  };
56
 
57
  export default ImageSelector;
 
6
  export interface ImageSelectorProps {}
7
 
8
  const examples = [
9
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
10
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
11
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/house-exmaple.jpg',
12
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/safari-example.png',
13
  ];
14
 
15
  const ImageSelector: React.FC<ImageSelectorProps> = () => {
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
+ fetcher('/api/upload', {
45
+ method: 'POST',
46
+ body: JSON.stringify({ url: example }),
47
+ })
48
+ }
49
+ />
50
+ ))}
51
+ </div>
52
+ </div>
53
+ </div>
54
+ );
55
  };
56
 
57
  export default ImageSelector;
components/chat/MemoizedReactMarkdown.tsx CHANGED
@@ -2,8 +2,8 @@ import { FC, memo } from 'react';
2
  import ReactMarkdown, { Options } from 'react-markdown';
3
 
4
  export const MemoizedReactMarkdown: FC<Options> = memo(
5
- ReactMarkdown,
6
- (prevProps, nextProps) =>
7
- prevProps.children === nextProps.children &&
8
- prevProps.className === nextProps.className,
9
  );
 
2
  import ReactMarkdown, { Options } from 'react-markdown';
3
 
4
  export const MemoizedReactMarkdown: FC<Options> = memo(
5
+ ReactMarkdown,
6
+ (prevProps, nextProps) =>
7
+ prevProps.children === nextProps.children &&
8
+ prevProps.className === nextProps.className,
9
  );
components/chat/PromptForm.tsx CHANGED
@@ -5,48 +5,48 @@ import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit';
5
  import { cn } from '@/lib/utils';
6
  import { Button, buttonVariants } from '@/components/ui/Button';
7
  import {
8
- Tooltip,
9
- TooltipContent,
10
- TooltipTrigger,
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({
22
- onSubmit,
23
- input,
24
- setInput,
25
- isLoading,
26
  }: PromptProps) {
27
- const { formRef, onKeyDown } = useEnterSubmit();
28
- const inputRef = React.useRef<HTMLTextAreaElement>(null);
29
- const router = useRouter();
30
- React.useEffect(() => {
31
- if (inputRef.current) {
32
- inputRef.current.focus();
33
- }
34
- }, []);
35
 
36
- return (
37
- <form
38
- onSubmit={async e => {
39
- e.preventDefault();
40
- if (!input?.trim()) {
41
- return;
42
- }
43
- setInput('');
44
- await onSubmit(input);
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 => {
@@ -65,33 +65,33 @@ export function PromptForm({
65
  </TooltipTrigger>
66
  <TooltipContent>New Chat</TooltipContent>
67
  </Tooltip> */}
68
- <Textarea
69
- ref={inputRef}
70
- tabIndex={0}
71
- onKeyDown={onKeyDown}
72
- rows={1}
73
- value={input}
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>
81
- <TooltipTrigger asChild>
82
- <Button
83
- type="submit"
84
- size="icon"
85
- disabled={isLoading || input === ''}
86
- >
87
- <IconArrowElbow />
88
- <span className="sr-only">Send message</span>
89
- </Button>
90
- </TooltipTrigger>
91
- <TooltipContent>Send message</TooltipContent>
92
- </Tooltip>
93
- </div>
94
- </div>
95
- </form>
96
- );
97
  }
 
5
  import { cn } from '@/lib/utils';
6
  import { Button, buttonVariants } from '@/components/ui/Button';
7
  import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipTrigger,
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({
22
+ onSubmit,
23
+ input,
24
+ setInput,
25
+ isLoading,
26
  }: PromptProps) {
27
+ const { formRef, onKeyDown } = useEnterSubmit();
28
+ const inputRef = React.useRef<HTMLTextAreaElement>(null);
29
+ const router = useRouter();
30
+ React.useEffect(() => {
31
+ if (inputRef.current) {
32
+ inputRef.current.focus();
33
+ }
34
+ }, []);
35
 
36
+ return (
37
+ <form
38
+ onSubmit={async e => {
39
+ e.preventDefault();
40
+ if (!input?.trim()) {
41
+ return;
42
+ }
43
+ setInput('');
44
+ await onSubmit(input);
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 => {
 
65
  </TooltipTrigger>
66
  <TooltipContent>New Chat</TooltipContent>
67
  </Tooltip> */}
68
+ <Textarea
69
+ ref={inputRef}
70
+ tabIndex={0}
71
+ onKeyDown={onKeyDown}
72
+ rows={1}
73
+ value={input}
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>
81
+ <TooltipTrigger asChild>
82
+ <Button
83
+ type="submit"
84
+ size="icon"
85
+ disabled={isLoading || input === ''}
86
+ >
87
+ <IconArrowElbow />
88
+ <span className="sr-only">Send message</span>
89
+ </Button>
90
+ </TooltipTrigger>
91
+ <TooltipContent>Send message</TooltipContent>
92
+ </Tooltip>
93
+ </div>
94
+ </div>
95
+ </form>
96
+ );
97
  }
components/chat/index.tsx CHANGED
@@ -10,37 +10,37 @@ import { Button } from '../ui/Button';
10
  import ImageSelector from './ImageSelector';
11
 
12
  export interface ChatProps extends React.ComponentProps<'div'> {
13
- id: string;
14
  }
15
 
16
  export function Chat({ id, className }: ChatProps) {
17
- const { messages, append, reload, stop, isLoading, input, setInput } =
18
- useChat();
19
 
20
- return (
21
- <>
22
- <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
23
- <div className="flex h-full">
24
- <div className="w-1/2 relative border-r border-gray-400 overflow-auto">
25
- {/* <ImageList /> */}
26
- <ImageSelector />
27
- </div>
28
- <div className="w-1/2 relative overflow-auto">
29
- <ChatList messages={messages} />
30
- <ChatScrollAnchor trackVisibility={isLoading} />
31
- </div>
32
- </div>
33
- </div>
34
- <ChatPanel
35
- id={id}
36
- isLoading={isLoading}
37
- stop={stop}
38
- append={append}
39
- reload={reload}
40
- messages={messages}
41
- input={input}
42
- setInput={setInput}
43
- />
44
- </>
45
- );
46
  }
 
10
  import ImageSelector from './ImageSelector';
11
 
12
  export interface ChatProps extends React.ComponentProps<'div'> {
13
+ id: string;
14
  }
15
 
16
  export function Chat({ id, className }: ChatProps) {
17
+ const { messages, append, reload, stop, isLoading, input, setInput } =
18
+ useChat();
19
 
20
+ return (
21
+ <>
22
+ <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
23
+ <div className="flex h-full">
24
+ <div className="w-1/2 relative border-r border-gray-400 overflow-auto">
25
+ {/* <ImageList /> */}
26
+ <ImageSelector />
27
+ </div>
28
+ <div className="w-1/2 relative overflow-auto">
29
+ <ChatList messages={messages} />
30
+ <ChatScrollAnchor trackVisibility={isLoading} />
31
+ </div>
32
+ </div>
33
+ </div>
34
+ <ChatPanel
35
+ id={id}
36
+ isLoading={isLoading}
37
+ stop={stop}
38
+ append={append}
39
+ reload={reload}
40
+ messages={messages}
41
+ input={input}
42
+ setInput={setInput}
43
+ />
44
+ </>
45
+ );
46
  }
components/project-sidebar/ProjectCard.tsx CHANGED
@@ -8,36 +8,36 @@ import { cn } from '@/lib/utils';
8
  import Chip from '../ui/Chip';
9
 
10
  export interface ProjectCardProps {
11
- projectInfo: ProjectBaseInfo;
12
  }
13
 
14
  const ProjectCard: React.FC<ProjectCardProps> = ({ projectInfo }) => {
15
- const {
16
- id,
17
- name,
18
- created_at,
19
- label_type,
20
- organization: { name: orgName },
21
- } = projectInfo;
22
 
23
- const { projectId: projectIdFromParam } = useParams();
24
 
25
- const formattedDate = format(created_at, 'yyyy-MM-dd');
26
- return (
27
- <Link
28
- className={cn(
29
- 'p-4 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
30
- Number(projectIdFromParam) === id && 'border-gray-500',
31
- )}
32
- href={`/project/${id}`}
33
- >
34
- <div className="overflow-hidden">
35
- <p className="text-xs text-gray-500">{orgName}</p>
36
- <p className="text-sm font-medium text-black mb-1">{name}</p>
37
- <p className="text-xs text-gray-500">{formattedDate}</p>
38
- </div>
39
- </Link>
40
- );
41
  };
42
 
43
  export default ProjectCard;
 
8
  import Chip from '../ui/Chip';
9
 
10
  export interface ProjectCardProps {
11
+ projectInfo: ProjectBaseInfo;
12
  }
13
 
14
  const ProjectCard: React.FC<ProjectCardProps> = ({ projectInfo }) => {
15
+ const {
16
+ id,
17
+ name,
18
+ created_at,
19
+ label_type,
20
+ organization: { name: orgName },
21
+ } = projectInfo;
22
 
23
+ const { projectId: projectIdFromParam } = useParams();
24
 
25
+ const formattedDate = format(created_at, 'yyyy-MM-dd');
26
+ return (
27
+ <Link
28
+ className={cn(
29
+ 'p-4 m-2 bg-white l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
30
+ Number(projectIdFromParam) === id && 'border-gray-500',
31
+ )}
32
+ href={`/project/${id}`}
33
+ >
34
+ <div className="overflow-hidden">
35
+ <p className="text-xs text-gray-500">{orgName}</p>
36
+ <p className="text-sm font-medium text-black mb-1">{name}</p>
37
+ <p className="text-xs text-gray-500">{formattedDate}</p>
38
+ </div>
39
+ </Link>
40
+ );
41
  };
42
 
43
  export default ProjectCard;
components/project-sidebar/ProjectListSideBar.tsx CHANGED
@@ -4,14 +4,14 @@ import ProjectCard from './ProjectCard';
4
  export interface ProjectListSideBarProps {}
5
 
6
  const ProjectListSideBar: React.FC<ProjectListSideBarProps> = async () => {
7
- const recentProjects = await fetchRecentProjectList();
8
- return (
9
- <>
10
- {recentProjects.map(project => (
11
- <ProjectCard key={project.id} projectInfo={project} />
12
- ))}
13
- </>
14
- );
15
  };
16
 
17
  export default ProjectListSideBar;
 
4
  export interface ProjectListSideBarProps {}
5
 
6
  const ProjectListSideBar: React.FC<ProjectListSideBarProps> = async () => {
7
+ const recentProjects = await fetchRecentProjectList();
8
+ return (
9
+ <>
10
+ {recentProjects.map(project => (
11
+ <ProjectCard key={project.id} projectInfo={project} />
12
+ ))}
13
+ </>
14
+ );
15
  };
16
 
17
  export default ProjectListSideBar;
components/project/Chat.tsx CHANGED
@@ -8,27 +8,27 @@ import { ChatScrollAnchor } from '../chat/ChatScrollAnchor';
8
  import { ChatPanel } from '../chat/ChatPanel';
9
 
10
  export interface ChatProps {
11
- mediaList: MediaDetails[];
12
  }
13
 
14
  const Chat: React.FC<ChatProps> = ({ mediaList }) => {
15
- const { messages, append, reload, stop, isLoading, input, setInput } =
16
- useChatWithMedia(mediaList);
17
- return (
18
- <>
19
- <ChatList messages={messages} />
20
- <ChatScrollAnchor trackVisibility={isLoading} />
21
- <ChatPanel
22
- isLoading={isLoading}
23
- stop={stop}
24
- append={append}
25
- reload={reload}
26
- messages={messages}
27
- input={input}
28
- setInput={setInput}
29
- />
30
- </>
31
- );
32
  };
33
 
34
  export default Chat;
 
8
  import { ChatPanel } from '../chat/ChatPanel';
9
 
10
  export interface ChatProps {
11
+ mediaList: MediaDetails[];
12
  }
13
 
14
  const Chat: React.FC<ChatProps> = ({ mediaList }) => {
15
+ const { messages, append, reload, stop, isLoading, input, setInput } =
16
+ useChatWithMedia(mediaList);
17
+ return (
18
+ <>
19
+ <ChatList messages={messages} />
20
+ <ChatScrollAnchor trackVisibility={isLoading} />
21
+ <ChatPanel
22
+ isLoading={isLoading}
23
+ stop={stop}
24
+ append={append}
25
+ reload={reload}
26
+ messages={messages}
27
+ input={input}
28
+ setInput={setInput}
29
+ />
30
+ </>
31
+ );
32
  };
33
 
34
  export default Chat;
components/project/MediaGrid.tsx CHANGED
@@ -2,17 +2,17 @@ import { MediaDetails } from '@/lib/fetch';
2
  import MediaTile from './MediaTile';
3
 
4
  export default function MediaGrid({
5
- mediaList,
6
  }: {
7
- mediaList: MediaDetails[];
8
  }) {
9
- return (
10
- <div className="relative size-full p-6 max-w-3xl mx-auto">
11
- <div className="columns-1 sm:columns-1 md:columns-2 lg:columns-2 xl:columns:3 gap-4 [&>img:not(:first-child)]:mt-4">
12
- {mediaList.map(media => (
13
- <MediaTile key={media.id} media={media} />
14
- ))}
15
- </div>
16
- </div>
17
- );
18
  }
 
2
  import MediaTile from './MediaTile';
3
 
4
  export default function MediaGrid({
5
+ mediaList,
6
  }: {
7
+ mediaList: MediaDetails[];
8
  }) {
9
+ return (
10
+ <div className="relative size-full p-6 max-w-3xl mx-auto">
11
+ <div className="columns-1 sm:columns-1 md:columns-2 lg:columns-2 xl:columns:3 gap-4 [&>img:not(:first-child)]:mt-4">
12
+ {mediaList.map(media => (
13
+ <MediaTile key={media.id} media={media} />
14
+ ))}
15
+ </div>
16
+ </div>
17
+ );
18
  }
components/project/MediaTile.tsx CHANGED
@@ -1,42 +1,42 @@
1
  import React from 'react';
2
  import Image from 'next/image';
3
  import {
4
- Tooltip,
5
- TooltipContent,
6
- TooltipTrigger,
7
  } from '@/components/ui/Tooltip';
8
  import { MediaDetails } from '@/lib/fetch';
9
 
10
  export interface MediaTileProps {
11
- media: MediaDetails;
12
  }
13
 
14
  export default function MediaTile({ media }: MediaTileProps) {
15
- const {
16
- url,
17
- thumbnails,
18
- id,
19
- name,
20
- properties: { width, height },
21
- } = media;
22
- // const imageSrc = thumbnails.length ? thumbnails[thumbnails.length - 1] : url;
23
- const imageSrc = url;
24
- return (
25
- <Tooltip>
26
- <TooltipTrigger asChild>
27
- <Image
28
- src={imageSrc}
29
- draggable={false}
30
- alt="dataset images"
31
- width={width}
32
- height={height}
33
- className="w-full h-auto relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content"
34
- />
35
- </TooltipTrigger>
36
- <TooltipContent>
37
- <p>{name}</p>
38
- <p className="font-light text-xs">{`${width} x ${height}`}</p>
39
- </TooltipContent>
40
- </Tooltip>
41
- );
42
  }
 
1
  import React from 'react';
2
  import Image from 'next/image';
3
  import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipTrigger,
7
  } from '@/components/ui/Tooltip';
8
  import { MediaDetails } from '@/lib/fetch';
9
 
10
  export interface MediaTileProps {
11
+ media: MediaDetails;
12
  }
13
 
14
  export default function MediaTile({ media }: MediaTileProps) {
15
+ const {
16
+ url,
17
+ thumbnails,
18
+ id,
19
+ name,
20
+ properties: { width, height },
21
+ } = media;
22
+ // const imageSrc = thumbnails.length ? thumbnails[thumbnails.length - 1] : url;
23
+ const imageSrc = url;
24
+ return (
25
+ <Tooltip>
26
+ <TooltipTrigger asChild>
27
+ <Image
28
+ src={imageSrc}
29
+ draggable={false}
30
+ alt="dataset images"
31
+ width={width}
32
+ height={height}
33
+ className="w-full h-auto relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content"
34
+ />
35
+ </TooltipTrigger>
36
+ <TooltipContent>
37
+ <p>{name}</p>
38
+ <p className="font-light text-xs">{`${width} x ${height}`}</p>
39
+ </TooltipContent>
40
+ </Tooltip>
41
+ );
42
  }
components/ui/Button.tsx CHANGED
@@ -5,52 +5,52 @@ import { cva, type VariantProps } from 'class-variance-authority';
5
  import { cn } from '@/lib/utils';
6
 
7
  const buttonVariants = cva(
8
- 'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9
- {
10
- variants: {
11
- variant: {
12
- default:
13
- 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14
- destructive:
15
- 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16
- outline:
17
- 'border border-input hover:bg-accent hover:text-accent-foreground',
18
- secondary:
19
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20
- ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21
- link: 'text-primary underline-offset-4 shadow-none hover:underline',
22
- },
23
- size: {
24
- default: 'h-8 px-4 py-2',
25
- sm: 'h-8 rounded-md px-3',
26
- lg: 'h-11 rounded-md px-8',
27
- icon: 'size-8 p-0',
28
- },
29
- },
30
- defaultVariants: {
31
- variant: 'default',
32
- size: 'default',
33
- },
34
- },
35
  );
36
 
37
  export interface ButtonProps
38
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
- VariantProps<typeof buttonVariants> {
40
- asChild?: boolean;
41
  }
42
 
43
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
- ({ className, variant, size, asChild = false, ...props }, ref) => {
45
- const Comp = asChild ? Slot : 'button';
46
- return (
47
- <Comp
48
- className={cn(buttonVariants({ variant, size, className }))}
49
- ref={ref}
50
- {...props}
51
- />
52
- );
53
- },
54
  );
55
  Button.displayName = 'Button';
56
 
 
5
  import { cn } from '@/lib/utils';
6
 
7
  const buttonVariants = cva(
8
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14
+ destructive:
15
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16
+ outline:
17
+ 'border border-input hover:bg-accent hover:text-accent-foreground',
18
+ secondary:
19
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20
+ ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21
+ link: 'text-primary underline-offset-4 shadow-none hover:underline',
22
+ },
23
+ size: {
24
+ default: 'h-8 px-4 py-2',
25
+ sm: 'h-8 rounded-md px-3',
26
+ lg: 'h-11 rounded-md px-8',
27
+ icon: 'size-8 p-0',
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: 'default',
32
+ size: 'default',
33
+ },
34
+ },
35
  );
36
 
37
  export interface ButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean;
41
  }
42
 
43
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : 'button';
46
+ return (
47
+ <Comp
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ ref={ref}
50
+ {...props}
51
+ />
52
+ );
53
+ },
54
  );
55
  Button.displayName = 'Button';
56
 
components/ui/Chip.tsx CHANGED
@@ -1,29 +1,29 @@
1
  import { cn } from '@/lib/utils';
2
 
3
  export interface ChipProps {
4
- label?: string;
5
- value: string;
6
- color?: 'gray' | 'blue';
7
- className?: string;
8
  }
9
 
10
  const Chip: React.FC<ChipProps> = ({
11
- label,
12
- value,
13
- color = 'gray',
14
- className,
15
  }) => {
16
- return (
17
- <div
18
- className={cn(
19
- `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-${color}-100 text-${color}-800 mr-1`,
20
- className,
21
- )}
22
- >
23
- {label && <span className="font-medium">{label} :</span>}
24
- <span>{value}</span>
25
- </div>
26
- );
27
  };
28
 
29
  export default Chip;
 
1
  import { cn } from '@/lib/utils';
2
 
3
  export interface ChipProps {
4
+ label?: string;
5
+ value: string;
6
+ color?: 'gray' | 'blue';
7
+ className?: string;
8
  }
9
 
10
  const Chip: React.FC<ChipProps> = ({
11
+ label,
12
+ value,
13
+ color = 'gray',
14
+ className,
15
  }) => {
16
+ return (
17
+ <div
18
+ className={cn(
19
+ `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-${color}-100 text-${color}-800 mr-1`,
20
+ className,
21
+ )}
22
+ >
23
+ {label && <span className="font-medium">{label} :</span>}
24
+ <span>{value}</span>
25
+ </div>
26
+ );
27
  };
28
 
29
  export default Chip;
components/ui/CodeBlock.tsx CHANGED
@@ -12,136 +12,136 @@ import { IconCheck, IconCopy, IconDownload } from '@/components/ui/Icons';
12
  import { Button } from '@/components/ui/Button';
13
 
14
  interface Props {
15
- language: string;
16
- value: string;
17
  }
18
 
19
  interface languageMap {
20
- [key: string]: string | undefined;
21
  }
22
 
23
  export const programmingLanguages: languageMap = {
24
- javascript: '.js',
25
- python: '.py',
26
- java: '.java',
27
- c: '.c',
28
- cpp: '.cpp',
29
- 'c++': '.cpp',
30
- 'c#': '.cs',
31
- ruby: '.rb',
32
- php: '.php',
33
- swift: '.swift',
34
- 'objective-c': '.m',
35
- kotlin: '.kt',
36
- typescript: '.ts',
37
- go: '.go',
38
- perl: '.pl',
39
- rust: '.rs',
40
- scala: '.scala',
41
- haskell: '.hs',
42
- lua: '.lua',
43
- shell: '.sh',
44
- sql: '.sql',
45
- html: '.html',
46
- css: '.css',
47
- // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48
  };
49
 
50
  export const generateRandomString = (length: number, lowercase = false) => {
51
- const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0
52
- let result = '';
53
- for (let i = 0; i < length; i++) {
54
- result += chars.charAt(Math.floor(Math.random() * chars.length));
55
- }
56
- return lowercase ? result.toLowerCase() : result;
57
  };
58
 
59
  const CodeBlock: FC<Props> = memo(({ language, value }) => {
60
- const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
61
 
62
- const downloadAsFile = () => {
63
- if (typeof window === 'undefined') {
64
- return;
65
- }
66
- const fileExtension = programmingLanguages[language] || '.file';
67
- const suggestedFileName = `file-${generateRandomString(
68
- 3,
69
- true,
70
- )}${fileExtension}`;
71
- const fileName = window.prompt('Enter file name' || '', suggestedFileName);
72
 
73
- if (!fileName) {
74
- // User pressed cancel on prompt.
75
- return;
76
- }
77
 
78
- const blob = new Blob([value], { type: 'text/plain' });
79
- const url = URL.createObjectURL(blob);
80
- const link = document.createElement('a');
81
- link.download = fileName;
82
- link.href = url;
83
- link.style.display = 'none';
84
- document.body.appendChild(link);
85
- link.click();
86
- document.body.removeChild(link);
87
- URL.revokeObjectURL(url);
88
- };
89
 
90
- const onCopy = () => {
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
101
- variant="ghost"
102
- className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
103
- onClick={downloadAsFile}
104
- size="icon"
105
- >
106
- <IconDownload />
107
- <span className="sr-only">Download</span>
108
- </Button>
109
- <Button
110
- variant="ghost"
111
- size="icon"
112
- className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
113
- onClick={onCopy}
114
- >
115
- {isCopied ? <IconCheck /> : <IconCopy />}
116
- <span className="sr-only">Copy code</span>
117
- </Button>
118
- </div>
119
- </div>
120
- <SyntaxHighlighter
121
- language={language}
122
- style={coldarkDark}
123
- PreTag="div"
124
- showLineNumbers
125
- customStyle={{
126
- margin: 0,
127
- width: '100%',
128
- background: 'transparent',
129
- padding: '1.5rem 1rem',
130
- }}
131
- lineNumberStyle={{
132
- userSelect: 'none',
133
- }}
134
- codeTagProps={{
135
- style: {
136
- fontSize: '0.9rem',
137
- fontFamily: 'var(--font-mono)',
138
- },
139
- }}
140
- >
141
- {value}
142
- </SyntaxHighlighter>
143
- </div>
144
- );
145
  });
146
  CodeBlock.displayName = 'CodeBlock';
147
 
 
12
  import { Button } from '@/components/ui/Button';
13
 
14
  interface Props {
15
+ language: string;
16
+ value: string;
17
  }
18
 
19
  interface languageMap {
20
+ [key: string]: string | undefined;
21
  }
22
 
23
  export const programmingLanguages: languageMap = {
24
+ javascript: '.js',
25
+ python: '.py',
26
+ java: '.java',
27
+ c: '.c',
28
+ cpp: '.cpp',
29
+ 'c++': '.cpp',
30
+ 'c#': '.cs',
31
+ ruby: '.rb',
32
+ php: '.php',
33
+ swift: '.swift',
34
+ 'objective-c': '.m',
35
+ kotlin: '.kt',
36
+ typescript: '.ts',
37
+ go: '.go',
38
+ perl: '.pl',
39
+ rust: '.rs',
40
+ scala: '.scala',
41
+ haskell: '.hs',
42
+ lua: '.lua',
43
+ shell: '.sh',
44
+ sql: '.sql',
45
+ html: '.html',
46
+ css: '.css',
47
+ // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48
  };
49
 
50
  export const generateRandomString = (length: number, lowercase = false) => {
51
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0
52
+ let result = '';
53
+ for (let i = 0; i < length; i++) {
54
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
55
+ }
56
+ return lowercase ? result.toLowerCase() : result;
57
  };
58
 
59
  const CodeBlock: FC<Props> = memo(({ language, value }) => {
60
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
61
 
62
+ const downloadAsFile = () => {
63
+ if (typeof window === 'undefined') {
64
+ return;
65
+ }
66
+ const fileExtension = programmingLanguages[language] || '.file';
67
+ const suggestedFileName = `file-${generateRandomString(
68
+ 3,
69
+ true,
70
+ )}${fileExtension}`;
71
+ const fileName = window.prompt('Enter file name' || '', suggestedFileName);
72
 
73
+ if (!fileName) {
74
+ // User pressed cancel on prompt.
75
+ return;
76
+ }
77
 
78
+ const blob = new Blob([value], { type: 'text/plain' });
79
+ const url = URL.createObjectURL(blob);
80
+ const link = document.createElement('a');
81
+ link.download = fileName;
82
+ link.href = url;
83
+ link.style.display = 'none';
84
+ document.body.appendChild(link);
85
+ link.click();
86
+ document.body.removeChild(link);
87
+ URL.revokeObjectURL(url);
88
+ };
89
 
90
+ const onCopy = () => {
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
101
+ variant="ghost"
102
+ className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
103
+ onClick={downloadAsFile}
104
+ size="icon"
105
+ >
106
+ <IconDownload />
107
+ <span className="sr-only">Download</span>
108
+ </Button>
109
+ <Button
110
+ variant="ghost"
111
+ size="icon"
112
+ className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
113
+ onClick={onCopy}
114
+ >
115
+ {isCopied ? <IconCheck /> : <IconCopy />}
116
+ <span className="sr-only">Copy code</span>
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ <SyntaxHighlighter
121
+ language={language}
122
+ style={coldarkDark}
123
+ PreTag="div"
124
+ showLineNumbers
125
+ customStyle={{
126
+ margin: 0,
127
+ width: '100%',
128
+ background: 'transparent',
129
+ padding: '1.5rem 1rem',
130
+ }}
131
+ lineNumberStyle={{
132
+ userSelect: 'none',
133
+ }}
134
+ codeTagProps={{
135
+ style: {
136
+ fontSize: '0.9rem',
137
+ fontFamily: 'var(--font-mono)',
138
+ },
139
+ }}
140
+ >
141
+ {value}
142
+ </SyntaxHighlighter>
143
+ </div>
144
+ );
145
  });
146
  CodeBlock.displayName = 'CodeBlock';
147
 
components/ui/DropdownMenu.tsx CHANGED
@@ -18,111 +18,111 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18
  const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
19
 
20
  const DropdownMenuSubContent = React.forwardRef<
21
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
22
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
23
  >(({ className, ...props }, ref) => (
24
- <DropdownMenuPrimitive.SubContent
25
- ref={ref}
26
- className={cn(
27
- 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
28
- className,
29
- )}
30
- {...props}
31
- />
32
  ));
33
  DropdownMenuSubContent.displayName =
34
- DropdownMenuPrimitive.SubContent.displayName;
35
 
36
  const DropdownMenuContent = React.forwardRef<
37
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
38
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
39
  >(({ className, sideOffset = 4, ...props }, ref) => (
40
- <DropdownMenuPrimitive.Portal>
41
- <DropdownMenuPrimitive.Content
42
- ref={ref}
43
- sideOffset={sideOffset}
44
- className={cn(
45
- 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
46
- className,
47
- )}
48
- {...props}
49
- />
50
- </DropdownMenuPrimitive.Portal>
51
  ));
52
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
53
 
54
  const DropdownMenuItem = React.forwardRef<
55
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
56
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
57
- inset?: boolean;
58
- }
59
  >(({ className, inset, ...props }, ref) => (
60
- <DropdownMenuPrimitive.Item
61
- ref={ref}
62
- className={cn(
63
- 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
64
- inset && 'pl-8',
65
- className,
66
- )}
67
- {...props}
68
- />
69
  ));
70
  DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
71
 
72
  const DropdownMenuLabel = React.forwardRef<
73
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
74
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
75
- inset?: boolean;
76
- }
77
  >(({ className, inset, ...props }, ref) => (
78
- <DropdownMenuPrimitive.Label
79
- ref={ref}
80
- className={cn(
81
- 'px-2 py-1.5 text-sm font-semibold',
82
- inset && 'pl-8',
83
- className,
84
- )}
85
- {...props}
86
- />
87
  ));
88
  DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
89
 
90
  const DropdownMenuSeparator = React.forwardRef<
91
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
92
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
93
  >(({ className, ...props }, ref) => (
94
- <DropdownMenuPrimitive.Separator
95
- ref={ref}
96
- className={cn('-mx-1 my-1 h-px bg-muted', className)}
97
- {...props}
98
- />
99
  ));
100
  DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
101
 
102
  const DropdownMenuShortcut = ({
103
- className,
104
- ...props
105
  }: React.HTMLAttributes<HTMLSpanElement>) => {
106
- return (
107
- <span
108
- className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
109
- {...props}
110
- />
111
- );
112
  };
113
  DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
114
 
115
  export {
116
- DropdownMenu,
117
- DropdownMenuTrigger,
118
- DropdownMenuContent,
119
- DropdownMenuItem,
120
- DropdownMenuLabel,
121
- DropdownMenuSeparator,
122
- DropdownMenuShortcut,
123
- DropdownMenuGroup,
124
- DropdownMenuPortal,
125
- DropdownMenuSub,
126
- DropdownMenuSubContent,
127
- DropdownMenuRadioGroup,
128
  };
 
18
  const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
19
 
20
  const DropdownMenuSubContent = React.forwardRef<
21
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
22
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
23
  >(({ className, ...props }, ref) => (
24
+ <DropdownMenuPrimitive.SubContent
25
+ ref={ref}
26
+ className={cn(
27
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
  ));
33
  DropdownMenuSubContent.displayName =
34
+ DropdownMenuPrimitive.SubContent.displayName;
35
 
36
  const DropdownMenuContent = React.forwardRef<
37
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
38
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
39
  >(({ className, sideOffset = 4, ...props }, ref) => (
40
+ <DropdownMenuPrimitive.Portal>
41
+ <DropdownMenuPrimitive.Content
42
+ ref={ref}
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ </DropdownMenuPrimitive.Portal>
51
  ));
52
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
53
 
54
  const DropdownMenuItem = React.forwardRef<
55
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
56
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
57
+ inset?: boolean;
58
+ }
59
  >(({ className, inset, ...props }, ref) => (
60
+ <DropdownMenuPrimitive.Item
61
+ ref={ref}
62
+ className={cn(
63
+ 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
64
+ inset && 'pl-8',
65
+ className,
66
+ )}
67
+ {...props}
68
+ />
69
  ));
70
  DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
71
 
72
  const DropdownMenuLabel = React.forwardRef<
73
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
74
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
75
+ inset?: boolean;
76
+ }
77
  >(({ className, inset, ...props }, ref) => (
78
+ <DropdownMenuPrimitive.Label
79
+ ref={ref}
80
+ className={cn(
81
+ 'px-2 py-1.5 text-sm font-semibold',
82
+ inset && 'pl-8',
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
  ));
88
  DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
89
 
90
  const DropdownMenuSeparator = React.forwardRef<
91
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
92
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
93
  >(({ className, ...props }, ref) => (
94
+ <DropdownMenuPrimitive.Separator
95
+ ref={ref}
96
+ className={cn('-mx-1 my-1 h-px bg-muted', className)}
97
+ {...props}
98
+ />
99
  ));
100
  DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
101
 
102
  const DropdownMenuShortcut = ({
103
+ className,
104
+ ...props
105
  }: React.HTMLAttributes<HTMLSpanElement>) => {
106
+ return (
107
+ <span
108
+ className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
109
+ {...props}
110
+ />
111
+ );
112
  };
113
  DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
114
 
115
  export {
116
+ DropdownMenu,
117
+ DropdownMenuTrigger,
118
+ DropdownMenuContent,
119
+ DropdownMenuItem,
120
+ DropdownMenuLabel,
121
+ DropdownMenuSeparator,
122
+ DropdownMenuShortcut,
123
+ DropdownMenuGroup,
124
+ DropdownMenuPortal,
125
+ DropdownMenuSub,
126
+ DropdownMenuSubContent,
127
+ DropdownMenuRadioGroup,
128
  };
components/ui/Icons.tsx CHANGED
@@ -5,557 +5,557 @@ import * as React from 'react';
5
  import { cn } from '@/lib/utils';
6
 
7
  function IconNextChat({
8
- className,
9
- inverted,
10
- ...props
11
  }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12
- const id = React.useId();
13
-
14
- return (
15
- <svg
16
- viewBox="0 0 17 17"
17
- fill="none"
18
- xmlns="http://www.w3.org/2000/svg"
19
- className={cn('size-4', className)}
20
- {...props}
21
- >
22
- <defs>
23
- <linearGradient
24
- id={`gradient-${id}-1`}
25
- x1="10.6889"
26
- y1="10.3556"
27
- x2="13.8445"
28
- y2="14.2667"
29
- gradientUnits="userSpaceOnUse"
30
- >
31
- <stop stopColor={inverted ? 'white' : 'black'} />
32
- <stop
33
- offset={1}
34
- stopColor={inverted ? 'white' : 'black'}
35
- stopOpacity={0}
36
- />
37
- </linearGradient>
38
- <linearGradient
39
- id={`gradient-${id}-2`}
40
- x1="11.7555"
41
- y1="4.8"
42
- x2="11.7376"
43
- y2="9.50002"
44
- gradientUnits="userSpaceOnUse"
45
- >
46
- <stop stopColor={inverted ? 'white' : 'black'} />
47
- <stop
48
- offset={1}
49
- stopColor={inverted ? 'white' : 'black'}
50
- stopOpacity={0}
51
- />
52
- </linearGradient>
53
- </defs>
54
- <path
55
- d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
56
- fill={inverted ? 'black' : 'white'}
57
- stroke={inverted ? 'black' : 'white'}
58
- strokeWidth={2}
59
- strokeLinecap="round"
60
- strokeLinejoin="round"
61
- />
62
- <mask
63
- id="mask0_91_2047"
64
- style={{ maskType: 'alpha' }}
65
- maskUnits="userSpaceOnUse"
66
- x={1}
67
- y={0}
68
- width={16}
69
- height={16}
70
- >
71
- <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
72
- </mask>
73
- <g mask="url(#mask0_91_2047)">
74
- <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
75
- <path
76
- d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
77
- fill={`url(#gradient-${id}-1)`}
78
- />
79
- <rect
80
- x="11.2222"
81
- y="4.8"
82
- width="1.06667"
83
- height="6.4"
84
- fill={`url(#gradient-${id}-2)`}
85
- />
86
- </g>
87
- </svg>
88
- );
89
  }
90
 
91
  function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92
- return (
93
- <svg
94
- fill="currentColor"
95
- viewBox="0 0 24 24"
96
- role="img"
97
- xmlns="http://www.w3.org/2000/svg"
98
- className={cn('size-4', className)}
99
- {...props}
100
- >
101
- <title>OpenAI icon</title>
102
- <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
103
- </svg>
104
- );
105
  }
106
 
107
  function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
108
- return (
109
- <svg
110
- aria-label="Vercel logomark"
111
- role="img"
112
- viewBox="0 0 74 64"
113
- className={cn('size-4', className)}
114
- {...props}
115
- >
116
- <path
117
- d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
118
- fill="currentColor"
119
- ></path>
120
- </svg>
121
- );
122
  }
123
 
124
  function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
125
- return (
126
- <svg
127
- role="img"
128
- viewBox="0 0 24 24"
129
- xmlns="http://www.w3.org/2000/svg"
130
- fill="currentColor"
131
- className={cn('size-4', className)}
132
- {...props}
133
- >
134
- <title>GitHub</title>
135
- <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
136
- </svg>
137
- );
138
  }
139
 
140
  function IconGoogle({ className, ...props }: React.ComponentProps<'svg'>) {
141
- return (
142
- <svg
143
- xmlns="http://www.w3.org/2000/svg"
144
- width="2443"
145
- height="2500"
146
- preserveAspectRatio="xMidYMid"
147
- viewBox="0 0 256 262"
148
- id="google"
149
- className={cn('size-4', className)}
150
- >
151
- <path
152
- fill="#4285F4"
153
- d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
154
- ></path>
155
- <path
156
- fill="#34A853"
157
- d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
158
- ></path>
159
- <path
160
- fill="#FBBC05"
161
- d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
162
- ></path>
163
- <path
164
- fill="#EB4335"
165
- d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
166
- ></path>
167
- </svg>
168
- );
169
  }
170
 
171
  function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
172
- return (
173
- <svg
174
- fill="none"
175
- shapeRendering="geometricPrecision"
176
- stroke="currentColor"
177
- strokeLinecap="round"
178
- strokeLinejoin="round"
179
- strokeWidth="1"
180
- viewBox="0 0 24 24"
181
- aria-hidden="true"
182
- className={cn('size-4', className)}
183
- {...props}
184
- >
185
- <path d="M16.88 3.549L7.12 20.451"></path>
186
- </svg>
187
- );
188
  }
189
 
190
  function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
191
- return (
192
- <svg
193
- xmlns="http://www.w3.org/2000/svg"
194
- viewBox="0 0 256 256"
195
- fill="currentColor"
196
- className={cn('size-4', className)}
197
- {...props}
198
- >
199
- <path d="m205.66 149.66-72 72a8 8 0 0 1-11.32 0l-72-72a8 8 0 0 1 11.32-11.32L120 196.69V40a8 8 0 0 1 16 0v156.69l58.34-58.35a8 8 0 0 1 11.32 11.32Z" />
200
- </svg>
201
- );
202
  }
203
 
204
  function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
205
- return (
206
- <svg
207
- xmlns="http://www.w3.org/2000/svg"
208
- viewBox="0 0 256 256"
209
- fill="currentColor"
210
- className={cn('size-4', className)}
211
- {...props}
212
- >
213
- <path d="m221.66 133.66-72 72a8 8 0 0 1-11.32-11.32L196.69 136H40a8 8 0 0 1 0-16h156.69l-58.35-58.34a8 8 0 0 1 11.32-11.32l72 72a8 8 0 0 1 0 11.32Z" />
214
- </svg>
215
- );
216
  }
217
 
218
  function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
219
- return (
220
- <svg
221
- xmlns="http://www.w3.org/2000/svg"
222
- viewBox="0 0 256 256"
223
- fill="currentColor"
224
- className={cn('size-4', className)}
225
- {...props}
226
- >
227
- <path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z" />
228
- </svg>
229
- );
230
  }
231
 
232
  function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
233
- return (
234
- <svg
235
- xmlns="http://www.w3.org/2000/svg"
236
- viewBox="0 0 256 256"
237
- fill="currentColor"
238
- className={cn('size-4', className)}
239
- {...props}
240
- >
241
- <path d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8Z" />
242
- </svg>
243
- );
244
  }
245
 
246
  function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
247
- return (
248
- <svg
249
- xmlns="http://www.w3.org/2000/svg"
250
- viewBox="0 0 256 256"
251
- fill="currentColor"
252
- className={cn('size-4', className)}
253
- {...props}
254
- >
255
- <path d="M200 32v144a8 8 0 0 1-8 8H67.31l34.35 34.34a8 8 0 0 1-11.32 11.32l-48-48a8 8 0 0 1 0-11.32l48-48a8 8 0 0 1 11.32 11.32L67.31 168H184V32a8 8 0 0 1 16 0Z" />
256
- </svg>
257
- );
258
  }
259
 
260
  function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
261
- return (
262
- <svg
263
- xmlns="http://www.w3.org/2000/svg"
264
- viewBox="0 0 256 256"
265
- fill="currentColor"
266
- className={cn('size-4 animate-spin', className)}
267
- {...props}
268
- >
269
- <path d="M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128Z" />
270
- </svg>
271
- );
272
  }
273
 
274
  function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
275
- return (
276
- <svg
277
- xmlns="http://www.w3.org/2000/svg"
278
- viewBox="0 0 256 256"
279
- fill="currentColor"
280
- className={cn('size-4', className)}
281
- {...props}
282
- >
283
- <path d="M216 48H40a16 16 0 0 0-16 16v160a15.84 15.84 0 0 0 9.25 14.5A16.05 16.05 0 0 0 40 240a15.89 15.89 0 0 0 10.25-3.78.69.69 0 0 0 .13-.11L82.5 208H216a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16ZM40 224Zm176-32H82.5a16 16 0 0 0-10.3 3.75l-.12.11L40 224V64h176Z" />
284
- </svg>
285
- );
286
  }
287
 
288
  function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
289
- return (
290
- <svg
291
- xmlns="http://www.w3.org/2000/svg"
292
- viewBox="0 0 256 256"
293
- fill="currentColor"
294
- className={cn('size-4', className)}
295
- {...props}
296
- >
297
- <path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16ZM96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Zm48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Z" />
298
- </svg>
299
- );
300
  }
301
 
302
  function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
303
- return (
304
- <svg
305
- xmlns="http://www.w3.org/2000/svg"
306
- viewBox="0 0 256 256"
307
- fill="currentColor"
308
- className={cn('size-4', className)}
309
- {...props}
310
- >
311
- <path d="M197.67 186.37a8 8 0 0 1 0 11.29C196.58 198.73 170.82 224 128 224c-37.39 0-64.53-22.4-80-39.85V208a8 8 0 0 1-16 0v-48a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16H55.44C67.76 183.35 93 208 128 208c36 0 58.14-21.46 58.36-21.68a8 8 0 0 1 11.31.05ZM216 40a8 8 0 0 0-8 8v23.85C192.53 54.4 165.39 32 128 32c-42.82 0-68.58 25.27-69.66 26.34a8 8 0 0 0 11.3 11.34C69.86 69.46 92 48 128 48c35 0 60.24 24.65 72.56 40H168a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8Z" />
312
- </svg>
313
- );
314
  }
315
 
316
  function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
317
- return (
318
- <svg
319
- xmlns="http://www.w3.org/2000/svg"
320
- viewBox="0 0 256 256"
321
- fill="currentColor"
322
- className={cn('size-4', className)}
323
- {...props}
324
- >
325
- <path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm0 192a88 88 0 1 1 88-88 88.1 88.1 0 0 1-88 88Zm24-120h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8Zm-8 48h-32v-32h32Z" />
326
- </svg>
327
- );
328
  }
329
 
330
  function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
331
- return (
332
- <svg
333
- xmlns="http://www.w3.org/2000/svg"
334
- viewBox="0 0 256 256"
335
- fill="currentColor"
336
- className={cn('size-4', className)}
337
- {...props}
338
- >
339
- <path d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16ZM40 56h40v144H40Zm176 144H96V56h120v144Z" />
340
- </svg>
341
- );
342
  }
343
 
344
  function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
345
- return (
346
- <svg
347
- xmlns="http://www.w3.org/2000/svg"
348
- viewBox="0 0 256 256"
349
- fill="currentColor"
350
- className={cn('size-4', className)}
351
- {...props}
352
- >
353
- <path d="M233.54 142.23a8 8 0 0 0-8-2 88.08 88.08 0 0 1-109.8-109.8 8 8 0 0 0-10-10 104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.09 103.09 0 0 0 62.52-20.88 104.84 104.84 0 0 0 37-52.91 8 8 0 0 0-1.98-7.98Zm-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104 106 106 0 0 0 14.92-1.06 89 89 0 0 1-26.02 31.4Z" />
354
- </svg>
355
- );
356
  }
357
 
358
  function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
359
- return (
360
- <svg
361
- xmlns="http://www.w3.org/2000/svg"
362
- viewBox="0 0 256 256"
363
- fill="currentColor"
364
- className={cn('size-4', className)}
365
- {...props}
366
- >
367
- <path d="M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0Zm72 88a64 64 0 1 1-64-64 64.07 64.07 0 0 1 64 64Zm-16 0a48 48 0 1 0-48 48 48.05 48.05 0 0 0 48-48ZM58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32ZM192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72Zm5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8Zm80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8Zm112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16Z" />
368
- </svg>
369
- );
370
  }
371
 
372
  function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
373
- return (
374
- <svg
375
- xmlns="http://www.w3.org/2000/svg"
376
- viewBox="0 0 256 256"
377
- fill="currentColor"
378
- className={cn('size-4', className)}
379
- {...props}
380
- >
381
- <path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
382
- </svg>
383
- );
384
  }
385
 
386
  function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
387
- return (
388
- <svg
389
- xmlns="http://www.w3.org/2000/svg"
390
- viewBox="0 0 256 256"
391
- fill="currentColor"
392
- className={cn('size-4', className)}
393
- {...props}
394
- >
395
- <path d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z" />
396
- </svg>
397
- );
398
  }
399
 
400
  function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
401
- return (
402
- <svg
403
- xmlns="http://www.w3.org/2000/svg"
404
- viewBox="0 0 256 256"
405
- fill="currentColor"
406
- className={cn('size-4', className)}
407
- {...props}
408
- >
409
- <path d="M224 152v56a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-56a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0Zm-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 132.69V40a8 8 0 0 0-16 0v92.69l-26.34-26.35a8 8 0 0 0-11.32 11.32Z" />
410
- </svg>
411
- );
412
  }
413
 
414
  function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
415
- return (
416
- <svg
417
- xmlns="http://www.w3.org/2000/svg"
418
- viewBox="0 0 256 256"
419
- fill="currentColor"
420
- className={cn('size-4', className)}
421
- {...props}
422
- >
423
- <path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128 50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z" />
424
- </svg>
425
- );
426
  }
427
 
428
  function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
429
- return (
430
- <svg
431
- xmlns="http://www.w3.org/2000/svg"
432
- fill="none"
433
- viewBox="0 0 24 24"
434
- strokeWidth={1.5}
435
- stroke="currentColor"
436
- className={cn('size-4', className)}
437
- {...props}
438
- >
439
- <path
440
- strokeLinecap="round"
441
- strokeLinejoin="round"
442
- d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
443
- />
444
- </svg>
445
- );
446
  }
447
 
448
  function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
449
- return (
450
- <svg
451
- xmlns="http://www.w3.org/2000/svg"
452
- fill="currentColor"
453
- className={cn('size-4', className)}
454
- viewBox="0 0 256 256"
455
- {...props}
456
- >
457
- <path d="m237.66 106.35-80-80A8 8 0 0 0 144 32v40.35c-25.94 2.22-54.59 14.92-78.16 34.91-28.38 24.08-46.05 55.11-49.76 87.37a12 12 0 0 0 20.68 9.58c11-11.71 50.14-48.74 107.24-52V192a8 8 0 0 0 13.66 5.65l80-80a8 8 0 0 0 0-11.3ZM160 172.69V144a8 8 0 0 0-8-8c-28.08 0-55.43 7.33-81.29 21.8a196.17 196.17 0 0 0-36.57 26.52c5.8-23.84 20.42-46.51 42.05-64.86C99.41 99.77 127.75 88 152 88a8 8 0 0 0 8-8V51.32L220.69 112Z" />
458
- </svg>
459
- );
460
  }
461
 
462
  function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
463
- return (
464
- <svg
465
- xmlns="http://www.w3.org/2000/svg"
466
- fill="currentColor"
467
- className={cn('size-4', className)}
468
- viewBox="0 0 256 256"
469
- {...props}
470
- >
471
- <path d="M117.25 157.92a60 60 0 1 0-66.5 0 95.83 95.83 0 0 0-47.22 37.71 8 8 0 1 0 13.4 8.74 80 80 0 0 1 134.14 0 8 8 0 0 0 13.4-8.74 95.83 95.83 0 0 0-47.22-37.71ZM40 108a44 44 0 1 1 44 44 44.05 44.05 0 0 1-44-44Zm210.14 98.7a8 8 0 0 1-11.07-2.33A79.83 79.83 0 0 0 172 168a8 8 0 0 1 0-16 44 44 0 1 0-16.34-84.87 8 8 0 1 1-5.94-14.85 60 60 0 0 1 55.53 105.64 95.83 95.83 0 0 1 47.22 37.71 8 8 0 0 1-2.33 11.07Z" />
472
- </svg>
473
- );
474
  }
475
 
476
  function IconExternalLink({
477
- className,
478
- ...props
479
  }: React.ComponentProps<'svg'>) {
480
- return (
481
- <svg
482
- xmlns="http://www.w3.org/2000/svg"
483
- fill="currentColor"
484
- className={cn('size-4', className)}
485
- viewBox="0 0 256 256"
486
- {...props}
487
- >
488
- <path d="M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8Z" />
489
- </svg>
490
- );
491
  }
492
 
493
  function IconChevronUpDown({
494
- className,
495
- ...props
496
  }: React.ComponentProps<'svg'>) {
497
- return (
498
- <svg
499
- xmlns="http://www.w3.org/2000/svg"
500
- fill="currentColor"
501
- className={cn('size-4', className)}
502
- viewBox="0 0 256 256"
503
- {...props}
504
- >
505
- <path d="M181.66 170.34a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-48-48a8 8 0 0 1 11.32-11.32L128 212.69l42.34-42.35a8 8 0 0 1 11.32 0Zm-96-84.68L128 43.31l42.34 42.35a8 8 0 0 0 11.32-11.32l-48-48a8 8 0 0 0-11.32 0l-48 48a8 8 0 0 0 11.32 11.32Z" />
506
- </svg>
507
- );
508
  }
509
 
510
  function IconLoading({ className, ...props }: React.ComponentProps<'svg'>) {
511
- return (
512
- <svg
513
- aria-hidden="true"
514
- className="size-8 text-gray-200 animate-spin dark:text-gray-600 fill-gray-600"
515
- viewBox="0 0 100 101"
516
- fill="none"
517
- xmlns="http://www.w3.org/2000/svg"
518
- >
519
- <path
520
- d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
521
- fill="currentColor"
522
- />
523
- <path
524
- d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
525
- fill="currentFill"
526
- />
527
- </svg>
528
- );
529
  }
530
 
531
  export {
532
- IconEdit,
533
- IconNextChat,
534
- IconOpenAI,
535
- IconVercel,
536
- IconGitHub,
537
- IconSeparator,
538
- IconArrowDown,
539
- IconArrowRight,
540
- IconUser,
541
- IconPlus,
542
- IconArrowElbow,
543
- IconSpinner,
544
- IconMessage,
545
- IconTrash,
546
- IconRefresh,
547
- IconStop,
548
- IconSidebar,
549
- IconMoon,
550
- IconSun,
551
- IconCopy,
552
- IconCheck,
553
- IconDownload,
554
- IconClose,
555
- IconShare,
556
- IconUsers,
557
- IconExternalLink,
558
- IconChevronUpDown,
559
- IconGoogle,
560
- IconLoading,
561
  };
 
5
  import { cn } from '@/lib/utils';
6
 
7
  function IconNextChat({
8
+ className,
9
+ inverted,
10
+ ...props
11
  }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12
+ const id = React.useId();
13
+
14
+ return (
15
+ <svg
16
+ viewBox="0 0 17 17"
17
+ fill="none"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ className={cn('size-4', className)}
20
+ {...props}
21
+ >
22
+ <defs>
23
+ <linearGradient
24
+ id={`gradient-${id}-1`}
25
+ x1="10.6889"
26
+ y1="10.3556"
27
+ x2="13.8445"
28
+ y2="14.2667"
29
+ gradientUnits="userSpaceOnUse"
30
+ >
31
+ <stop stopColor={inverted ? 'white' : 'black'} />
32
+ <stop
33
+ offset={1}
34
+ stopColor={inverted ? 'white' : 'black'}
35
+ stopOpacity={0}
36
+ />
37
+ </linearGradient>
38
+ <linearGradient
39
+ id={`gradient-${id}-2`}
40
+ x1="11.7555"
41
+ y1="4.8"
42
+ x2="11.7376"
43
+ y2="9.50002"
44
+ gradientUnits="userSpaceOnUse"
45
+ >
46
+ <stop stopColor={inverted ? 'white' : 'black'} />
47
+ <stop
48
+ offset={1}
49
+ stopColor={inverted ? 'white' : 'black'}
50
+ stopOpacity={0}
51
+ />
52
+ </linearGradient>
53
+ </defs>
54
+ <path
55
+ d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
56
+ fill={inverted ? 'black' : 'white'}
57
+ stroke={inverted ? 'black' : 'white'}
58
+ strokeWidth={2}
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ />
62
+ <mask
63
+ id="mask0_91_2047"
64
+ style={{ maskType: 'alpha' }}
65
+ maskUnits="userSpaceOnUse"
66
+ x={1}
67
+ y={0}
68
+ width={16}
69
+ height={16}
70
+ >
71
+ <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
72
+ </mask>
73
+ <g mask="url(#mask0_91_2047)">
74
+ <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
75
+ <path
76
+ d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
77
+ fill={`url(#gradient-${id}-1)`}
78
+ />
79
+ <rect
80
+ x="11.2222"
81
+ y="4.8"
82
+ width="1.06667"
83
+ height="6.4"
84
+ fill={`url(#gradient-${id}-2)`}
85
+ />
86
+ </g>
87
+ </svg>
88
+ );
89
  }
90
 
91
  function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92
+ return (
93
+ <svg
94
+ fill="currentColor"
95
+ viewBox="0 0 24 24"
96
+ role="img"
97
+ xmlns="http://www.w3.org/2000/svg"
98
+ className={cn('size-4', className)}
99
+ {...props}
100
+ >
101
+ <title>OpenAI icon</title>
102
+ <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
103
+ </svg>
104
+ );
105
  }
106
 
107
  function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
108
+ return (
109
+ <svg
110
+ aria-label="Vercel logomark"
111
+ role="img"
112
+ viewBox="0 0 74 64"
113
+ className={cn('size-4', className)}
114
+ {...props}
115
+ >
116
+ <path
117
+ d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
118
+ fill="currentColor"
119
+ ></path>
120
+ </svg>
121
+ );
122
  }
123
 
124
  function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
125
+ return (
126
+ <svg
127
+ role="img"
128
+ viewBox="0 0 24 24"
129
+ xmlns="http://www.w3.org/2000/svg"
130
+ fill="currentColor"
131
+ className={cn('size-4', className)}
132
+ {...props}
133
+ >
134
+ <title>GitHub</title>
135
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
136
+ </svg>
137
+ );
138
  }
139
 
140
  function IconGoogle({ className, ...props }: React.ComponentProps<'svg'>) {
141
+ return (
142
+ <svg
143
+ xmlns="http://www.w3.org/2000/svg"
144
+ width="2443"
145
+ height="2500"
146
+ preserveAspectRatio="xMidYMid"
147
+ viewBox="0 0 256 262"
148
+ id="google"
149
+ className={cn('size-4', className)}
150
+ >
151
+ <path
152
+ fill="#4285F4"
153
+ d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
154
+ ></path>
155
+ <path
156
+ fill="#34A853"
157
+ d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
158
+ ></path>
159
+ <path
160
+ fill="#FBBC05"
161
+ d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
162
+ ></path>
163
+ <path
164
+ fill="#EB4335"
165
+ d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
166
+ ></path>
167
+ </svg>
168
+ );
169
  }
170
 
171
  function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
172
+ return (
173
+ <svg
174
+ fill="none"
175
+ shapeRendering="geometricPrecision"
176
+ stroke="currentColor"
177
+ strokeLinecap="round"
178
+ strokeLinejoin="round"
179
+ strokeWidth="1"
180
+ viewBox="0 0 24 24"
181
+ aria-hidden="true"
182
+ className={cn('size-4', className)}
183
+ {...props}
184
+ >
185
+ <path d="M16.88 3.549L7.12 20.451"></path>
186
+ </svg>
187
+ );
188
  }
189
 
190
  function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
191
+ return (
192
+ <svg
193
+ xmlns="http://www.w3.org/2000/svg"
194
+ viewBox="0 0 256 256"
195
+ fill="currentColor"
196
+ className={cn('size-4', className)}
197
+ {...props}
198
+ >
199
+ <path d="m205.66 149.66-72 72a8 8 0 0 1-11.32 0l-72-72a8 8 0 0 1 11.32-11.32L120 196.69V40a8 8 0 0 1 16 0v156.69l58.34-58.35a8 8 0 0 1 11.32 11.32Z" />
200
+ </svg>
201
+ );
202
  }
203
 
204
  function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
205
+ return (
206
+ <svg
207
+ xmlns="http://www.w3.org/2000/svg"
208
+ viewBox="0 0 256 256"
209
+ fill="currentColor"
210
+ className={cn('size-4', className)}
211
+ {...props}
212
+ >
213
+ <path d="m221.66 133.66-72 72a8 8 0 0 1-11.32-11.32L196.69 136H40a8 8 0 0 1 0-16h156.69l-58.35-58.34a8 8 0 0 1 11.32-11.32l72 72a8 8 0 0 1 0 11.32Z" />
214
+ </svg>
215
+ );
216
  }
217
 
218
  function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
219
+ return (
220
+ <svg
221
+ xmlns="http://www.w3.org/2000/svg"
222
+ viewBox="0 0 256 256"
223
+ fill="currentColor"
224
+ className={cn('size-4', className)}
225
+ {...props}
226
+ >
227
+ <path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z" />
228
+ </svg>
229
+ );
230
  }
231
 
232
  function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
233
+ return (
234
+ <svg
235
+ xmlns="http://www.w3.org/2000/svg"
236
+ viewBox="0 0 256 256"
237
+ fill="currentColor"
238
+ className={cn('size-4', className)}
239
+ {...props}
240
+ >
241
+ <path d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8Z" />
242
+ </svg>
243
+ );
244
  }
245
 
246
  function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
247
+ return (
248
+ <svg
249
+ xmlns="http://www.w3.org/2000/svg"
250
+ viewBox="0 0 256 256"
251
+ fill="currentColor"
252
+ className={cn('size-4', className)}
253
+ {...props}
254
+ >
255
+ <path d="M200 32v144a8 8 0 0 1-8 8H67.31l34.35 34.34a8 8 0 0 1-11.32 11.32l-48-48a8 8 0 0 1 0-11.32l48-48a8 8 0 0 1 11.32 11.32L67.31 168H184V32a8 8 0 0 1 16 0Z" />
256
+ </svg>
257
+ );
258
  }
259
 
260
  function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
261
+ return (
262
+ <svg
263
+ xmlns="http://www.w3.org/2000/svg"
264
+ viewBox="0 0 256 256"
265
+ fill="currentColor"
266
+ className={cn('size-4 animate-spin', className)}
267
+ {...props}
268
+ >
269
+ <path d="M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128Z" />
270
+ </svg>
271
+ );
272
  }
273
 
274
  function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
275
+ return (
276
+ <svg
277
+ xmlns="http://www.w3.org/2000/svg"
278
+ viewBox="0 0 256 256"
279
+ fill="currentColor"
280
+ className={cn('size-4', className)}
281
+ {...props}
282
+ >
283
+ <path d="M216 48H40a16 16 0 0 0-16 16v160a15.84 15.84 0 0 0 9.25 14.5A16.05 16.05 0 0 0 40 240a15.89 15.89 0 0 0 10.25-3.78.69.69 0 0 0 .13-.11L82.5 208H216a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16ZM40 224Zm176-32H82.5a16 16 0 0 0-10.3 3.75l-.12.11L40 224V64h176Z" />
284
+ </svg>
285
+ );
286
  }
287
 
288
  function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
289
+ return (
290
+ <svg
291
+ xmlns="http://www.w3.org/2000/svg"
292
+ viewBox="0 0 256 256"
293
+ fill="currentColor"
294
+ className={cn('size-4', className)}
295
+ {...props}
296
+ >
297
+ <path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16ZM96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Zm48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Z" />
298
+ </svg>
299
+ );
300
  }
301
 
302
  function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
303
+ return (
304
+ <svg
305
+ xmlns="http://www.w3.org/2000/svg"
306
+ viewBox="0 0 256 256"
307
+ fill="currentColor"
308
+ className={cn('size-4', className)}
309
+ {...props}
310
+ >
311
+ <path d="M197.67 186.37a8 8 0 0 1 0 11.29C196.58 198.73 170.82 224 128 224c-37.39 0-64.53-22.4-80-39.85V208a8 8 0 0 1-16 0v-48a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16H55.44C67.76 183.35 93 208 128 208c36 0 58.14-21.46 58.36-21.68a8 8 0 0 1 11.31.05ZM216 40a8 8 0 0 0-8 8v23.85C192.53 54.4 165.39 32 128 32c-42.82 0-68.58 25.27-69.66 26.34a8 8 0 0 0 11.3 11.34C69.86 69.46 92 48 128 48c35 0 60.24 24.65 72.56 40H168a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8Z" />
312
+ </svg>
313
+ );
314
  }
315
 
316
  function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
317
+ return (
318
+ <svg
319
+ xmlns="http://www.w3.org/2000/svg"
320
+ viewBox="0 0 256 256"
321
+ fill="currentColor"
322
+ className={cn('size-4', className)}
323
+ {...props}
324
+ >
325
+ <path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm0 192a88 88 0 1 1 88-88 88.1 88.1 0 0 1-88 88Zm24-120h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8Zm-8 48h-32v-32h32Z" />
326
+ </svg>
327
+ );
328
  }
329
 
330
  function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
331
+ return (
332
+ <svg
333
+ xmlns="http://www.w3.org/2000/svg"
334
+ viewBox="0 0 256 256"
335
+ fill="currentColor"
336
+ className={cn('size-4', className)}
337
+ {...props}
338
+ >
339
+ <path d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16ZM40 56h40v144H40Zm176 144H96V56h120v144Z" />
340
+ </svg>
341
+ );
342
  }
343
 
344
  function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
345
+ return (
346
+ <svg
347
+ xmlns="http://www.w3.org/2000/svg"
348
+ viewBox="0 0 256 256"
349
+ fill="currentColor"
350
+ className={cn('size-4', className)}
351
+ {...props}
352
+ >
353
+ <path d="M233.54 142.23a8 8 0 0 0-8-2 88.08 88.08 0 0 1-109.8-109.8 8 8 0 0 0-10-10 104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.09 103.09 0 0 0 62.52-20.88 104.84 104.84 0 0 0 37-52.91 8 8 0 0 0-1.98-7.98Zm-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104 106 106 0 0 0 14.92-1.06 89 89 0 0 1-26.02 31.4Z" />
354
+ </svg>
355
+ );
356
  }
357
 
358
  function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
359
+ return (
360
+ <svg
361
+ xmlns="http://www.w3.org/2000/svg"
362
+ viewBox="0 0 256 256"
363
+ fill="currentColor"
364
+ className={cn('size-4', className)}
365
+ {...props}
366
+ >
367
+ <path d="M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0Zm72 88a64 64 0 1 1-64-64 64.07 64.07 0 0 1 64 64Zm-16 0a48 48 0 1 0-48 48 48.05 48.05 0 0 0 48-48ZM58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32ZM192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72Zm5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8Zm80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8Zm112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16Z" />
368
+ </svg>
369
+ );
370
  }
371
 
372
  function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
373
+ return (
374
+ <svg
375
+ xmlns="http://www.w3.org/2000/svg"
376
+ viewBox="0 0 256 256"
377
+ fill="currentColor"
378
+ className={cn('size-4', className)}
379
+ {...props}
380
+ >
381
+ <path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
382
+ </svg>
383
+ );
384
  }
385
 
386
  function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
387
+ return (
388
+ <svg
389
+ xmlns="http://www.w3.org/2000/svg"
390
+ viewBox="0 0 256 256"
391
+ fill="currentColor"
392
+ className={cn('size-4', className)}
393
+ {...props}
394
+ >
395
+ <path d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z" />
396
+ </svg>
397
+ );
398
  }
399
 
400
  function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
401
+ return (
402
+ <svg
403
+ xmlns="http://www.w3.org/2000/svg"
404
+ viewBox="0 0 256 256"
405
+ fill="currentColor"
406
+ className={cn('size-4', className)}
407
+ {...props}
408
+ >
409
+ <path d="M224 152v56a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-56a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0Zm-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 132.69V40a8 8 0 0 0-16 0v92.69l-26.34-26.35a8 8 0 0 0-11.32 11.32Z" />
410
+ </svg>
411
+ );
412
  }
413
 
414
  function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
415
+ return (
416
+ <svg
417
+ xmlns="http://www.w3.org/2000/svg"
418
+ viewBox="0 0 256 256"
419
+ fill="currentColor"
420
+ className={cn('size-4', className)}
421
+ {...props}
422
+ >
423
+ <path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128 50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z" />
424
+ </svg>
425
+ );
426
  }
427
 
428
  function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
429
+ return (
430
+ <svg
431
+ xmlns="http://www.w3.org/2000/svg"
432
+ fill="none"
433
+ viewBox="0 0 24 24"
434
+ strokeWidth={1.5}
435
+ stroke="currentColor"
436
+ className={cn('size-4', className)}
437
+ {...props}
438
+ >
439
+ <path
440
+ strokeLinecap="round"
441
+ strokeLinejoin="round"
442
+ d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
443
+ />
444
+ </svg>
445
+ );
446
  }
447
 
448
  function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
449
+ return (
450
+ <svg
451
+ xmlns="http://www.w3.org/2000/svg"
452
+ fill="currentColor"
453
+ className={cn('size-4', className)}
454
+ viewBox="0 0 256 256"
455
+ {...props}
456
+ >
457
+ <path d="m237.66 106.35-80-80A8 8 0 0 0 144 32v40.35c-25.94 2.22-54.59 14.92-78.16 34.91-28.38 24.08-46.05 55.11-49.76 87.37a12 12 0 0 0 20.68 9.58c11-11.71 50.14-48.74 107.24-52V192a8 8 0 0 0 13.66 5.65l80-80a8 8 0 0 0 0-11.3ZM160 172.69V144a8 8 0 0 0-8-8c-28.08 0-55.43 7.33-81.29 21.8a196.17 196.17 0 0 0-36.57 26.52c5.8-23.84 20.42-46.51 42.05-64.86C99.41 99.77 127.75 88 152 88a8 8 0 0 0 8-8V51.32L220.69 112Z" />
458
+ </svg>
459
+ );
460
  }
461
 
462
  function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
463
+ return (
464
+ <svg
465
+ xmlns="http://www.w3.org/2000/svg"
466
+ fill="currentColor"
467
+ className={cn('size-4', className)}
468
+ viewBox="0 0 256 256"
469
+ {...props}
470
+ >
471
+ <path d="M117.25 157.92a60 60 0 1 0-66.5 0 95.83 95.83 0 0 0-47.22 37.71 8 8 0 1 0 13.4 8.74 80 80 0 0 1 134.14 0 8 8 0 0 0 13.4-8.74 95.83 95.83 0 0 0-47.22-37.71ZM40 108a44 44 0 1 1 44 44 44.05 44.05 0 0 1-44-44Zm210.14 98.7a8 8 0 0 1-11.07-2.33A79.83 79.83 0 0 0 172 168a8 8 0 0 1 0-16 44 44 0 1 0-16.34-84.87 8 8 0 1 1-5.94-14.85 60 60 0 0 1 55.53 105.64 95.83 95.83 0 0 1 47.22 37.71 8 8 0 0 1-2.33 11.07Z" />
472
+ </svg>
473
+ );
474
  }
475
 
476
  function IconExternalLink({
477
+ className,
478
+ ...props
479
  }: React.ComponentProps<'svg'>) {
480
+ return (
481
+ <svg
482
+ xmlns="http://www.w3.org/2000/svg"
483
+ fill="currentColor"
484
+ className={cn('size-4', className)}
485
+ viewBox="0 0 256 256"
486
+ {...props}
487
+ >
488
+ <path d="M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8Z" />
489
+ </svg>
490
+ );
491
  }
492
 
493
  function IconChevronUpDown({
494
+ className,
495
+ ...props
496
  }: React.ComponentProps<'svg'>) {
497
+ return (
498
+ <svg
499
+ xmlns="http://www.w3.org/2000/svg"
500
+ fill="currentColor"
501
+ className={cn('size-4', className)}
502
+ viewBox="0 0 256 256"
503
+ {...props}
504
+ >
505
+ <path d="M181.66 170.34a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-48-48a8 8 0 0 1 11.32-11.32L128 212.69l42.34-42.35a8 8 0 0 1 11.32 0Zm-96-84.68L128 43.31l42.34 42.35a8 8 0 0 0 11.32-11.32l-48-48a8 8 0 0 0-11.32 0l-48 48a8 8 0 0 0 11.32 11.32Z" />
506
+ </svg>
507
+ );
508
  }
509
 
510
  function IconLoading({ className, ...props }: React.ComponentProps<'svg'>) {
511
+ return (
512
+ <svg
513
+ aria-hidden="true"
514
+ className="size-8 text-gray-200 animate-spin dark:text-gray-600 fill-gray-600"
515
+ viewBox="0 0 100 101"
516
+ fill="none"
517
+ xmlns="http://www.w3.org/2000/svg"
518
+ >
519
+ <path
520
+ d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
521
+ fill="currentColor"
522
+ />
523
+ <path
524
+ d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
525
+ fill="currentFill"
526
+ />
527
+ </svg>
528
+ );
529
  }
530
 
531
  export {
532
+ IconEdit,
533
+ IconNextChat,
534
+ IconOpenAI,
535
+ IconVercel,
536
+ IconGitHub,
537
+ IconSeparator,
538
+ IconArrowDown,
539
+ IconArrowRight,
540
+ IconUser,
541
+ IconPlus,
542
+ IconArrowElbow,
543
+ IconSpinner,
544
+ IconMessage,
545
+ IconTrash,
546
+ IconRefresh,
547
+ IconStop,
548
+ IconSidebar,
549
+ IconMoon,
550
+ IconSun,
551
+ IconCopy,
552
+ IconCheck,
553
+ IconDownload,
554
+ IconClose,
555
+ IconShare,
556
+ IconUsers,
557
+ IconExternalLink,
558
+ IconChevronUpDown,
559
+ IconGoogle,
560
+ IconLoading,
561
  };
components/ui/Input.tsx CHANGED
@@ -3,22 +3,22 @@ import * as React from 'react';
3
  import { cn } from '@/lib/utils';
4
 
5
  export interface InputProps
6
- extends React.InputHTMLAttributes<HTMLInputElement> {}
7
 
8
  const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
- ({ className, type, ...props }, ref) => {
10
- return (
11
- <input
12
- type={type}
13
- className={cn(
14
- 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
15
- className,
16
- )}
17
- ref={ref}
18
- {...props}
19
- />
20
- );
21
- },
22
  );
23
  Input.displayName = 'Input';
24
 
 
3
  import { cn } from '@/lib/utils';
4
 
5
  export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
 
8
  const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
15
+ className,
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ );
21
+ },
22
  );
23
  Input.displayName = 'Input';
24
 
components/ui/Loading.tsx CHANGED
@@ -1,9 +1,9 @@
1
  import { IconLoading } from '@/components/ui/Icons';
2
 
3
  export default function Loading() {
4
- return (
5
- <div className="flex justify-center items-center size-full text-sm">
6
- <IconLoading />
7
- </div>
8
- );
9
  }
 
1
  import { IconLoading } from '@/components/ui/Icons';
2
 
3
  export default function Loading() {
4
+ return (
5
+ <div className="flex justify-center items-center size-full text-sm">
6
+ <IconLoading />
7
+ </div>
8
+ );
9
  }
components/ui/Separator.tsx CHANGED
@@ -6,25 +6,25 @@ import * as SeparatorPrimitive from '@radix-ui/react-separator';
6
  import { cn } from '@/lib/utils';
7
 
8
  const Separator = React.forwardRef<
9
- React.ElementRef<typeof SeparatorPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
  >(
12
- (
13
- { className, orientation = 'horizontal', decorative = true, ...props },
14
- ref,
15
- ) => (
16
- <SeparatorPrimitive.Root
17
- ref={ref}
18
- decorative={decorative}
19
- orientation={orientation}
20
- className={cn(
21
- 'shrink-0 bg-border',
22
- orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
23
- className,
24
- )}
25
- {...props}
26
- />
27
- ),
28
  );
29
  Separator.displayName = SeparatorPrimitive.Root.displayName;
30
 
 
6
  import { cn } from '@/lib/utils';
7
 
8
  const Separator = React.forwardRef<
9
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
  >(
12
+ (
13
+ { className, orientation = 'horizontal', decorative = true, ...props },
14
+ ref,
15
+ ) => (
16
+ <SeparatorPrimitive.Root
17
+ ref={ref}
18
+ decorative={decorative}
19
+ orientation={orientation}
20
+ className={cn(
21
+ 'shrink-0 bg-border',
22
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ ),
28
  );
29
  Separator.displayName = SeparatorPrimitive.Root.displayName;
30
 
components/ui/Textarea.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import * as React from 'react'
2
 
3
- import { cn } from '@/lib/utils'
4
 
5
  export interface TextareaProps
6
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
@@ -11,14 +11,14 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
11
  <textarea
12
  className={cn(
13
  'flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
14
- className
15
  )}
16
  ref={ref}
17
  {...props}
18
  />
19
- )
20
- }
21
- )
22
- Textarea.displayName = 'Textarea'
23
 
24
- export { Textarea }
 
1
+ import * as React from 'react';
2
 
3
+ import { cn } from '@/lib/utils';
4
 
5
  export interface TextareaProps
6
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
 
11
  <textarea
12
  className={cn(
13
  'flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
14
+ className,
15
  )}
16
  ref={ref}
17
  {...props}
18
  />
19
+ );
20
+ },
21
+ );
22
+ Textarea.displayName = 'Textarea';
23
 
24
+ export { Textarea };
components/ui/Tooltip.tsx CHANGED
@@ -12,18 +12,18 @@ const Tooltip = TooltipPrimitive.Root;
12
  const TooltipTrigger = TooltipPrimitive.Trigger;
13
 
14
  const TooltipContent = React.forwardRef<
15
- React.ElementRef<typeof TooltipPrimitive.Content>,
16
- React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
  >(({ className, sideOffset = 4, ...props }, ref) => (
18
- <TooltipPrimitive.Content
19
- ref={ref}
20
- sideOffset={sideOffset}
21
- className={cn(
22
- 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
23
- className,
24
- )}
25
- {...props}
26
- />
27
  ));
28
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29
 
 
12
  const TooltipTrigger = TooltipPrimitive.Trigger;
13
 
14
  const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
  >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Content
19
+ ref={ref}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
  ));
28
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29
 
lib/fetch/index.ts CHANGED
@@ -2,90 +2,90 @@ import { auth } from '@/auth';
2
  import { v5 as uuidV5 } from 'uuid';
3
 
4
  interface ApiResponse<T> {
5
- code: number; // code from server 0
6
- message: string;
7
- data: T;
8
  }
9
 
10
  const clefApiBuilder = <Params extends object | void, Resp>(
11
- path: string,
12
- options?: {
13
- // default to GET
14
- method: 'GET' | 'POST';
15
- // default to 'app'
16
- prefix: 'api' | 'app' | 'api.dev' | 'app.dev';
17
- },
18
  ) => {
19
- return async (params: Params): Promise<Resp> => {
20
- const session = await auth();
21
- const email = session?.user?.email;
22
 
23
- if (!email || !email.endsWith('@landing.ai')) {
24
- throw new Response('Unauthorized', {
25
- status: 401,
26
- });
27
- }
28
 
29
- const sessionUser = {
30
- id: uuidV5(email, uuidV5.URL),
31
- orgId: '-1024',
32
- email: email,
33
- username: email.split('@')[0],
34
- userRole: 'adminPortal',
35
- bucket: 'fake_bucket',
36
- };
37
 
38
- const prefix = options?.prefix ?? 'app';
39
- const baseURL = `https://${prefix}.landing.ai/${path}`;
40
 
41
- const method = options?.method ?? 'GET';
42
- // Create a URL object with query params
43
- const url = new URL(baseURL);
44
- if (method === 'GET') {
45
- Object.entries(params ?? {}).forEach(([key, value]) =>
46
- url.searchParams.append(key, value),
47
- );
48
- }
49
 
50
- const fetchParams: RequestInit = {
51
- method: options?.method ?? 'GET',
52
- headers: {
53
- sessionuser: JSON.stringify(sessionUser), // faked production user
54
- apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k', // dev key
55
- 'X-ROUTE': 'mingruizhang-landing',
56
- },
57
- };
58
 
59
- if (method === 'POST' && params instanceof Object) {
60
- const formData = new FormData();
61
- for (const [key, value] of Object.entries(params)) {
62
- formData.append(key, value);
63
- }
64
 
65
- fetchParams.body = formData;
66
- }
67
 
68
- const res = await fetch(url.toString(), fetchParams);
69
 
70
- if (!res.ok) {
71
- console.error('ERROR: fetch data failure', res.status, res.statusText);
72
- // This will activate the closest `error.js` Error Boundary
73
- throw new Error('Failed to fetch data');
74
- }
75
- const { data }: ApiResponse<Resp> = await res.json();
76
- return data;
77
- };
78
  };
79
 
80
  export type ProjectBaseInfo = {
81
- id: number;
82
- name: string;
83
- created_at: Date;
84
- label_type: string;
85
- organization: {
86
- id: number;
87
- name: string;
88
- };
89
  };
90
  /**
91
  * Fetch recent projects from all organizations from past 30 days, excluding
@@ -95,22 +95,22 @@ export type ProjectBaseInfo = {
95
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
96
  */
97
  export const fetchRecentProjectList = clefApiBuilder<void, ProjectBaseInfo[]>(
98
- 'api/admin/projects/recent',
99
  );
100
 
101
  export type MediaDetails = {
102
- id: number;
103
- name: string;
104
- path: string;
105
- url: string;
106
- projectId: number;
107
- thumbnails: string[];
108
- properties: {
109
- width: number;
110
- height: number;
111
- format: string;
112
- orientation: number;
113
- };
114
  };
115
 
116
  /**
@@ -118,6 +118,6 @@ export type MediaDetails = {
118
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
119
  */
120
  export const fetchProjectMedia = clefApiBuilder<
121
- { projectId: number },
122
- MediaDetails[]
123
  >('api/admin/project/media');
 
2
  import { v5 as uuidV5 } from 'uuid';
3
 
4
  interface ApiResponse<T> {
5
+ code: number; // code from server 0
6
+ message: string;
7
+ data: T;
8
  }
9
 
10
  const clefApiBuilder = <Params extends object | void, Resp>(
11
+ path: string,
12
+ options?: {
13
+ // default to GET
14
+ method: 'GET' | 'POST';
15
+ // default to 'app'
16
+ prefix: 'api' | 'app' | 'api.dev' | 'app.dev';
17
+ },
18
  ) => {
19
+ return async (params: Params): Promise<Resp> => {
20
+ const session = await auth();
21
+ const email = session?.user?.email;
22
 
23
+ if (!email || !email.endsWith('@landing.ai')) {
24
+ throw new Response('Unauthorized', {
25
+ status: 401,
26
+ });
27
+ }
28
 
29
+ const sessionUser = {
30
+ id: uuidV5(email, uuidV5.URL),
31
+ orgId: '-1024',
32
+ email: email,
33
+ username: email.split('@')[0],
34
+ userRole: 'adminPortal',
35
+ bucket: 'fake_bucket',
36
+ };
37
 
38
+ const prefix = options?.prefix ?? 'app';
39
+ const baseURL = `https://${prefix}.landing.ai/${path}`;
40
 
41
+ const method = options?.method ?? 'GET';
42
+ // Create a URL object with query params
43
+ const url = new URL(baseURL);
44
+ if (method === 'GET') {
45
+ Object.entries(params ?? {}).forEach(([key, value]) =>
46
+ url.searchParams.append(key, value),
47
+ );
48
+ }
49
 
50
+ const fetchParams: RequestInit = {
51
+ method: options?.method ?? 'GET',
52
+ headers: {
53
+ sessionuser: JSON.stringify(sessionUser), // faked production user
54
+ apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k', // dev key
55
+ 'X-ROUTE': 'mingruizhang-landing',
56
+ },
57
+ };
58
 
59
+ if (method === 'POST' && params instanceof Object) {
60
+ const formData = new FormData();
61
+ for (const [key, value] of Object.entries(params)) {
62
+ formData.append(key, value);
63
+ }
64
 
65
+ fetchParams.body = formData;
66
+ }
67
 
68
+ const res = await fetch(url.toString(), fetchParams);
69
 
70
+ if (!res.ok) {
71
+ console.error('ERROR: fetch data failure', res.status, res.statusText);
72
+ // This will activate the closest `error.js` Error Boundary
73
+ throw new Error('Failed to fetch data');
74
+ }
75
+ const { data }: ApiResponse<Resp> = await res.json();
76
+ return data;
77
+ };
78
  };
79
 
80
  export type ProjectBaseInfo = {
81
+ id: number;
82
+ name: string;
83
+ created_at: Date;
84
+ label_type: string;
85
+ organization: {
86
+ id: number;
87
+ name: string;
88
+ };
89
  };
90
  /**
91
  * Fetch recent projects from all organizations from past 30 days, excluding
 
95
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
96
  */
97
  export const fetchRecentProjectList = clefApiBuilder<void, ProjectBaseInfo[]>(
98
+ 'api/admin/projects/recent',
99
  );
100
 
101
  export type MediaDetails = {
102
+ id: number;
103
+ name: string;
104
+ path: string;
105
+ url: string;
106
+ projectId: number;
107
+ thumbnails: string[];
108
+ properties: {
109
+ width: number;
110
+ height: number;
111
+ format: string;
112
+ orientation: number;
113
+ };
114
  };
115
 
116
  /**
 
118
  * @author https://github.com/landing-ai/landing-platform/blob/mingrui-04-08-meaningful-project/packages/server-clef/src/main_app/controllers/admin/get_admin_meaningful_project_controller.ts
119
  */
120
  export const fetchProjectMedia = clefApiBuilder<
121
+ { projectId: number },
122
+ MediaDetails[]
123
  >('api/admin/project/media');