wuyiqun0718 commited on
Commit
478d5b9
1 Parent(s): a10c720

feat: clone vision agent ui

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .eslintrc.json +26 -0
  2. .gitignore +41 -0
  3. .prettierrc +34 -0
  4. .vscode.settings.json +3 -0
  5. Dockerfile +41 -0
  6. app/(logout)/sign-in/page.tsx +18 -0
  7. app/(logout)/unauthorized/page.tsx +24 -0
  8. app/api/auth/[...nextauth]/route.ts +2 -0
  9. app/api/chat/route.ts +57 -0
  10. app/api/sign/route.ts +42 -0
  11. app/api/upload/route.ts +51 -0
  12. app/api/vision-agent/route.ts +74 -0
  13. app/chat/[id]/page.tsx +14 -0
  14. app/chat/layout.tsx +29 -0
  15. app/chat/page.tsx +118 -0
  16. app/globals.css +159 -0
  17. app/layout.tsx +63 -0
  18. app/page.tsx +12 -0
  19. app/project/[projectId]/page.tsx +33 -0
  20. app/project/layout.tsx +35 -0
  21. app/project/page.tsx +8 -0
  22. auth.ts +66 -0
  23. components/Header.tsx +45 -0
  24. components/LoginButton.tsx +38 -0
  25. components/LoginMenu.tsx +33 -0
  26. components/Providers.tsx +18 -0
  27. components/TailwindIndicator.tsx +14 -0
  28. components/ThemeToggle.tsx +32 -0
  29. components/UserMenu.tsx +68 -0
  30. components/chat-sidebar/ChatAdminToggle.tsx +14 -0
  31. components/chat-sidebar/ChatCard.tsx +63 -0
  32. components/chat-sidebar/ChatListSidebar.tsx +27 -0
  33. components/chat/ChatList.tsx +26 -0
  34. components/chat/ChatMessage.tsx +151 -0
  35. components/chat/ChatMessageActions.tsx +41 -0
  36. components/chat/Composer.tsx +197 -0
  37. components/chat/ImageSelector.tsx +91 -0
  38. components/chat/MemoizedReactMarkdown.tsx +9 -0
  39. components/chat/index.tsx +46 -0
  40. components/project-sidebar/ProjectCard.tsx +64 -0
  41. components/project-sidebar/ProjectListSideBar.tsx +17 -0
  42. components/project/ClassBar.tsx +22 -0
  43. components/project/MediaGrid.tsx +18 -0
  44. components/project/MediaTile.tsx +53 -0
  45. components/project/ProjectChat.tsx +59 -0
  46. components/ui/Button.tsx +57 -0
  47. components/ui/Chip.tsx +30 -0
  48. components/ui/CodeBlock.tsx +148 -0
  49. components/ui/DropdownMenu.tsx +128 -0
  50. components/ui/Icons.tsx +585 -0
