Spaces:
Sleeping
Sleeping
MingruiZhang
commited on
Commit
•
f80b091
1
Parent(s):
c3e8f3d
done (#4)
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- app/(logout)/sign-in/page.tsx +11 -11
- app/(logout)/unauthorized/page.tsx +16 -16
- app/api/auth/[...nextauth]/route.ts +2 -2
- app/api/chat/route.ts +46 -46
- app/api/upload/route.ts +21 -21
- app/chat/layout.tsx +18 -18
- app/chat/page.tsx +2 -2
- app/globals.css +80 -80
- app/layout.tsx +41 -41
- app/page.tsx +10 -10
- app/project/[projectId]/page.tsx +17 -17
- app/project/layout.tsx +18 -18
- app/project/page.tsx +6 -6
- auth.ts +43 -43
- components/Header.tsx +16 -16
- components/LoginButton.tsx +23 -23
- components/Providers.tsx +5 -5
- components/TailwindIndicator.tsx +11 -11
- components/ThemeToggle.tsx +21 -21
- components/UserMenu.tsx +49 -49
- components/chat-sidebar/ChatCard.tsx +16 -16
- components/chat-sidebar/ChatListSidebar.tsx +9 -9
- components/chat/ButtonScrollToBottom.tsx +22 -22
- components/chat/ChatList.tsx +15 -15
- components/chat/ChatMessage.tsx +55 -55
- components/chat/ChatMessageActions.tsx +23 -23
- components/chat/ChatPanel.tsx +59 -59
- components/chat/ChatScrollAnchor.tsx +15 -15
- components/chat/EmptyScreen.tsx +41 -41
- components/chat/ImageList.tsx +53 -53
- components/chat/ImageSelector.tsx +43 -43
- components/chat/MemoizedReactMarkdown.tsx +4 -4
- components/chat/PromptForm.tsx +61 -61
- components/chat/index.tsx +29 -29
- components/project-sidebar/ProjectCard.tsx +25 -25
- components/project-sidebar/ProjectListSideBar.tsx +8 -8
- components/project/Chat.tsx +18 -18
- components/project/MediaGrid.tsx +11 -11
- components/project/MediaTile.tsx +31 -31
- components/ui/Button.tsx +40 -40
- components/ui/Chip.tsx +19 -19
- components/ui/CodeBlock.tsx +113 -113
- components/ui/DropdownMenu.tsx +77 -77
- components/ui/Icons.tsx +463 -463
- components/ui/Input.tsx +14 -14
- components/ui/Loading.tsx +5 -5
- components/ui/Separator.tsx +18 -18
- components/ui/Textarea.tsx +8 -8
- components/ui/Tooltip.tsx +11 -11
- 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 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
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 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
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 |
-
|
7 |
-
|
8 |
-
|
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 |
-
|
17 |
});
|
18 |
|
19 |
export async function POST(req: Request) {
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
|
65 |
-
|
66 |
|
67 |
-
|
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 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
|
16 |
-
|
17 |
-
|
18 |
|
19 |
-
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
|
31 |
-
|
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 |
-
|
7 |
}
|
8 |
|
9 |
export default async function Layout({ children }: ChatLayoutProps) {
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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 |
-
|
6 |
-
|
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 |
-
|
7 |
-
|
8 |
-
|
9 |
|
10 |
-
|
11 |
-
|
12 |
|
13 |
-
|
14 |
-
|
15 |
|
16 |
-
|
17 |
-
|
18 |
|
19 |
-
|
20 |
-
|
21 |
|
22 |
-
|
23 |
-
|
24 |
|
25 |
-
|
26 |
-
|
27 |
|
28 |
-
|
29 |
-
|
30 |
|
31 |
-
|
32 |
-
|
33 |
|
34 |
-
|
35 |
|
36 |
-
|
37 |
-
|
38 |
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
|
43 |
-
|
44 |
-
|
45 |
|
46 |
-
|
47 |
-
|
48 |
|
49 |
-
|
50 |
-
|
51 |
|
52 |
-
|
53 |
-
|
54 |
|
55 |
-
|
56 |
-
|
57 |
|
58 |
-
|
59 |
-
|
60 |
|
61 |
-
|
62 |
-
|
63 |
|
64 |
-
|
65 |
-
|
66 |
|
67 |
-
|
68 |
-
|
69 |
}
|
70 |
|
71 |
@layer base {
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
}
|
79 |
|
80 |
@layer components {
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
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 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
};
|
25 |
|
26 |
export const viewport = {
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
};
|
32 |
|
33 |
interface RootLayoutProps {
|
34 |
-
|
35 |
}
|
36 |
|
37 |
export default function RootLayout({ children }: RootLayoutProps) {
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
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 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
|
10 |
-
|
11 |
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
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 |
-
|
9 |
-
|
10 |
-
|
11 |
}
|
12 |
|
13 |
export default async function Page({ params }: PageProps) {
|
14 |
-
|
15 |
|
16 |
-
|
17 |
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
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 |
-
|
7 |
}
|
8 |
|
9 |
export default async function Layout({ children }: ChatLayoutProps) {
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
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 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
}
|
13 |
|
14 |
export const {
|
15 |
-
|
16 |
-
|
17 |
} = NextAuth({
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
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 |
-
|
11 |
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
<Link href="/project">Projects</Link>
|
20 |
</Button> */}
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
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 |
-
|
12 |
}
|
13 |
|
14 |
export function LoginButton({ oauth, ...props }: LoginButtonProps) {
|
15 |
-
|
16 |
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
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 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
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 |
-
|
3 |
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
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 |
-
|
11 |
-
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
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 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
} from '@/components/ui/DropdownMenu';
|
15 |
import { IconExternalLink } from '@/components/ui/Icons';
|
16 |
|
17 |
export interface UserMenuProps {
|
18 |
-
|
19 |
}
|
20 |
|
21 |
function getUserInitials(name: string) {
|
22 |
-
|
23 |
-
|
24 |
}
|
25 |
|
26 |
export function UserMenu({ user }: UserMenuProps) {
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
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 |
-
|
9 |
-
|
10 |
}
|
11 |
|
12 |
const ChatCard: React.FC<ChatCardProps> = ({ id, title }) => {
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
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 |
-
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
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 |
-
|
9 |
}
|
10 |
|
11 |
export function ChatList({ messages }: ChatList) {
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
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 |
-
|
16 |
}
|
17 |
|
18 |
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
|
47 |
-
|
48 |
-
|
49 |
|
50 |
-
|
51 |
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
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 |
-
|
13 |
}
|
14 |
|
15 |
export function ChatMessageActions({
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
}: ChatMessageActionsProps) {
|
20 |
-
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
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 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
}
|
19 |
|
20 |
export function ChatPanel({
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
}: ChatPanelProps) {
|
31 |
-
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
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 |
-
|
10 |
}
|
11 |
|
12 |
export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
|
28 |
-
|
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 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
];
|
13 |
|
14 |
export function EmptyScreen() {
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
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 |
-
|
12 |
-
|
13 |
-
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
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 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
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 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
];
|
14 |
|
15 |
const ImageSelector: React.FC<ImageSelectorProps> = () => {
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
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 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
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 |
-
|
9 |
-
|
10 |
-
|
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 |
-
|
17 |
-
|
18 |
-
|
19 |
}
|
20 |
|
21 |
export function PromptForm({
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
}: PromptProps) {
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
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 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
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 |
-
|
14 |
}
|
15 |
|
16 |
export function Chat({ id, className }: ChatProps) {
|
17 |
-
|
18 |
-
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
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 |
-
|
12 |
}
|
13 |
|
14 |
const ProjectCard: React.FC<ProjectCardProps> = ({ projectInfo }) => {
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
|
23 |
-
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
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 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
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 |
-
|
12 |
}
|
13 |
|
14 |
const Chat: React.FC<ChatProps> = ({ mediaList }) => {
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
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 |
-
|
6 |
}: {
|
7 |
-
|
8 |
}) {
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
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 |
-
|
5 |
-
|
6 |
-
|
7 |
} from '@/components/ui/Tooltip';
|
8 |
import { MediaDetails } from '@/lib/fetch';
|
9 |
|
10 |
export interface MediaTileProps {
|
11 |
-
|
12 |
}
|
13 |
|
14 |
export default function MediaTile({ media }: MediaTileProps) {
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
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 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
);
|
36 |
|
37 |
export interface ButtonProps
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
}
|
42 |
|
43 |
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
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 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
}
|
9 |
|
10 |
const Chip: React.FC<ChipProps> = ({
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
}) => {
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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 |
-
|
16 |
-
|
17 |
}
|
18 |
|
19 |
interface languageMap {
|
20 |
-
|
21 |
}
|
22 |
|
23 |
export const programmingLanguages: languageMap = {
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
};
|
49 |
|
50 |
export const generateRandomString = (length: number, lowercase = false) => {
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
};
|
58 |
|
59 |
const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
60 |
-
|
61 |
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
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 |
-
|
22 |
-
|
23 |
>(({ className, ...props }, ref) => (
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
));
|
33 |
DropdownMenuSubContent.displayName =
|
34 |
-
|
35 |
|
36 |
const DropdownMenuContent = React.forwardRef<
|
37 |
-
|
38 |
-
|
39 |
>(({ className, sideOffset = 4, ...props }, ref) => (
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
));
|
52 |
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
53 |
|
54 |
const DropdownMenuItem = React.forwardRef<
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
>(({ className, inset, ...props }, ref) => (
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
));
|
70 |
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
71 |
|
72 |
const DropdownMenuLabel = React.forwardRef<
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
>(({ className, inset, ...props }, ref) => (
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
));
|
88 |
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
89 |
|
90 |
const DropdownMenuSeparator = React.forwardRef<
|
91 |
-
|
92 |
-
|
93 |
>(({ className, ...props }, ref) => (
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
));
|
100 |
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
101 |
|
102 |
const DropdownMenuShortcut = ({
|
103 |
-
|
104 |
-
|
105 |
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
};
|
113 |
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
114 |
|
115 |
export {
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
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 |
-
|
9 |
-
|
10 |
-
|
11 |
}: React.ComponentProps<'svg'> & { inverted?: boolean }) {
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
}
|
90 |
|
91 |
function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
}
|
106 |
|
107 |
function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
}
|
123 |
|
124 |
function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
}
|
139 |
|
140 |
function IconGoogle({ className, ...props }: React.ComponentProps<'svg'>) {
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
}
|
170 |
|
171 |
function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
}
|
189 |
|
190 |
function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
}
|
203 |
|
204 |
function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
}
|
217 |
|
218 |
function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
}
|
231 |
|
232 |
function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
}
|
245 |
|
246 |
function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
}
|
259 |
|
260 |
function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
}
|
273 |
|
274 |
function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
}
|
287 |
|
288 |
function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
}
|
301 |
|
302 |
function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
}
|
315 |
|
316 |
function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
}
|
329 |
|
330 |
function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
}
|
343 |
|
344 |
function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
}
|
357 |
|
358 |
function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
}
|
371 |
|
372 |
function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
}
|
385 |
|
386 |
function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
}
|
399 |
|
400 |
function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
}
|
413 |
|
414 |
function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
}
|
427 |
|
428 |
function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
}
|
447 |
|
448 |
function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
}
|
461 |
|
462 |
function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
}
|
475 |
|
476 |
function IconExternalLink({
|
477 |
-
|
478 |
-
|
479 |
}: React.ComponentProps<'svg'>) {
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
}
|
492 |
|
493 |
function IconChevronUpDown({
|
494 |
-
|
495 |
-
|
496 |
}: React.ComponentProps<'svg'>) {
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
}
|
509 |
|
510 |
function IconLoading({ className, ...props }: React.ComponentProps<'svg'>) {
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
}
|
530 |
|
531 |
export {
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
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 |
-
|
7 |
|
8 |
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
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 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
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 |
-
|
10 |
-
|
11 |
>(
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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 |
-
|
16 |
-
|
17 |
>(({ className, sideOffset = 4, ...props }, ref) => (
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
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 |
-
|
6 |
-
|
7 |
-
|
8 |
}
|
9 |
|
10 |
const clefApiBuilder = <Params extends object | void, Resp>(
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
) => {
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
|
38 |
-
|
39 |
-
|
40 |
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
|
65 |
-
|
66 |
-
|
67 |
|
68 |
-
|
69 |
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
};
|
79 |
|
80 |
export type ProjectBaseInfo = {
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
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 |
-
|
99 |
);
|
100 |
|
101 |
export type MediaDetails = {
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
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 |
-
|
122 |
-
|
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');
|