.eslintrc.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json.schemastore.org/eslintrc",
3
+ "root": true,
4
+ "extends": [
5
+ "next/core-web-vitals",
6
+ "prettier",
7
+ "plugin:tailwindcss/recommended"
8
+ ],
9
+ "plugins": ["tailwindcss"],
10
+ "rules": {
11
+ "tailwindcss/no-custom-classname": "off",
12
+ "tailwindcss/classnames-order": "off"
13
+ },
14
+ "settings": {
15
+ "tailwindcss": {
16
+ "callees": ["cn", "cva"],
17
+ "config": "tailwind.config.js"
18
+ }
19
+ },
20
+ "overrides": [
21
+ {
22
+ "files": ["*.ts", "*.tsx"],
23
+ "parser": "@typescript-eslint/parser"
24
+ }
25
+ ]
26
+ }
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ node_modules
5
+ .pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ coverage
10
+
11
+ # next.js
12
+ .next/
13
+ out/
14
+ build
15
+
16
+ # misc
17
+ .DS_Store
18
+ *.pem
19
+
20
+ # debug
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ .pnpm-debug.log*
25
+
26
+ # local env files
27
+ .env.local
28
+ .env.development.local
29
+ .env.test.local
30
+ .env.production.local
31
+
32
+ # turbo
33
+ .turbo
34
+
35
+ .env
36
+ .vercel
37
+ .vscode
38
+ .env*.local
39
+
40
+ # ts
41
+ tsconfig.tsbuildinfo
.prettierrc ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "endOfLine": "lf",
3
+ "semi": true,
4
+ "singleQuote": true,
5
+ "arrowParens": "avoid",
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "trailingComma": "all",
9
+ "bracketSpacing": true,
10
+ "importOrder": [
11
+ "^(react/(.*)$)|^(react$)",
12
+ "^(next/(.*)$)|^(next$)",
13
+ "<THIRD_PARTY_MODULES>",
14
+ "",
15
+ "^types$",
16
+ "^@/types/(.*)$",
17
+ "^@/config/(.*)$",
18
+ "^@/lib/(.*)$",
19
+ "^@/hooks/(.*)$",
20
+ "^@/components/ui/(.*)$",
21
+ "^@/components/(.*)$",
22
+ "^@/registry/(.*)$",
23
+ "^@/styles/(.*)$",
24
+ "^@/app/(.*)$",
25
+ "",
26
+ "^[./]"
27
+ ],
28
+ "importOrderSeparation": false,
29
+ "importOrderSortSpecifiers": true,
30
+ "importOrderBuiltinModulesToTop": true,
31
+ "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"],
32
+ "importOrderMergeDuplicateImports": true,
33
+ "importOrderCombineTypeAndValueImports": true
34
+ }
.vscode.settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "prettier.prettierPath": "./node_modules/prettier/index.cjs"
3
+ }
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20
2
+ ENV PNPM_HOME="/pnpm"
3
+ ENV PATH="$PNPM_HOME:$PATH"
4
+ RUN corepack enable
5
+ WORKDIR /app
6
+ COPY package.json pnpm-lock.yaml ./
7
+ RUN pnpm i --frozen-lockfile
8
+
9
+ # Rebuild the source code only when needed
10
+ FROM base AS builder
11
+ WORKDIR /app
12
+ COPY --from=deps --link /app/node_modules ./node_modules
13
+ COPY --link . .
14
+
15
+ RUN pnpm run build
16
+
17
+ # Production image, copy all the files and run next
18
+ FROM base AS runner
19
+ WORKDIR /app
20
+
21
+ ENV NODE_ENV production
22
+
23
+ RUN \
24
+ addgroup --system --gid 1001 nodejs; \
25
+ adduser --system --uid 1001 nextjs
26
+
27
+ COPY --from=builder --link /app/public ./public
28
+
29
+ # Automatically leverage output traces to reduce image size
30
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
31
+ COPY --from=builder --link --chown=1001:1001 /app/.next/standalone ./
32
+ COPY --from=builder --link --chown=1001:1001 /app/.next/static ./.next/static
33
+
34
+ USER nextjs
35
+
36
+ EXPOSE 7860
37
+
38
+ ENV PORT 7860
39
+ ENV HOSTNAME 0.0.0.0
40
+
41
+ CMD ["node", "server.js"]
app/(logout)/sign-in/page.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/auth';
2
+ import { LoginButton } from '@/components/LoginButton';
3
+ import { redirect } from 'next/navigation';
4
+
5
+ export default async function SignInPage() {
6
+ const session = await auth();
7
+ // redirect to home if user is already logged in
8
+ if (session?.user) {
9
+ redirect('/');
10
+ }
11
+
12
+ return (
13
+ <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
14
+ <LoginButton oauth="google" />
15
+ <LoginButton oauth="github" />
16
+ </div>
17
+ );
18
+ }
app/(logout)/unauthorized/page.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import { Button } from '@/components/ui/Button';
4
+ import Link from 'next/link';
5
+
6
+ export default async function Unauthorized() {
7
+ const session = await auth();
8
+ // redirect to home if user is already logged in
9
+ if (session?.user) {
10
+ redirect('/');
11
+ }
12
+
13
+ return (
14
+ <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
15
+ <div>
16
+ You are not authorized to view this page. Please sign in with Landing
17
+ account to continue.
18
+ </div>
19
+ <Button asChild className="mt-16">
20
+ <Link href="/sign-in">Sign in</Link>
21
+ </Button>
22
+ </div>
23
+ );
24
+ }
app/api/auth/[...nextauth]/route.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { GET, POST } from '@/auth';
2
+ export const runtime = 'edge';
app/api/chat/route.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { OpenAIStream, StreamingTextResponse } from 'ai';
2
+ import OpenAI from 'openai';
3
+
4
+ import { auth } from '@/auth';
5
+ import {
6
+ ChatCompletionMessageParam,
7
+ ChatCompletionContentPart,
8
+ } from 'openai/resources';
9
+ import { MessageBase } from '../../../lib/types';
10
+
11
+ export const runtime = 'edge';
12
+
13
+ const openai = new OpenAI({
14
+ apiKey: process.env.OPENAI_API_KEY,
15
+ });
16
+
17
+ export const POST = async (req: Request) => {
18
+ const json = await req.json();
19
+ const { messages } = json as {
20
+ messages: MessageBase[];
21
+ id: string;
22
+ url: string;
23
+ };
24
+
25
+ const session = await auth();
26
+ if (!session?.user?.email) {
27
+ return new Response('Unauthorized', {
28
+ status: 401,
29
+ });
30
+ }
31
+
32
+ const formattedMessage: ChatCompletionMessageParam[] = messages.map(
33
+ message => {
34
+ const contentWithImage: ChatCompletionContentPart[] = [
35
+ {
36
+ type: 'text',
37
+ text: message.content as string,
38
+ },
39
+ ];
40
+ return {
41
+ role: 'user',
42
+ content: contentWithImage,
43
+ };
44
+ },
45
+ );
46
+ const res = await openai.chat.completions.create({
47
+ model: 'gpt-4-vision-preview',
48
+ messages: formattedMessage,
49
+ temperature: 0.3,
50
+ stream: true,
51
+ max_tokens: 300,
52
+ });
53
+
54
+ const stream = OpenAIStream(res);
55
+
56
+ return new StreamingTextResponse(stream);
57
+ };
app/api/sign/route.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getPresignedUrl } from '@/lib/aws';
2
+ import { withLogging } from '../../../lib/logger';
3
+ import { nanoid } from '@/lib/utils';
4
+
5
+ /**
6
+ * @param req
7
+ * @returns
8
+ */
9
+ export const POST = withLogging(
10
+ async (
11
+ session,
12
+ json: {
13
+ id?: string;
14
+ fileName: string;
15
+ fileType: string;
16
+ },
17
+ ): Promise<Response> => {
18
+ const user = session?.user?.email ?? 'anonymous';
19
+ // if (!email) {
20
+ // return new Response('Unauthorized', {
21
+ // status: 401,
22
+ // });
23
+ // }
24
+
25
+ try {
26
+ const { fileName, fileType, id } = json;
27
+
28
+ const signedFileName = `${user}/${id ?? nanoid()}/${fileName}`;
29
+ const res = await getPresignedUrl(signedFileName, fileType);
30
+ return Response.json({
31
+ id,
32
+ signedUrl: res.url,
33
+ publicUrl: `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${signedFileName}`,
34
+ fields: res.fields,
35
+ });
36
+ } catch (error) {
37
+ return new Response((error as Error).message, {
38
+ status: 400,
39
+ });
40
+ }
41
+ },
42
+ );
app/api/upload/route.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/auth';
2
+ import { createKVChat } from '@/lib/kv/chat';
3
+ import { withLogging } from '@/lib/logger';
4
+ import { ChatEntity, MessageBase } from '@/lib/types';
5
+ import { nanoid } from '@/lib/utils';
6
+ import { Session } from 'next-auth';
7
+ import { revalidatePath } from 'next/cache';
8
+
9
+ /**
10
+ * @param req
11
+ * @returns
12
+ */
13
+ export const POST = withLogging(
14
+ async (
15
+ session,
16
+ json: {
17
+ id?: string;
18
+ url: string;
19
+ initMessages?: MessageBase[];
20
+ },
21
+ ): Promise<Response> => {
22
+ const user = session?.user?.email ?? 'anonymous';
23
+ // if (!email) {
24
+ // return new Response('Unauthorized', {
25
+ // status: 401,
26
+ // });
27
+ // }
28
+
29
+ try {
30
+ const { id, url, initMessages } = json;
31
+
32
+ const payload: ChatEntity = {
33
+ url,
34
+ id: id ?? nanoid(),
35
+ user,
36
+ messages: initMessages ?? [],
37
+ updatedAt: Date.now(),
38
+ };
39
+
40
+ await createKVChat(payload);
41
+
42
+ revalidatePath('/chat', 'layout');
43
+
44
+ return Response.json(payload);
45
+ } catch (error) {
46
+ return new Response((error as Error).message, {
47
+ status: 400,
48
+ });
49
+ }
50
+ },
51
+ );
app/api/vision-agent/route.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StreamingTextResponse } from 'ai';
2
+
3
+ // import { auth } from '@/auth';
4
+ import { MessageBase } from '../../../lib/types';
5
+ import { withLogging } from '@/lib/logger';
6
+ import { CLEANED_SEPARATOR } from '@/lib/constants';
7
+ import { cleanAnswerMessage, cleanInputMessage } from '@/lib/messageUtils';
8
+
9
+ // export const runtime = 'edge';
10
+ export const dynamic = 'force-dynamic';
11
+ export const maxDuration = 300; // This function can run for a maximum of 5 minutes
12
+
13
+ export const POST = withLogging(
14
+ async (
15
+ _session,
16
+ json: {
17
+ messages: MessageBase[];
18
+ id: string;
19
+ url: string;
20
+ },
21
+ ) => {
22
+ const { messages, url } = json;
23
+
24
+ // const session = await auth();
25
+ // if (!session?.user?.email) {
26
+ // return new Response('Unauthorized', {
27
+ // status: 401,
28
+ // });
29
+ // }
30
+
31
+ const formData = new FormData();
32
+ formData.append(
33
+ 'input',
34
+ JSON.stringify(
35
+ messages.map(message => {
36
+ if (message.role !== 'assistant') {
37
+ return {
38
+ ...message,
39
+ content: cleanInputMessage(message.content),
40
+ };
41
+ } else {
42
+ const splitedContent = message.content.split(CLEANED_SEPARATOR);
43
+ return {
44
+ ...message,
45
+ content:
46
+ splitedContent.length > 1
47
+ ? cleanAnswerMessage(splitedContent[1])
48
+ : message.content,
49
+ };
50
+ }
51
+ }),
52
+ ),
53
+ );
54
+ formData.append('image', url);
55
+
56
+ const fetchResponse = await fetch(
57
+ 'https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&visualize_output=true',
58
+ // 'http://localhost:5050/v1/agent/chat?agent_class=vision_agent',
59
+ {
60
+ method: 'POST',
61
+ headers: {
62
+ apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k',
63
+ },
64
+ body: formData,
65
+ },
66
+ );
67
+
68
+ if (fetchResponse.body) {
69
+ return new StreamingTextResponse(fetchResponse.body);
70
+ } else {
71
+ return fetchResponse;
72
+ }
73
+ },
74
+ );
app/chat/[id]/page.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getKVChat } from '@/lib/kv/chat';
2
+ import { Chat } from '@/components/chat';
3
+
4
+ interface PageProps {
5
+ params: {
6
+ id: string;
7
+ };
8
+ }
9
+
10
+ export default async function Page({ params }: PageProps) {
11
+ const { id: chatId } = params;
12
+ const chat = await getKVChat(chatId);
13
+ return <Chat chat={chat} />;
14
+ }
app/chat/layout.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth, authEmail } from '@/auth';
2
+ import ChatSidebarList from '@/components/chat-sidebar/ChatListSidebar';
3
+ import Loading from '@/components/ui/Loading';
4
+ import { Suspense } from 'react';
5
+
6
+ interface ChatLayoutProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ export default async function Layout({ children }: ChatLayoutProps) {
11
+ const { email, isAdmin } = await authEmail();
12
+ return (
13
+ <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
14
+ <div
15
+ data-state={email ? 'open' : 'closed'}
16
+ className="peer absolute inset-y-0 z-30 hidden border-r bg-muted duration-300 ease-in-out -translate-x-full data-[state=open]:translate-x-0 lg:flex lg:w-[250px] h-full flex-col overflow-auto py-2"
17
+ >
18
+ <Suspense fallback={<Loading />}>
19
+ <ChatSidebarList />
20
+ </Suspense>
21
+ </div>
22
+ <Suspense fallback={<Loading />}>
23
+ <div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px]">
24
+ {children}
25
+ </div>
26
+ </Suspense>
27
+ </div>
28
+ );
29
+ }
app/chat/page.tsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import ImageSelector from '@/components/chat/ImageSelector';
4
+ import { generateInputImageMarkdown } from '@/lib/messageUtils';
5
+ import { ChatEntity, MessageBase } from '@/lib/types';
6
+ import { fetcher } from '@/lib/utils';
7
+ import Image from 'next/image';
8
+ import { useRouter } from 'next/navigation';
9
+
10
+ import {
11
+ Tooltip,
12
+ TooltipContent,
13
+ TooltipTrigger,
14
+ } from '@/components/ui/Tooltip';
15
+ import { IconDiscord, IconGitHub } from '@/components/ui/Icons';
16
+ import Link from 'next/link';
17
+ import { Button } from '@/components/ui/Button';
18
+ import Img from '@/components/ui/Img';
19
+
20
+ const exampleMessages = [
21
+ {
22
+ heading: 'Counting',
23
+ subheading: 'number of cereals in an image',
24
+ url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
25
+ initMessages: [
26
+ {
27
+ role: 'user',
28
+ content:
29
+ 'how many cereals are there in the image?' +
30
+ '\n\n' +
31
+ generateInputImageMarkdown(
32
+ 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
33
+ ),
34
+ id: 'fake-id-1',
35
+ },
36
+ ],
37
+ },
38
+ // {
39
+ // heading: 'Detecting',
40
+ // url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
41
+ // subheading: 'number of cereals in an image',
42
+ // message: `How many cereals are there in the image?`,
43
+ // },
44
+ ];
45
+
46
+ export default function Page() {
47
+ const router = useRouter();
48
+ return (
49
+ <div className="mx-auto max-w-2xl px-4 mt-8">
50
+ <div className="rounded-lg border bg-background p-8 mb-6">
51
+ <h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
52
+ <p>
53
+ Vision Agent is a library that helps you utilize agent frameworks for
54
+ your vision tasks. Vision Agent aims to provide an in-seconds
55
+ experience by allowing users to describe their problem in text and
56
+ utilizing agent frameworks to solve the task for them.
57
+ </p>
58
+ <div className="my-2">
59
+ <Tooltip>
60
+ <TooltipTrigger asChild>
61
+ <Button variant="link" size="icon" asChild className="mr-2">
62
+ <Link
63
+ href="https://github.com/landing-ai/vision-agent"
64
+ target="_blank"
65
+ >
66
+ <IconGitHub className="size-6" />
67
+ </Link>
68
+ </Button>
69
+ </TooltipTrigger>
70
+ <TooltipContent>Github</TooltipContent>
71
+ </Tooltip>
72
+ <Tooltip>
73
+ <TooltipTrigger asChild>
74
+ <Button variant="link" size="icon" asChild className="mr-2">
75
+ <Link href="https://discord.gg/wZ2A7J69" target="_blank">
76
+ <IconDiscord className="size-6" />
77
+ </Link>
78
+ </Button>
79
+ </TooltipTrigger>
80
+ <TooltipContent>Discord</TooltipContent>
81
+ </Tooltip>
82
+ </div>
83
+ <ImageSelector />
84
+ </div>
85
+ <div className="mb-4 grid grid-cols-2 gap-2 px-4 sm:px-0">
86
+ {exampleMessages.map((example, index) => (
87
+ <div
88
+ key={index}
89
+ className={`cursor-pointer rounded-lg border bg-white p-4 hover:bg-zinc-50 dark:bg-zinc-950 dark:hover:bg-zinc-900 flex items-center size-full ${
90
+ index > 1 && 'hidden md:block'
91
+ }`}
92
+ onClick={async () => {
93
+ const resp = await fetcher<ChatEntity>('/api/upload', {
94
+ method: 'POST',
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ },
98
+ body: JSON.stringify({
99
+ url: example.url,
100
+ initMessages: example.initMessages,
101
+ }),
102
+ });
103
+ if (resp) {
104
+ router.push(`/chat/${resp.id}`);
105
+ }
106
+ }}
107
+ >
108
+ <Img src={example.url} alt="example images" className="w-1/4" />
109
+ <div className="flex items-start flex-col h-full ml-3 w-3/4">
110
+ <div className="text-sm font-semibold">{example.heading}</div>
111
+ <div className="text-sm text-zinc-600">{example.subheading}</div>
112
+ </div>
113
+ </div>
114
+ ))}
115
+ </div>
116
+ </div>
117
+ );
118
+ }
app/globals.css ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
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
+ }
113
+
114
+ /* Light theme. */
115
+ :root {
116
+ --color-canvas-default: #ffffff;
117
+ --color-canvas-subtle: #f6f8fa;
118
+ --color-border-default: #d0d7de;
119
+ --color-border-muted: hsla(210, 18%, 87%, 1);
120
+ }
121
+
122
+ /* Dark theme. */
123
+ @media (prefers-color-scheme: dark) {
124
+ :root {
125
+ --color-canvas-default: #0d1117;
126
+ --color-canvas-subtle: #161b22;
127
+ --color-border-default: #30363d;
128
+ --color-border-muted: #21262d;
129
+ }
130
+ }
131
+
132
+ table {
133
+ border-spacing: 0;
134
+ border-collapse: collapse;
135
+ display: block;
136
+ margin-top: 0;
137
+ margin-bottom: 16px;
138
+ width: max-content;
139
+ max-width: 100%;
140
+ overflow: auto;
141
+ }
142
+
143
+ tr {
144
+ border-top: 1px solid var(--color-border-muted);
145
+ }
146
+
147
+ td,
148
+ th {
149
+ padding: 6px 13px;
150
+ border: 1px solid var(--color-border-default);
151
+ }
152
+
153
+ th {
154
+ font-weight: 600;
155
+ }
156
+
157
+ table img {
158
+ background-color: transparent;
159
+ }
app/layout.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Toaster } from 'react-hot-toast';
2
+ import { GeistSans } from 'geist/font/sans';
3
+ import { GeistMono } from 'geist/font/mono';
4
+
5
+ import '@/app/globals.css';
6
+ import { cn } from '@/lib/utils';
7
+ import { TailwindIndicator } from '@/components/TailwindIndicator';
8
+ import { Providers } from '@/components/Providers';
9
+ import { Header } from '@/components/Header';
10
+ import { ThemeToggle } from '@/components/ThemeToggle';
11
+
12
+ export const metadata = {
13
+ metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
14
+ title: {
15
+ default: 'Vision Agent',
16
+ template: `%s - Vision Agent`,
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 ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from '@/auth';
2
+ import { redirect } from 'next/navigation';
3
+
4
+ export default async function Page() {
5
+ redirect('/chat');
6
+
7
+ // return (
8
+ // <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2">
9
+ // Welcome to Insight Playground
10
+ // </div>
11
+ // );
12
+ }
app/project/[projectId]/page.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import MediaGrid from '@/components/project/MediaGrid';
2
+ import { fetchProjectClass, fetchProjectMedia } from '@/lib/fetch';
3
+ import ProjectChat from '@/components/project/ProjectChat';
4
+ import ClassBar from '@/components/project/ClassBar';
5
+
6
+ interface PageProps {
7
+ params: {
8
+ projectId: string;
9
+ };
10
+ }
11
+
12
+ export default async function Page({ params }: PageProps) {
13
+ const { projectId } = params;
14
+
15
+ const [mediaList, classList] = await Promise.all([
16
+ fetchProjectMedia({ projectId: Number(projectId) }),
17
+ fetchProjectClass({ projectId: Number(projectId) }),
18
+ ]);
19
+
20
+ return (
21
+ <div className="pt-4 md:pt-10 h-full">
22
+ <div className="flex h-full">
23
+ <div className="w-1/2 relative border-r border-gray-300 overflow-auto">
24
+ <ClassBar classList={classList} />
25
+ <MediaGrid mediaList={mediaList} />
26
+ </div>
27
+ <div className="w-1/2 relative overflow-auto">
28
+ <ProjectChat mediaList={mediaList} />
29
+ </div>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
app/project/layout.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ProjectListSideBar from '@/components/project-sidebar/ProjectListSideBar';
2
+ import { Suspense } from 'react';
3
+ import Loading from '@/components/ui/Loading';
4
+ import { authEmail } from '@/auth';
5
+ import { redirect } from 'next/navigation';
6
+
7
+ interface ChatLayoutProps {
8
+ children: React.ReactNode;
9
+ }
10
+
11
+ export default async function Layout({ children }: ChatLayoutProps) {
12
+ const { isAdmin } = await authEmail();
13
+
14
+ if (!isAdmin) {
15
+ redirect('/');
16
+ }
17
+
18
+ return (
19
+ <div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
20
+ <div
21
+ data-state="open"
22
+ 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"
23
+ >
24
+ <Suspense fallback={<Loading />}>
25
+ <ProjectListSideBar />
26
+ </Suspense>
27
+ </div>
28
+ <Suspense fallback={<Loading />}>
29
+ <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]">
30
+ {children}
31
+ </div>
32
+ </Suspense>
33
+ </div>
34
+ );
35
+ }
app/project/page.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export default function Page() {
2
+ const content = '<- Select a project from sidebar';
3
+ return (
4
+ <div className="flex flex-col h-[calc(100vh-theme(spacing.16))] items-center justify-center py-10 space-y-2 font-medium">
5
+ {content}
6
+ </div>
7
+ );
8
+ }
auth.ts ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import NextAuth, { type DefaultSession } from 'next-auth';
2
+ import GitHub from 'next-auth/providers/github';
3
+ import Google from 'next-auth/providers/google';
4
+
5
+ declare module 'next-auth' {
6
+ interface Session {
7
+ user: {
8
+ /** The user's id. */
9
+ id: string;
10
+ } & DefaultSession['user'];
11
+ }
12
+ }
13
+
14
+ const restrictedPath = ['/project'];
15
+
16
+ export const {
17
+ handlers: { GET, POST },
18
+ auth,
19
+ } = NextAuth({
20
+ providers: [
21
+ GitHub,
22
+ Google({
23
+ clientId: process.env.GOOGLE_CLIENT_ID!,
24
+ clientSecret: process.env.GOOGLE_SECRET!,
25
+ }),
26
+ ],
27
+ callbacks: {
28
+ // signIn({ profile }) {
29
+ // if (profile?.email?.endsWith('@landing.ai')) {
30
+ // return !!profile;
31
+ // } else {
32
+ // return '/unauthorized';
33
+ // }
34
+ // },
35
+ jwt({ token, profile }) {
36
+ if (profile) {
37
+ token.id = profile.id || profile.sub;
38
+ token.image = profile.avatar_url || profile.picture;
39
+ }
40
+ return token;
41
+ },
42
+ session: ({ session, token }) => {
43
+ if (session?.user && token?.id) {
44
+ session.user.id = String(token.id);
45
+ }
46
+ return session;
47
+ },
48
+ authorized({ request, auth }) {
49
+ const isAdmin = !!auth?.user?.email?.endsWith('landing.ai');
50
+ return restrictedPath.find(path =>
51
+ request.nextUrl.pathname.startsWith(path),
52
+ )
53
+ ? isAdmin
54
+ : true;
55
+ },
56
+ },
57
+ pages: {
58
+ signIn: '/sign-in', // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
59
+ },
60
+ });
61
+
62
+ export async function authEmail() {
63
+ const session = await auth();
64
+ const email = session?.user?.email;
65
+ return { email, isAdmin: !!email?.endsWith('landing.ai') };
66
+ }
components/Header.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import Link from 'next/link';
3
+
4
+ import { auth, authEmail } from '@/auth';
5
+ import { Button } from '@/components/ui/Button';
6
+ import { UserMenu } from '@/components/UserMenu';
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipTrigger,
11
+ } from '@/components/ui/Tooltip';
12
+ import { IconPlus, IconSeparator } from '@/components/ui/Icons';
13
+ import { LoginMenu } from './LoginMenu';
14
+ import { redirect } from 'next/navigation';
15
+
16
+ export async function Header() {
17
+ const session = await auth();
18
+ const { isAdmin } = await authEmail();
19
+ return (
20
+ <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">
21
+ {/* <Tooltip>
22
+ <TooltipTrigger asChild>
23
+ <Button variant="link" asChild className="mr-2">
24
+ <Link href="/chat">
25
+ <IconPlus />
26
+ </Link>
27
+ </Button>
28
+ </TooltipTrigger>
29
+ <TooltipContent>New chat</TooltipContent>
30
+ </Tooltip> */}
31
+ {isAdmin && (
32
+ <Button variant="link" asChild className="mr-2">
33
+ <Link href="/project">Projects (Internal)</Link>
34
+ </Button>
35
+ )}
36
+ <Button variant="link" asChild className="mr-2">
37
+ <Link href="/chat">Chat</Link>
38
+ </Button>
39
+ <IconSeparator className="size-6 text-muted-foreground/50" />
40
+ <div className="flex items-center">
41
+ {session?.user ? <UserMenu user={session!.user} /> : <LoginMenu />}
42
+ </div>
43
+ </header>
44
+ );
45
+ }
components/LoginButton.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { signIn } from 'next-auth/react';
5
+
6
+ import { cn } from '@/lib/utils';
7
+ import { Button, type ButtonProps } from '@/components/ui/Button';
8
+ import { IconGitHub, IconSpinner, IconGoogle } from '@/components/ui/Icons';
9
+
10
+ interface LoginButtonProps extends ButtonProps {
11
+ oauth: 'github' | 'google';
12
+ }
13
+
14
+ export function LoginButton({ oauth, ...props }: LoginButtonProps) {
15
+ const [isLoading, setIsLoading] = React.useState(false);
16
+
17
+ const icon =
18
+ oauth === 'github' ? (
19
+ <IconGitHub className="mr-2" />
20
+ ) : (
21
+ <IconGoogle className="mr-2" />
22
+ );
23
+ return (
24
+ <Button
25
+ variant="outline"
26
+ onClick={() => {
27
+ setIsLoading(true);
28
+ // next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
29
+ signIn(oauth, { callbackUrl: `/` });
30
+ }}
31
+ disabled={isLoading}
32
+ {...props}
33
+ >
34
+ {isLoading ? <IconSpinner className="mr-2 animate-spin" /> : icon}
35
+ Sign in with {oauth.charAt(0).toUpperCase() + oauth.slice(1)}
36
+ </Button>
37
+ );
38
+ }
components/LoginMenu.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/Button';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from '@/components/ui/DropdownMenu';
11
+ import { LoginButton } from './LoginButton';
12
+
13
+ export interface UserMenuProps {}
14
+
15
+ export function LoginMenu() {
16
+ return (
17
+ <div className="flex items-center justify-between">
18
+ <DropdownMenu>
19
+ <DropdownMenuTrigger asChild>
20
+ <Button variant="ghost">Sign in</Button>
21
+ </DropdownMenuTrigger>
22
+ <DropdownMenuContent sideOffset={8} align="end" className="w-[220px]">
23
+ <DropdownMenuItem className="flex-col items-center">
24
+ <LoginButton oauth="google" />
25
+ </DropdownMenuItem>
26
+ <DropdownMenuItem className="flex-col items-center">
27
+ <LoginButton oauth="github" />
28
+ </DropdownMenuItem>
29
+ </DropdownMenuContent>
30
+ </DropdownMenu>
31
+ </div>
32
+ );
33
+ }
components/Providers.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { ThemeProvider as NextThemesProvider } from 'next-themes';
5
+ import { ThemeProviderProps } from 'next-themes/dist/types';
6
+ import { TooltipProvider } from '@/components/ui/Tooltip';
7
+ import { ThemeToggle } from './ThemeToggle';
8
+
9
+ export function Providers({ children, ...props }: ThemeProviderProps) {
10
+ return (
11
+ <NextThemesProvider {...props}>
12
+ <TooltipProvider>
13
+ {children}
14
+ <ThemeToggle />
15
+ </TooltipProvider>
16
+ </NextThemesProvider>
17
+ );
18
+ }
components/TailwindIndicator.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function TailwindIndicator() {
2
+ if (process.env.NODE_ENV === 'production') return null;
3
+
4
+ return (
5
+ <div className="fixed bottom-1 left-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
6
+ <div className="block sm:hidden">xs</div>
7
+ <div className="hidden sm:block md:hidden">sm</div>
8
+ <div className="hidden md:block lg:hidden">md</div>
9
+ <div className="hidden lg:block xl:hidden">lg</div>
10
+ <div className="hidden xl:block 2xl:hidden">xl</div>
11
+ <div className="hidden 2xl:block">2xl</div>
12
+ </div>
13
+ );
14
+ }
components/ThemeToggle.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { useTheme } from 'next-themes';
5
+
6
+ import { Button } from '@/components/ui/Button';
7
+ import { IconMoon, IconSun } from '@/components/ui/Icons';
8
+
9
+ export function ThemeToggle() {
10
+ const { setTheme, theme } = useTheme();
11
+ const [_, startTransition] = React.useTransition();
12
+
13
+ return (
14
+ <Button
15
+ variant="ghost"
16
+ size="icon"
17
+ className="fixed bottom-4 right-4 z-50 dark:bg-zinc-950 dark:text-white transition-all p-2 rounded-full shadow-md"
18
+ onClick={() => {
19
+ startTransition(() => {
20
+ setTheme(theme === 'light' ? 'dark' : 'light');
21
+ });
22
+ }}
23
+ >
24
+ {theme === '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 ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Image from 'next/image';
4
+ import { type Session } from 'next-auth';
5
+ import { signOut } from 'next-auth/react';
6
+
7
+ import { Button } from '@/components/ui/Button';
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from '@/components/ui/DropdownMenu';
15
+ import { IconExternalLink } from '@/components/ui/Icons';
16
+
17
+ export interface UserMenuProps {
18
+ user: Session['user'];
19
+ }
20
+
21
+ function getUserInitials(name: string) {
22
+ const [firstName, lastName] = name.split(' ');
23
+ return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2);
24
+ }
25
+
26
+ export function UserMenu({ user }: UserMenuProps) {
27
+ return (
28
+ <div className="flex items-center justify-between">
29
+ <DropdownMenu>
30
+ <DropdownMenuTrigger asChild>
31
+ <Button variant="ghost">
32
+ {user?.image ? (
33
+ <Image
34
+ className="size-6 transition-opacity duration-300 rounded-full select-none ring-1 ring-zinc-100/10 hover:opacity-80"
35
+ src={user?.image ?? ''}
36
+ alt={user.name ?? 'Avatar'}
37
+ height={48}
38
+ width={48}
39
+ />
40
+ ) : (
41
+ <div className="flex items-center justify-center text-xs font-medium uppercase rounded-full select-none size-7 shrink-0 bg-muted/50 text-muted-foreground">
42
+ {user?.name ? getUserInitials(user?.name) : null}
43
+ </div>
44
+ )}
45
+ <span className="ml-2">{user?.name}</span>
46
+ </Button>
47
+ </DropdownMenuTrigger>
48
+ <DropdownMenuContent sideOffset={8} align="start" className="w-[180px]">
49
+ <DropdownMenuItem className="flex-col items-start">
50
+ <div className="text-xs font-medium">{user?.name}</div>
51
+ <div className="text-xs text-zinc-500">{user?.email}</div>
52
+ </DropdownMenuItem>
53
+ <DropdownMenuSeparator />
54
+ <DropdownMenuItem
55
+ onClick={() =>
56
+ signOut({
57
+ callbackUrl: '/',
58
+ })
59
+ }
60
+ className="text-xs"
61
+ >
62
+ Log Out
63
+ </DropdownMenuItem>
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ </div>
67
+ );
68
+ }
components/chat-sidebar/ChatAdminToggle.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { chatViewMode } from '@/state/chat';
4
+ import { useAtom } from 'jotai';
5
+ import React from 'react';
6
+
7
+ export interface ChatAdminToggleProps {}
8
+
9
+ const ChatAdminToggle: React.FC<ChatAdminToggleProps> = () => {
10
+ const modeAtom = useAtom(chatViewMode);
11
+ return null;
12
+ };
13
+
14
+ export default ChatAdminToggle;
components/chat-sidebar/ChatCard.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { PropsWithChildren } from 'react';
4
+ import Link from 'next/link';
5
+ import { useParams, usePathname, useRouter } from 'next/navigation';
6
+ import { cn } from '@/lib/utils';
7
+ import { ChatEntity } from '@/lib/types';
8
+ import Image from 'next/image';
9
+ import clsx from 'clsx';
10
+ import Img from '../ui/Img';
11
+ import { format } from 'date-fns';
12
+ import { cleanInputMessage } from '@/lib/messageUtils';
13
+ // import { format } from 'date-fns';
14
+
15
+ type ChatCardProps = PropsWithChildren<{
16
+ chat: ChatEntity;
17
+ }>;
18
+
19
+ export const ChatCardLayout: React.FC<
20
+ PropsWithChildren<{ link: string; classNames?: clsx.ClassValue }>
21
+ > = ({ link, children, classNames }) => {
22
+ return (
23
+ <Link
24
+ className={cn(
25
+ 'p-2 m-2 bg-background max-h-[100px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
26
+ classNames,
27
+ )}
28
+ href={link}
29
+ >
30
+ {children}
31
+ </Link>
32
+ );
33
+ };
34
+
35
+ const ChatCard: React.FC<ChatCardProps> = ({ chat }) => {
36
+ const { id: chatIdFromParam } = useParams();
37
+ const pathname = usePathname();
38
+ const { id, url, messages, user, updatedAt } = chat;
39
+ const firstMessage = cleanInputMessage(messages?.[0]?.content ?? '');
40
+ const title = firstMessage
41
+ ? firstMessage.length > 50
42
+ ? firstMessage.slice(0, 50) + '...'
43
+ : firstMessage
44
+ : '(No messages yet)';
45
+ return (
46
+ <ChatCardLayout
47
+ link={`/chat/${id}`}
48
+ classNames={chatIdFromParam === id && 'border-gray-500'}
49
+ >
50
+ <div className="overflow-hidden flex items-center size-full">
51
+ <Img src={url} alt={`chat-${id}-card-image`} className="w-1/4" />
52
+ <div className="flex items-start flex-col h-full ml-3 w-3/4">
53
+ <p className="text-sm mb-1">{title}</p>
54
+ <p className="text-xs text-gray-500">
55
+ {updatedAt ? format(Number(updatedAt), 'yyyy-MM-dd') : '-'}
56
+ </p>
57
+ </div>
58
+ </div>
59
+ </ChatCardLayout>
60
+ );
61
+ };
62
+
63
+ export default ChatCard;
components/chat-sidebar/ChatListSidebar.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getKVChats } from '@/lib/kv/chat';
2
+ import ChatCard, { ChatCardLayout } from './ChatCard';
3
+ import { IconPlus } from '../ui/Icons';
4
+ import { auth } from '@/auth';
5
+
6
+ export interface ChatSidebarListProps {}
7
+
8
+ export default async function ChatSidebarList({}: ChatSidebarListProps) {
9
+ const session = await auth();
10
+ if (!session || !session.user) {
11
+ return null;
12
+ }
13
+ const chats = await getKVChats();
14
+ return (
15
+ <>
16
+ <ChatCardLayout link="/chat">
17
+ <div className="overflow-hidden flex items-center size-full">
18
+ <IconPlus className="w-1/4 font-bold" />
19
+ <p className="text-sm w-3/4 ml-3 font-bold">New chat</p>
20
+ </div>
21
+ </ChatCardLayout>
22
+ {chats.map(chat => (
23
+ <ChatCard key={chat.id} chat={chat} />
24
+ ))}
25
+ </>
26
+ );
27
+ }
components/chat/ChatList.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { Separator } from '@/components/ui/Separator';
4
+ import { ChatMessage } from '@/components/chat/ChatMessage';
5
+ import { MessageBase } from '../../lib/types';
6
+
7
+ export interface ChatList {
8
+ messages: MessageBase[];
9
+ }
10
+
11
+ export function ChatList({ messages }: ChatList) {
12
+ return (
13
+ <div className="relative mx-auto max-w-5xl 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 ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Inspired by Chatbot-UI and modified to fit the needs of this project
2
+ // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3
+
4
+ import remarkGfm from 'remark-gfm';
5
+ import remarkMath from 'remark-math';
6
+
7
+ import { useMemo } from 'react';
8
+ import { cn } from '@/lib/utils';
9
+ import { CodeBlock } from '@/components/ui/CodeBlock';
10
+ import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
11
+ import { IconOpenAI, IconUser } from '@/components/ui/Icons';
12
+ import { MessageBase } from '../../lib/types';
13
+ import {
14
+ Tooltip,
15
+ TooltipContent,
16
+ TooltipTrigger,
17
+ } from '@/components/ui/Tooltip';
18
+ import Img from '../ui/Img';
19
+ import { getCleanedUpMessages } from '@/lib/messageUtils';
20
+
21
+ export interface ChatMessageProps {
22
+ message: MessageBase;
23
+ }
24
+
25
+ export function ChatMessage({ message, ...props }: ChatMessageProps) {
26
+ const { logs, content } = useMemo(() => {
27
+ return getCleanedUpMessages({
28
+ content: message.content,
29
+ role: message.role,
30
+ });
31
+ }, [message.content, message.role]);
32
+ return (
33
+ <div className={cn('group relative mb-4 flex items-start')} {...props}>
34
+ <div
35
+ className={cn(
36
+ 'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
37
+ message.role === 'user'
38
+ ? 'bg-background'
39
+ : 'bg-primary text-primary-foreground',
40
+ )}
41
+ >
42
+ {message.role === 'user' ? <IconUser /> : <IconOpenAI />}
43
+ </div>
44
+ <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
45
+ {logs && message.role !== 'user' && (
46
+ <div className="bg-slate-100 dark:bg-slate-900 mb-4 p-4 max-h-[400px] overflow-auto">
47
+ <div className="text-xl font-bold">Thinking Process</div>
48
+ <MemoizedReactMarkdown
49
+ className="break-words text-sm"
50
+ remarkPlugins={[remarkGfm, remarkMath]}
51
+ components={{
52
+ p({ children }) {
53
+ return (
54
+ <p className="my-2 last:mb-0 whitespace-pre-line">
55
+ {children}
56
+ </p>
57
+ );
58
+ },
59
+ code({ children, ...props }) {
60
+ return (
61
+ <code className="whitespace-pre-line">{children}</code>
62
+ );
63
+ },
64
+ }}
65
+ >
66
+ {logs}
67
+ </MemoizedReactMarkdown>
68
+ </div>
69
+ )}
70
+ <MemoizedReactMarkdown
71
+ className="break-words"
72
+ remarkPlugins={[remarkGfm, remarkMath]}
73
+ components={{
74
+ p({ children, ...props }) {
75
+ if (
76
+ props.node.children.some(
77
+ child => child.type === 'element' && child.tagName === 'img',
78
+ )
79
+ ) {
80
+ return (
81
+ <p className="flex flex-wrap gap-2 items-start">{children}</p>
82
+ );
83
+ }
84
+ return (
85
+ <p className="my-2 last:mb-0 whitespace-pre-line">{children}</p>
86
+ );
87
+ },
88
+ img(props) {
89
+ return (
90
+ <Tooltip>
91
+ <TooltipTrigger asChild>
92
+ <Img
93
+ src={props.src ?? '/landing.png'}
94
+ alt={props.alt ?? 'answer-image'}
95
+ quality={100}
96
+ className="cursor-zoom-in"
97
+ sizes="(min-width: 66em) 25vw,
98
+ (min-width: 44em) 40vw,
99
+ 100vw"
100
+ />
101
+ </TooltipTrigger>
102
+ <TooltipContent>
103
+ <Img
104
+ className="m-2"
105
+ src={props.src ?? '/landing.png'}
106
+ alt={props.alt ?? 'answer-image'}
107
+ quality={100}
108
+ width={500}
109
+ />
110
+ </TooltipContent>
111
+ </Tooltip>
112
+ );
113
+ },
114
+ code({ node, inline, className, children, ...props }) {
115
+ if (children.length) {
116
+ if (children[0] == '▍') {
117
+ return (
118
+ <span className="mt-1 cursor-default animate-pulse">▍</span>
119
+ );
120
+ }
121
+
122
+ children[0] = (children[0] as string).replace('`▍`', '▍');
123
+ }
124
+
125
+ const match = /language-(\w+)/.exec(className || '');
126
+ if (inline) {
127
+ return (
128
+ <code className={className} {...props}>
129
+ {children}
130
+ </code>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <CodeBlock
136
+ key={Math.random()}
137
+ language={(match && match[1]) || ''}
138
+ value={String(children).replace(/\n$/, '')}
139
+ {...props}
140
+ />
141
+ );
142
+ },
143
+ }}
144
+ >
145
+ {content}
146
+ </MemoizedReactMarkdown>
147
+ {/* <ChatMessageActions message={message} /> */}
148
+ </div>
149
+ </div>
150
+ );
151
+ }
components/chat/ChatMessageActions.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { type Message } from 'ai';
4
+
5
+ import { Button } from '@/components/ui/Button';
6
+ import { IconCheck, IconCopy } from '@/components/ui/Icons';
7
+ import { useCopyToClipboard } from '@/lib/hooks/useCopyToClipboard';
8
+ import { cn } from '@/lib/utils';
9
+ import { MessageBase } from '../../lib/types';
10
+
11
+ interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
+ message: MessageBase;
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/Composer.tsx ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { type UseChatHelpers } from 'ai/react';
5
+ import Textarea from 'react-textarea-autosize';
6
+
7
+ import { Button } from '@/components/ui/Button';
8
+ import { MessageBase } from '../../lib/types';
9
+ import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit';
10
+ import Img from '../ui/Img';
11
+ import {
12
+ Tooltip,
13
+ TooltipContent,
14
+ TooltipTrigger,
15
+ } from '@/components/ui/Tooltip';
16
+ import {
17
+ IconArrowDown,
18
+ IconArrowElbow,
19
+ IconRefresh,
20
+ IconStop,
21
+ } from '@/components/ui/Icons';
22
+ import { cn } from '@/lib/utils';
23
+ import { generateInputImageMarkdown } from '@/lib/messageUtils';
24
+
25
+ export interface ComposerProps
26
+ extends Pick<
27
+ UseChatHelpers,
28
+ 'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput'
29
+ > {
30
+ id?: string;
31
+ title?: string;
32
+ messages: MessageBase[];
33
+ url?: string;
34
+ isAtBottom: boolean;
35
+ scrollToBottom: () => void;
36
+ }
37
+
38
+ export function Composer({
39
+ id,
40
+ title,
41
+ isLoading,
42
+ stop,
43
+ append,
44
+ reload,
45
+ input,
46
+ setInput,
47
+ messages,
48
+ isAtBottom,
49
+ scrollToBottom,
50
+ url,
51
+ }: ComposerProps) {
52
+ const { formRef, onKeyDown } = useEnterSubmit();
53
+ const inputRef = React.useRef<HTMLTextAreaElement>(null);
54
+ React.useEffect(() => {
55
+ if (inputRef.current) {
56
+ inputRef.current.focus();
57
+ }
58
+ }, []);
59
+
60
+ return (
61
+ // <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] h-[178px]">
62
+ <div className="mx-auto sm:max-w-3xl sm:px-4 h-full">
63
+ <div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4 h-full">
64
+ <form
65
+ onSubmit={async e => {
66
+ e.preventDefault();
67
+ if (!input?.trim()) {
68
+ return;
69
+ }
70
+ setInput('');
71
+ await append({
72
+ id,
73
+ content:
74
+ input + (url ? '\n\n' + generateInputImageMarkdown(url) : ''),
75
+ role: 'user',
76
+ });
77
+ scrollToBottom();
78
+ }}
79
+ ref={formRef}
80
+ className="h-full"
81
+ >
82
+ <div className="relative flex px-8 pl-2 overflow-hidden size-full bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2 items-start">
83
+ {url && (
84
+ <div className="w-1/5 p-2 h-full flex items-center justify-center relative">
85
+ <Tooltip>
86
+ <TooltipTrigger asChild>
87
+ <Img
88
+ src={url}
89
+ className="cursor-zoom-in"
90
+ alt="preview-image"
91
+ />
92
+ </TooltipTrigger>
93
+ <TooltipContent>
94
+ <Img
95
+ src={url}
96
+ className="m-2"
97
+ quality={100}
98
+ width={500}
99
+ alt="zoomed-in-image"
100
+ />
101
+ </TooltipContent>
102
+ </Tooltip>
103
+ </div>
104
+ )}
105
+ <Textarea
106
+ ref={inputRef}
107
+ tabIndex={0}
108
+ onKeyDown={onKeyDown}
109
+ rows={1}
110
+ value={input}
111
+ disabled={isLoading}
112
+ onChange={e => setInput(e.target.value)}
113
+ placeholder={
114
+ isLoading
115
+ ? 'Vision Agent is thinking...'
116
+ : 'Ask question about the image.'
117
+ }
118
+ spellCheck={false}
119
+ className="min-h-[60px] w-4/5 resize-none bg-transparent px-4 py-[1.3em] focus-within:outline-none sm:text-sm"
120
+ />
121
+ {/* Scroll to bottom Icon */}
122
+ <div
123
+ className={cn(
124
+ 'absolute top-3 right-4 transition-opacity duration-300',
125
+ isAtBottom ? 'opacity-0' : 'opacity-100',
126
+ )}
127
+ >
128
+ <Tooltip>
129
+ <TooltipTrigger asChild>
130
+ <Button
131
+ variant="outline"
132
+ size="icon"
133
+ className="bg-background"
134
+ onClick={() => scrollToBottom()}
135
+ >
136
+ <IconArrowDown />
137
+ </Button>
138
+ </TooltipTrigger>
139
+ <TooltipContent>Scroll to bottom</TooltipContent>
140
+ </Tooltip>
141
+ </div>
142
+ {/* Stop / Regenerate Icon */}
143
+ <div className="absolute bottom-14 right-4">
144
+ {isLoading ? (
145
+ <Tooltip>
146
+ <TooltipTrigger asChild>
147
+ <Button
148
+ variant="outline"
149
+ size="icon"
150
+ className="bg-background"
151
+ onClick={() => stop()}
152
+ >
153
+ <IconStop />
154
+ </Button>
155
+ </TooltipTrigger>
156
+ <TooltipContent>Stop generating</TooltipContent>
157
+ </Tooltip>
158
+ ) : (
159
+ messages?.length >= 2 && (
160
+ <Tooltip>
161
+ <TooltipTrigger asChild>
162
+ <Button
163
+ variant="outline"
164
+ size="icon"
165
+ className="bg-background"
166
+ onClick={() => reload()}
167
+ >
168
+ <IconRefresh />
169
+ </Button>
170
+ </TooltipTrigger>
171
+ <TooltipContent>Regenerate response</TooltipContent>
172
+ </Tooltip>
173
+ )
174
+ )}
175
+ </div>
176
+ {/* Submit Icon */}
177
+ <div className="absolute bottom-3 right-4">
178
+ <Tooltip>
179
+ <TooltipTrigger asChild>
180
+ <Button
181
+ type="submit"
182
+ size="icon"
183
+ disabled={isLoading || input === ''}
184
+ >
185
+ <IconArrowElbow />
186
+ </Button>
187
+ </TooltipTrigger>
188
+ <TooltipContent>Send message</TooltipContent>
189
+ </Tooltip>
190
+ </div>
191
+ </div>
192
+ </form>
193
+ </div>
194
+ </div>
195
+ // </div>
196
+ );
197
+ }
components/chat/ImageSelector.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import useImageUpload from '../../lib/hooks/useImageUpload';
5
+ import { cn, fetcher } from '@/lib/utils';
6
+ import { SignedPayload, MessageBase, ChatEntity } from '@/lib/types';
7
+ import { useRouter } from 'next/navigation';
8
+ import Loading from '../ui/Loading';
9
+ import toast from 'react-hot-toast';
10
+
11
+ export interface ImageSelectorProps {}
12
+
13
+ type Example = {
14
+ url: string;
15
+ initMessages: MessageBase[];
16
+ };
17
+
18
+ const ImageSelector: React.FC<ImageSelectorProps> = () => {
19
+ const router = useRouter();
20
+ const [isUploading, setUploading] = useState(false);
21
+ const { getRootProps, getInputProps, isDragActive } = useImageUpload(
22
+ undefined,
23
+ async files => {
24
+ const formData = new FormData();
25
+ if (files.length !== 1) {
26
+ throw new Error('Only one image can be uploaded at a time');
27
+ }
28
+ setUploading(true);
29
+ const reader = new FileReader();
30
+ reader.readAsDataURL(files[0]);
31
+ reader.onload = async () => {
32
+ const { id, signedUrl, publicUrl, fields } =
33
+ await fetcher<SignedPayload>('/api/sign', {
34
+ method: 'POST',
35
+ body: JSON.stringify({
36
+ fileType: files[0].type,
37
+ fileName: files[0].name,
38
+ }),
39
+ });
40
+ const formData = new FormData();
41
+ Object.entries(fields).forEach(([key, value]) => {
42
+ formData.append(key, value as string);
43
+ });
44
+ formData.append('file', files[0]);
45
+
46
+ const uploadResponse = await fetch(signedUrl, {
47
+ method: 'POST',
48
+ body: formData,
49
+ });
50
+ if (!uploadResponse.ok) {
51
+ toast.error(uploadResponse.statusText);
52
+ return;
53
+ }
54
+ const resp = await fetcher<ChatEntity>('/api/upload', {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ },
59
+ body: JSON.stringify({
60
+ id,
61
+ url: publicUrl,
62
+ }),
63
+ });
64
+ setUploading(false);
65
+ if (resp) {
66
+ router.push(`/chat/${resp.id}`);
67
+ }
68
+ };
69
+ },
70
+ );
71
+ return (
72
+ <div
73
+ {...getRootProps()}
74
+ className={cn(
75
+ 'dropzone border-2 border-dashed border-gray-400 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer',
76
+ isDragActive && 'bg-gray-500/50 border-solid',
77
+ )}
78
+ >
79
+ <input {...getInputProps()} />
80
+ <div className="text-gray-400 text-md">
81
+ {isUploading ? (
82
+ <Loading />
83
+ ) : (
84
+ 'Start using Vision Agent by selecting an image'
85
+ )}
86
+ </div>
87
+ </div>
88
+ );
89
+ };
90
+
91
+ export default ImageSelector;
components/chat/MemoizedReactMarkdown.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { FC, memo } from 'react';
2
+ import ReactMarkdown, { Options } from 'react-markdown';
3
+
4
+ export const MemoizedReactMarkdown: FC<Options> = memo(
5
+ ReactMarkdown,
6
+ (prevProps, nextProps) =>
7
+ prevProps.children === nextProps.children &&
8
+ prevProps.className === nextProps.className,
9
+ );
components/chat/index.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { ChatList } from '@/components/chat/ChatList';
4
+ import { Composer } from '@/components/chat/Composer';
5
+ import { ChatEntity } from '@/lib/types';
6
+ import useVisionAgent from '@/lib/hooks/useVisionAgent';
7
+ import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
8
+
9
+ export interface ChatProps extends React.ComponentProps<'div'> {
10
+ chat: ChatEntity;
11
+ }
12
+
13
+ export function Chat({ chat }: ChatProps) {
14
+ const { url, id } = chat;
15
+ const { messages, append, reload, stop, isLoading, input, setInput } =
16
+ useVisionAgent(chat);
17
+
18
+ const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
19
+ useScrollAnchor();
20
+
21
+ return (
22
+ <>
23
+ <div className="h-full overflow-auto relative" ref={scrollRef}>
24
+ <div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
25
+ <ChatList messages={messages} />
26
+ <div className="h-px w-full" ref={visibilityRef} />
27
+ </div>
28
+ </div>
29
+ <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] h-[178px]">
30
+ <Composer
31
+ id={id}
32
+ url={url}
33
+ isLoading={isLoading}
34
+ stop={stop}
35
+ append={append}
36
+ reload={reload}
37
+ messages={messages}
38
+ input={input}
39
+ setInput={setInput}
40
+ isAtBottom={isAtBottom}
41
+ scrollToBottom={scrollToBottom}
42
+ />
43
+ </div>
44
+ </>
45
+ );
46
+ }
components/project-sidebar/ProjectCard.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { ProjectBaseInfo } from '@/lib/fetch';
4
+ import { format } from 'date-fns';
5
+ import Link from 'next/link';
6
+ import { useParams } from 'next/navigation';
7
+ import { cn } from '@/lib/utils';
8
+ import Chip from '../ui/Chip';
9
+
10
+ export interface ProjectCardProps {
11
+ projectInfo: ProjectBaseInfo;
12
+ }
13
+
14
+ export enum LabelType {
15
+ BoundingBox = 'bounding_box',
16
+ Segmentation = 'segmentation',
17
+ Classification = 'classification',
18
+ AnomalyDetection = 'anomaly_detection',
19
+ SegmentationInstantLearning = 'segmentation_instant_learning',
20
+ }
21
+
22
+ const LabelTypeDisplayText: { [key: string]: string } = {
23
+ [LabelType.BoundingBox]: 'detection',
24
+ [LabelType.Segmentation]: 'segmentation',
25
+ [LabelType.Classification]: 'classification',
26
+ [LabelType.AnomalyDetection]: 'anomaly',
27
+ [LabelType.SegmentationInstantLearning]: 'vp',
28
+ };
29
+
30
+ const ProjectCard: React.FC<ProjectCardProps> = ({ projectInfo }) => {
31
+ const { id, name, created_at, label_type, orgName, subscription } =
32
+ projectInfo;
33
+
34
+ const { projectId: projectIdFromParam } = useParams();
35
+
36
+ const formattedDate = format(created_at, 'yyyy-MM-dd');
37
+ return (
38
+ <Link
39
+ className={cn(
40
+ 'p-4 m-2 bg-background l:h-[250px] rounded-xl shadow-md flex items-center border border-transparent hover:border-gray-500 transition-all cursor-pointer',
41
+ Number(projectIdFromParam) === id && 'border-gray-500',
42
+ )}
43
+ href={`/project/${id}`}
44
+ >
45
+ <div className="overflow-hidden w-full">
46
+ <div className="flex items-center justify-between w-full">
47
+ <p className="text-xs text-gray-500 truncate mr-2 truncate">
48
+ {orgName}
49
+ </p>
50
+ <p className="text-xs text-gray-500 italic">{subscription}</p>
51
+ </div>
52
+ <div className="flex mb-1 items-center">
53
+ <p className="text-sm font-medium text-gray mr-2 truncate">{name}</p>
54
+ <Chip value={LabelTypeDisplayText[label_type]} />
55
+ </div>
56
+ <div className="flex items-center truncate">
57
+ <p className="text-xs text-gray-500">{formattedDate}</p>
58
+ </div>
59
+ </div>
60
+ </Link>
61
+ );
62
+ };
63
+
64
+ export default ProjectCard;
components/project-sidebar/ProjectListSideBar.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { fetchRecentProjectList } from '@/lib/fetch';
2
+ import ProjectCard from './ProjectCard';
3
+
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/ClassBar.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ClassDetails } from '@/lib/fetch';
2
+ import Chip from '../ui/Chip';
3
+
4
+ export interface ClassBarProps {
5
+ classList: ClassDetails[];
6
+ }
7
+
8
+ export default async function ClassBar({ classList }: ClassBarProps) {
9
+ return (
10
+ <div className="border-b border-gray-300 px-3 pb-3 max-w-3xl mx-auto">
11
+ {classList.map(classItem => {
12
+ return (
13
+ <Chip
14
+ key={classItem.id}
15
+ value={classItem.name}
16
+ className="px-3 py-1 my-1"
17
+ />
18
+ );
19
+ })}
20
+ </div>
21
+ );
22
+ }
components/project/MediaGrid.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MediaDetails } from '@/lib/fetch';
2
+ import MediaTile from './MediaTile';
3
+
4
+ export default function MediaGrid({
5
+ mediaList,
6
+ }: {
7
+ mediaList: MediaDetails[];
8
+ }) {
9
+ return (
10
+ <div className="relative size-full p-3 max-w-3xl mx-auto">
11
+ <div className="columns-1 sm:columns-1 md:columns-2 lg:columns-2 xl:columns:3 gap-3 [&>img:not(:first-child)]:mt-3">
12
+ {mediaList.map(media => (
13
+ <MediaTile key={media.id} media={media} />
14
+ ))}
15
+ </div>
16
+ </div>
17
+ );
18
+ }
components/project/MediaTile.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import Image from 'next/image';
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipTrigger,
9
+ } from '@/components/ui/Tooltip';
10
+ import { MediaDetails } from '@/lib/fetch';
11
+ import { useAtom } from 'jotai';
12
+ import { selectedMediaIdAtom } from '@/state/media';
13
+ import { cn } from '@/lib/utils';
14
+
15
+ export interface MediaTileProps {
16
+ media: MediaDetails;
17
+ }
18
+
19
+ export default function MediaTile({ media }: MediaTileProps) {
20
+ const {
21
+ url,
22
+ thumbnails,
23
+ id,
24
+ name,
25
+ properties: { width, height },
26
+ } = media;
27
+ const [selectedMediaId, setSelectedMediaId] = useAtom(selectedMediaIdAtom);
28
+ const selected = selectedMediaId === id;
29
+ // const imageSrc = thumbnails.length ? thumbnails[thumbnails.length - 1] : url;
30
+ const imageSrc = url;
31
+ return (
32
+ <Tooltip>
33
+ <TooltipTrigger asChild>
34
+ <Image
35
+ src={imageSrc}
36
+ draggable={false}
37
+ alt="dataset images"
38
+ width={width}
39
+ height={height}
40
+ onClick={() => setSelectedMediaId(id)}
41
+ className={cn(
42
+ 'w-full h-auto relative rounded-xl overflow-hidden shadow-md cursor-pointer transition-transform hover:scale-105 box-content',
43
+ selected && 'border-2 border-primary',
44
+ )}
45
+ />
46
+ </TooltipTrigger>
47
+ <TooltipContent>
48
+ <p>{name}</p>
49
+ <p className="font-light text-xs">{`${width} x ${height}`}</p>
50
+ </TooltipContent>
51
+ </Tooltip>
52
+ );
53
+ }
components/project/ProjectChat.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { MediaDetails } from '@/lib/fetch';
4
+ import React from 'react';
5
+ import { ChatList } from '../chat/ChatList';
6
+ import useVisionAgent from '@/lib/hooks/useVisionAgent';
7
+ import { nanoid } from '@/lib/utils';
8
+ import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
9
+ import { Composer } from '../chat/Composer';
10
+ import { useAtomValue } from 'jotai';
11
+ import { selectedMediaIdAtom } from '@/state/media';
12
+
13
+ export interface ChatProps {
14
+ mediaList: MediaDetails[];
15
+ }
16
+
17
+ const ProjectChat: React.FC<ChatProps> = ({ mediaList }) => {
18
+ const selectedMediaId = useAtomValue(selectedMediaIdAtom);
19
+ // fallback to the first media
20
+ const selectedMedia =
21
+ mediaList.find(media => media.id === selectedMediaId) ?? mediaList[0];
22
+ const { messages, append, reload, stop, isLoading, input, setInput } =
23
+ useVisionAgent({
24
+ url: selectedMedia.url,
25
+ messages: [],
26
+ user: 'does-not-matter@landing.ai',
27
+ updatedAt: Date.now(),
28
+ });
29
+
30
+ const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
31
+ useScrollAnchor();
32
+
33
+ return (
34
+ <>
35
+ <div className="h-full overflow-auto" ref={scrollRef}>
36
+ <div className="pb-[200px] pt-4 md:pt-10" ref={messagesRef}>
37
+ <ChatList messages={messages} />
38
+ <div className="h-px w-full" ref={visibilityRef} />
39
+ </div>
40
+ </div>
41
+ <div className="absolute inset-x-0 bottom-0 w-full h-[178px]">
42
+ <Composer
43
+ url={selectedMedia.url}
44
+ isLoading={isLoading}
45
+ stop={stop}
46
+ append={append}
47
+ reload={reload}
48
+ messages={messages}
49
+ input={input}
50
+ setInput={setInput}
51
+ isAtBottom={isAtBottom}
52
+ scrollToBottom={scrollToBottom}
53
+ />
54
+ </div>
55
+ </>
56
+ );
57
+ };
58
+
59
+ export default ProjectChat;
components/ui/Button.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
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
+
57
+ export { Button, buttonVariants };
components/ui/Chip.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from '@/lib/utils';
2
+
3
+ export interface ChipProps {
4
+ label?: string;
5
+ value: string;
6
+ color?: 'gray' | 'blue' | 'yellow' | 'purple';
7
+ className?: string;
8
+ }
9
+
10
+ const Chip: React.FC<ChipProps> = ({
11
+ label,
12
+ value,
13
+ className,
14
+ color = 'gray',
15
+ }) => {
16
+ return (
17
+ <div
18
+ className={cn(
19
+ 'inline-flex items-center px-1.5 rounded-full text-xs mr-2 bg-gray-100 text-gray-500',
20
+ `bg-${color}-100 text-${color}-500`,
21
+ className,
22
+ )}
23
+ >
24
+ {label && <span className="font-medium">{label} :</span>}
25
+ <span>{value}</span>
26
+ </div>
27
+ );
28
+ };
29
+
30
+ export default Chip;
components/ui/CodeBlock.tsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Inspired by Chatbot-UI and modified to fit the needs of this project
2
+ // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
3
+
4
+ 'use client';
5
+
6
+ import { FC, memo } from 'react';
7
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
8
+ import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
9
+
10
+ import { useCopyToClipboard } from '@/lib/hooks/useCopyToClipboard';
11
+ import { IconCheck, IconCopy, IconDownload } from '@/components/ui/Icons';
12
+ import { Button } from '@/components/ui/Button';
13
+
14
+ interface Props {
15
+ language: string;
16
+ value: string;
17
+ }
18
+
19
+ interface languageMap {
20
+ [key: string]: string | undefined;
21
+ }
22
+
23
+ export const programmingLanguages: languageMap = {
24
+ javascript: '.js',
25
+ python: '.py',
26
+ java: '.java',
27
+ c: '.c',
28
+ cpp: '.cpp',
29
+ 'c++': '.cpp',
30
+ 'c#': '.cs',
31
+ ruby: '.rb',
32
+ php: '.php',
33
+ swift: '.swift',
34
+ 'objective-c': '.m',
35
+ kotlin: '.kt',
36
+ typescript: '.ts',
37
+ go: '.go',
38
+ perl: '.pl',
39
+ rust: '.rs',
40
+ scala: '.scala',
41
+ haskell: '.hs',
42
+ lua: '.lua',
43
+ shell: '.sh',
44
+ sql: '.sql',
45
+ html: '.html',
46
+ css: '.css',
47
+ // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48
+ };
49
+
50
+ export const generateRandomString = (length: number, lowercase = false) => {
51
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0
52
+ let result = '';
53
+ for (let i = 0; i < length; i++) {
54
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
55
+ }
56
+ return lowercase ? result.toLowerCase() : result;
57
+ };
58
+
59
+ const CodeBlock: FC<Props> = memo(({ language, value }) => {
60
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
61
+
62
+ const downloadAsFile = () => {
63
+ if (typeof window === 'undefined') {
64
+ return;
65
+ }
66
+ const fileExtension = programmingLanguages[language] || '.file';
67
+ const suggestedFileName = `file-${generateRandomString(
68
+ 3,
69
+ true,
70
+ )}${fileExtension}`;
71
+ const fileName = window.prompt('Enter file name' || '', suggestedFileName);
72
+
73
+ if (!fileName) {
74
+ // User pressed cancel on prompt.
75
+ return;
76
+ }
77
+
78
+ const blob = new Blob([value], { type: 'text/plain' });
79
+ const url = URL.createObjectURL(blob);
80
+ const link = document.createElement('a');
81
+ link.download = fileName;
82
+ link.href = url;
83
+ link.style.display = 'none';
84
+ document.body.appendChild(link);
85
+ link.click();
86
+ document.body.removeChild(link);
87
+ URL.revokeObjectURL(url);
88
+ };
89
+
90
+ const onCopy = () => {
91
+ if (isCopied) return;
92
+ copyToClipboard(value);
93
+ };
94
+
95
+ return (
96
+ <div className="relative w-full font-sans codeblock bg-zinc-950">
97
+ <div className="flex items-center justify-between w-full px-6 py-2 pr-4 bg-zinc-800 text-zinc-100">
98
+ <span className="text-xs lowercase">{language}</span>
99
+ <div className="flex items-center space-x-1">
100
+ <Button
101
+ variant="ghost"
102
+ className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
103
+ onClick={downloadAsFile}
104
+ size="icon"
105
+ >
106
+ <IconDownload />
107
+ <span className="sr-only">Download</span>
108
+ </Button>
109
+ <Button
110
+ variant="ghost"
111
+ size="icon"
112
+ className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
113
+ onClick={onCopy}
114
+ >
115
+ {isCopied ? <IconCheck /> : <IconCopy />}
116
+ <span className="sr-only">Copy code</span>
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ <SyntaxHighlighter
121
+ language={language}
122
+ style={coldarkDark}
123
+ PreTag="div"
124
+ showLineNumbers
125
+ customStyle={{
126
+ margin: 0,
127
+ width: '100%',
128
+ background: 'transparent',
129
+ padding: '1.5rem 1rem',
130
+ }}
131
+ lineNumberStyle={{
132
+ userSelect: 'none',
133
+ }}
134
+ codeTagProps={{
135
+ style: {
136
+ fontSize: '0.9rem',
137
+ fontFamily: 'var(--font-mono)',
138
+ },
139
+ }}
140
+ >
141
+ {value}
142
+ </SyntaxHighlighter>
143
+ </div>
144
+ );
145
+ });
146
+ CodeBlock.displayName = 'CodeBlock';
147
+
148
+ export { CodeBlock };
components/ui/DropdownMenu.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
5
+
6
+ import { cn } from '@/lib/utils';
7
+
8
+ const DropdownMenu = DropdownMenuPrimitive.Root;
9
+
10
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
11
+
12
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
13
+
14
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
15
+
16
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
17
+
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 ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
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 IconDiscord({ className, ...props }: React.ComponentProps<'svg'>) {
387
+ return (
388
+ <svg
389
+ width="800px"
390
+ height="800px"
391
+ viewBox="0 -28.5 256 256"
392
+ version="1.1"
393
+ xmlns="http://www.w3.org/2000/svg"
394
+ preserveAspectRatio="xMidYMid"
395
+ className={cn('size-4', className)}
396
+ {...props}
397
+ >
398
+ <g>
399
+ <path
400
+ d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z"
401
+ fill="#5865F2"
402
+ fillRule="nonzero"
403
+ ></path>
404
+ </g>
405
+ </svg>
406
+ );
407
+ }
408
+
409
+ function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
410
+ return (
411
+ <svg
412
+ xmlns="http://www.w3.org/2000/svg"
413
+ viewBox="0 0 256 256"
414
+ fill="currentColor"
415
+ className={cn('size-4', className)}
416
+ {...props}
417
+ >
418
+ <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" />
419
+ </svg>
420
+ );
421
+ }
422
+
423
+ function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
424
+ return (
425
+ <svg
426
+ xmlns="http://www.w3.org/2000/svg"
427
+ viewBox="0 0 256 256"
428
+ fill="currentColor"
429
+ className={cn('size-4', className)}
430
+ {...props}
431
+ >
432
+ <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" />
433
+ </svg>
434
+ );
435
+ }
436
+
437
+ function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
438
+ return (
439
+ <svg
440
+ xmlns="http://www.w3.org/2000/svg"
441
+ viewBox="0 0 256 256"
442
+ fill="currentColor"
443
+ className={cn('size-4', className)}
444
+ {...props}
445
+ >
446
+ <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" />
447
+ </svg>
448
+ );
449
+ }
450
+
451
+ function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
452
+ return (
453
+ <svg
454
+ xmlns="http://www.w3.org/2000/svg"
455
+ fill="none"
456
+ viewBox="0 0 24 24"
457
+ strokeWidth={1.5}
458
+ stroke="currentColor"
459
+ className={cn('size-4', className)}
460
+ {...props}
461
+ >
462
+ <path
463
+ strokeLinecap="round"
464
+ strokeLinejoin="round"
465
+ 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"
466
+ />
467
+ </svg>
468
+ );
469
+ }
470
+
471
+ function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
472
+ return (
473
+ <svg
474
+ xmlns="http://www.w3.org/2000/svg"
475
+ fill="currentColor"
476
+ className={cn('size-4', className)}
477
+ viewBox="0 0 256 256"
478
+ {...props}
479
+ >
480
+ <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" />
481
+ </svg>
482
+ );
483
+ }
484
+
485
+ function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
486
+ return (
487
+ <svg
488
+ xmlns="http://www.w3.org/2000/svg"
489
+ fill="currentColor"
490
+ className={cn('size-4', className)}
491
+ viewBox="0 0 256 256"
492
+ {...props}
493
+ >
494
+ <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" />
495
+ </svg>
496
+ );
497
+ }
498
+
499
+ function IconExternalLink({
500
+ className,
501
+ ...props
502
+ }: React.ComponentProps<'svg'>) {
503
+ return (
504
+ <svg
505
+ xmlns="http://www.w3.org/2000/svg"
506
+ fill="currentColor"
507
+ className={cn('size-4', className)}
508
+ viewBox="0 0 256 256"
509
+ {...props}
510
+ >
511
+ <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" />
512
+ </svg>
513
+ );
514
+ }
515
+
516
+ function IconChevronUpDown({
517
+ className,
518
+ ...props
519
+ }: React.ComponentProps<'svg'>) {
520
+ return (
521
+ <svg
522
+ xmlns="http://www.w3.org/2000/svg"
523
+ fill="currentColor"
524
+ className={cn('size-4', className)}
525
+ viewBox="0 0 256 256"
526
+ {...props}
527
+ >
528
+ <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" />
529
+ </svg>
530
+ );
531
+ }
532
+
533
+ function IconLoading({ className, ...props }: React.ComponentProps<'svg'>) {
534
+ return (
535
+ <svg
536
+ aria-hidden="true"
537
+ className="size-8 text-gray-200 animate-spin fill-gray-600"
538
+ viewBox="0 0 100 101"
539
+ fill="none"
540
+ xmlns="http://www.w3.org/2000/svg"
541
+ >
542
+ <path
543
+ 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"
544
+ fill="currentColor"
545
+ />
546
+ <path
547
+ 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"
548
+ fill="currentFill"
549
+ />
550
+ </svg>
551
+ );
552
+ }
553
+
554
+ export {
555
+ IconEdit,
556
+ IconNextChat,
557
+ IconOpenAI,
558
+ IconVercel,
559
+ IconGitHub,
560
+ IconSeparator,
561
+ IconArrowDown,
562
+ IconArrowRight,
563
+ IconUser,
564
+ IconPlus,
565
+ IconArrowElbow,
566
+ IconSpinner,
567
+ IconMessage,
568
+ IconTrash,
569
+ IconRefresh,
570
+ IconStop,
571
+ IconSidebar,
572
+ IconMoon,
573
+ IconSun,
574
+ IconCopy,
575
+ IconCheck,
576
+ IconDownload,
577
+ IconClose,
578
+ IconShare,
579
+ IconUsers,
580
+ IconExternalLink,
581
+ IconChevronUpDown,
582
+ IconGoogle,
583
+ IconLoading,
584
+ IconDiscord,
585
+ };