MingruiZhang commited on
Commit
92f037b
1 Parent(s): 3ebf44a

feat: Restruct Composer and Homepage (#64)

Browse files

![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/f1d7e03a-3c59-4aad-b328-de577d7ece99)

app/api/vision-agent/route.ts CHANGED
@@ -17,11 +17,10 @@ export const POST = withLogging(
17
  messages: MessageBase[];
18
  id: string;
19
  mediaUrl: string;
20
- enableSelfReflection: boolean;
21
  },
22
  request,
23
  ) => {
24
- const { messages, mediaUrl, enableSelfReflection } = json;
25
 
26
  // const session = await auth();
27
  // if (!session?.user?.email) {
@@ -56,7 +55,7 @@ export const POST = withLogging(
56
  formData.append('image', mediaUrl);
57
 
58
  const fetchResponse = await fetch(
59
- `https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&visualize_output=true&self_reflection=${enableSelfReflection}`,
60
  // `http://localhost:5001/v1/agent/chat?agent_class=vision_agent&self_reflection=${enableSelfReflection}`,
61
  {
62
  method: 'POST',
 
17
  messages: MessageBase[];
18
  id: string;
19
  mediaUrl: string;
 
20
  },
21
  request,
22
  ) => {
23
+ const { messages, mediaUrl } = json;
24
 
25
  // const session = await auth();
26
  // if (!session?.user?.email) {
 
55
  formData.append('image', mediaUrl);
56
 
57
  const fetchResponse = await fetch(
58
+ `https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent&self_reflection=false`,
59
  // `http://localhost:5001/v1/agent/chat?agent_class=vision_agent&self_reflection=${enableSelfReflection}`,
60
  {
61
  method: 'POST',
app/chat/page.tsx CHANGED
@@ -1,25 +1,14 @@
1
  'use client';
2
 
3
- import ImageSelector from '@/components/chat/ImageSelector';
4
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
5
- import { fetcher } from '@/lib/utils';
6
  import { useRouter } from 'next/navigation';
7
 
8
- import {
9
- Tooltip,
10
- TooltipContent,
11
- TooltipTrigger,
12
- } from '@/components/ui/Tooltip';
13
- import { IconDiscord, IconGitHub } from '@/components/ui/Icons';
14
- import Link from 'next/link';
15
- import { Button } from '@/components/ui/Button';
16
- import Img from '@/components/ui/Img';
17
  import { MessageRaw } from '@/lib/db/types';
18
- import { dbPostCreateChat } from '@/lib/db/functions';
19
  import { useState } from 'react';
20
- import Loading from '@/components/ui/Loading';
 
 
21
 
22
- // const EXAMPLE_URL = 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg';
23
  const EXAMPLE_URL =
24
  'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png';
25
  const EXAMPLE_HEADER = 'Count and find';
@@ -51,77 +40,35 @@ const exampleMessages = [
51
 
52
  export default function Page() {
53
  const router = useRouter();
54
- const [isUploading, setUploading] = useState<false | Number>(false);
55
  return (
56
- <div className="mx-auto max-w-2xl px-4 mt-8">
57
- <div className="rounded-lg border bg-background p-8 mb-6">
58
- <h1 className="mb-2 text-lg font-semibold">Welcome to Vision Agent</h1>
59
- <p>
60
- Vision Agent is a library that helps you utilize agent frameworks for
61
- your vision tasks. Vision Agent aims to provide an in-seconds
62
- experience by allowing users to describe their problem in text and
63
- utilizing agent frameworks to solve the task for them.
64
- </p>
65
- <div className="my-2">
66
- <Tooltip>
67
- <TooltipTrigger asChild>
68
- <Button variant="link" size="icon" asChild className="mr-2">
69
- <Link
70
- href="https://github.com/landing-ai/vision-agent"
71
- target="_blank"
72
- >
73
- <IconGitHub className="size-6" />
74
- </Link>
75
- </Button>
76
- </TooltipTrigger>
77
- <TooltipContent>Github</TooltipContent>
78
- </Tooltip>
79
- <Tooltip>
80
- <TooltipTrigger asChild>
81
- <Button variant="link" size="icon" asChild className="mr-2">
82
- <Link href="https://discord.gg/wZ2A7J69" target="_blank">
83
- <IconDiscord className="size-6" />
84
- </Link>
85
- </Button>
86
- </TooltipTrigger>
87
- <TooltipContent>Discord</TooltipContent>
88
- </Tooltip>
89
- </div>
90
- <ImageSelector />
91
- </div>
92
- <div className="mb-4 grid grid-cols-2 gap-2 px-4 sm:px-0">
93
- {exampleMessages.map((example, index) => (
94
- <div
95
- key={index}
96
- className={`relative 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 ${
97
- index > 1 && 'hidden md:block'
98
- }`}
99
- onClick={async () => {
100
- setUploading(index);
101
- const resp = await dbPostCreateChat({
102
- mediaUrl: example.url,
103
- initMessages: example.initMessages,
104
- title: example.heading,
105
- });
106
- setUploading(false);
107
- if (resp) {
108
- router.push(`/chat/${resp.id}`);
109
- }
110
- }}
111
- >
112
- {isUploading === index && (
113
- <div className="absolute top-0 left-0 size-full flex items-center justify-center bg-white/60">
114
- <Loading />
115
- </div>
116
- )}
117
- <Img src={example.url} alt="example images" className="w-1/4" />
118
- <div className="flex items-start flex-col h-full ml-3 w-3/4">
119
- <div className="text-sm font-semibold">{example.heading}</div>
120
- <div className="text-sm text-zinc-600">{example.subheading}</div>
121
- </div>
122
- </div>
123
- ))}
124
- </div>
125
  </div>
126
  );
127
  }
 
1
  'use client';
2
 
 
3
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
 
4
  import { useRouter } from 'next/navigation';
5
 
 
 
 
 
 
 
 
 
 
6
  import { MessageRaw } from '@/lib/db/types';
 
7
  import { useState } from 'react';
8
+ import { Composer } from '@/components/chat/Composer';
9
+ import { dbPostCreateChat } from '@/lib/db/functions';
10
+ import { nanoid } from '@/lib/utils';
11
 
 
12
  const EXAMPLE_URL =
13
  'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png';
14
  const EXAMPLE_HEADER = 'Count and find';
 
40
 
41
  export default function Page() {
42
  const router = useRouter();
 
43
  return (
44
+ <div className="mx-auto w-[1024px] max-w-full px-4 mt-[200px]">
45
+ <h1 className="mb-4 text-5xl text-center">Vision Agent</h1>
46
+ <h4 className="mb-8 text-center">
47
+ Generate code to solve your vision problem with simple prompts.
48
+ </h4>
49
+ <Composer
50
+ onSubmit={async ({ input, mediaUrl }) => {
51
+ const newId = nanoid();
52
+ const resp = await dbPostCreateChat({
53
+ id: newId,
54
+ mediaUrl: mediaUrl,
55
+ title: `conversation-${newId}`,
56
+ initMessages: [
57
+ {
58
+ role: 'user',
59
+ content:
60
+ input +
61
+ (mediaUrl
62
+ ? '\n\n' + generateInputImageMarkdown(mediaUrl)
63
+ : ''),
64
+ },
65
+ ],
66
+ });
67
+ if (resp) {
68
+ router.push(`/chat/${newId}`);
69
+ }
70
+ }}
71
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  </div>
73
  );
74
  }
app/layout.tsx CHANGED
@@ -53,7 +53,7 @@ export default function RootLayout(props: RootLayoutProps) {
53
  >
54
  <div className="flex flex-col min-h-screen">
55
  <Header />
56
- <main className="flex py-8 h-[calc(100vh-64px)] bg-muted/50 overflow-hidden relative">
57
  {children}
58
  </main>
59
  </div>
 
53
  >
54
  <div className="flex flex-col min-h-screen">
55
  <Header />
56
+ <main className="flex py-8 h-[calc(100vh-64px)] bg-background overflow-hidden relative">
57
  {children}
58
  </main>
59
  </div>
components/Header.tsx CHANGED
@@ -12,6 +12,12 @@ import LandingLogo from '@/assets/svg/LandingAI_white.svg';
12
  import ChatSelectServer from './ChatSelectServer';
13
  import Loading from './ui/Loading';
14
  import { Skeleton } from './ui/Skeleton';
 
 
 
 
 
 
15
 
16
  export async function Header() {
17
  const session = await auth();
@@ -64,6 +70,29 @@ export async function Header() {
64
  <Button variant="link" asChild className="mr-2">
65
  <Link href="/chat">New conversation</Link>
66
  </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  <IconSeparator className="size-6 text-muted-foreground/50" />
68
  <div className="flex items-center grow-0">
69
  {session?.user ? <UserMenu user={session!.user} /> : <LoginMenu />}
 
12
  import ChatSelectServer from './ChatSelectServer';
13
  import Loading from './ui/Loading';
14
  import { Skeleton } from './ui/Skeleton';
15
+ import {
16
+ Tooltip,
17
+ TooltipContent,
18
+ TooltipTrigger,
19
+ } from '@/components/ui/Tooltip';
20
+ import { IconDiscord, IconGitHub } from '@/components/ui/Icons';
21
 
22
  export async function Header() {
23
  const session = await auth();
 
70
  <Button variant="link" asChild className="mr-2">
71
  <Link href="/chat">New conversation</Link>
72
  </Button>
73
+ <Tooltip>
74
+ <TooltipTrigger asChild>
75
+ <Button variant="link" size="icon" asChild className="mr-2">
76
+ <Link
77
+ href="https://github.com/landing-ai/vision-agent"
78
+ target="_blank"
79
+ >
80
+ <IconGitHub className="size-5" />
81
+ </Link>
82
+ </Button>
83
+ </TooltipTrigger>
84
+ <TooltipContent>Github</TooltipContent>
85
+ </Tooltip>
86
+ <Tooltip>
87
+ <TooltipTrigger asChild>
88
+ <Button variant="link" size="icon" asChild className="mr-2">
89
+ <Link href="https://discord.gg/wZ2A7J69" target="_blank">
90
+ <IconDiscord className="size-5" />
91
+ </Link>
92
+ </Button>
93
+ </TooltipTrigger>
94
+ <TooltipContent>Discord</TooltipContent>
95
+ </Tooltip>
96
  <IconSeparator className="size-6 text-muted-foreground/50" />
97
  <div className="flex items-center grow-0">
98
  {session?.user ? <UserMenu user={session!.user} /> : <LoginMenu />}
components/chat/ChatClient.tsx CHANGED
@@ -5,9 +5,13 @@ import { Composer } from '@/components/chat/Composer';
5
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
  import { Session } from 'next-auth';
8
- import { useState } from 'react';
9
  import { ChatWithMessages } from '@/lib/db/types';
10
  import { ChatMessage } from './ChatMessage';
 
 
 
 
11
 
12
  export interface ChatClientProps {
13
  chat: ChatWithMessages;
@@ -15,15 +19,21 @@ export interface ChatClientProps {
15
 
16
  const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
17
  const { mediaUrl, id } = chat;
18
- const { messages, append, reload, stop, isLoading, input, setInput } =
19
- useVisionAgent(chat);
20
 
21
- const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
22
  useScrollAnchor();
23
 
 
 
 
 
 
 
 
24
  return (
25
  <div
26
- className="h-full overflow-auto mx-auto max-w-5xl min-w-3xl border rounded-lg"
27
  ref={scrollRef}
28
  >
29
  <div className="overflow-auto h-full pt-6 px-6" ref={messagesRef}>
@@ -36,21 +46,37 @@ const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
36
  isLoading={isLoading && index === messages.length - 1}
37
  />
38
  ))}
39
- <div className="h-px w-full" ref={visibilityRef} />
40
  </div>
41
- <div className="sticky bottom-3 w-full">
42
  <Composer
43
  id={id}
44
  mediaUrl={mediaUrl}
45
  isLoading={isLoading}
46
- stop={stop}
47
- append={append}
48
- reload={reload}
49
- messages={messages}
50
- input={input}
51
- setInput={setInput}
 
 
 
 
 
52
  />
53
  </div>
 
 
 
 
 
 
 
 
 
 
 
54
  </div>
55
  );
56
  };
 
5
  import useVisionAgent from '@/lib/hooks/useVisionAgent';
6
  import { useScrollAnchor } from '@/lib/hooks/useScrollAnchor';
7
  import { Session } from 'next-auth';
8
+ import { useEffect } from 'react';
9
  import { ChatWithMessages } from '@/lib/db/types';
10
  import { ChatMessage } from './ChatMessage';
11
+ import { Button } from '../ui/Button';
12
+ import { cn } from '@/lib/utils';
13
+ import { IconArrowDown } from '../ui/Icons';
14
+ import { generateInputImageMarkdown } from '@/lib/messageUtils';
15
 
16
  export interface ChatClientProps {
17
  chat: ChatWithMessages;
 
19
 
20
  const ChatClient: React.FC<ChatClientProps> = ({ chat }) => {
21
  const { mediaUrl, id } = chat;
22
+ const { messages, append, isLoading, reload } = useVisionAgent(chat);
 
23
 
24
+ const { messagesRef, scrollRef, visibilityRef, isVisible, scrollToBottom } =
25
  useScrollAnchor();
26
 
27
+ // Scroll to bottom when messages are loading
28
+ useEffect(() => {
29
+ if (isLoading && messages.length) {
30
+ scrollToBottom();
31
+ }
32
+ }, [isLoading, scrollToBottom, messages]);
33
+
34
  return (
35
  <div
36
+ className="h-full overflow-auto mx-auto w-[1024px] max-w-full border rounded-lg relative"
37
  ref={scrollRef}
38
  >
39
  <div className="overflow-auto h-full pt-6 px-6" ref={messagesRef}>
 
46
  isLoading={isLoading && index === messages.length - 1}
47
  />
48
  ))}
49
+ <div className="h-[108px] w-full" ref={visibilityRef} />
50
  </div>
51
+ <div className="absolute bottom-3 w-full">
52
  <Composer
53
  id={id}
54
  mediaUrl={mediaUrl}
55
  isLoading={isLoading}
56
+ onSubmit={async ({ input, mediaUrl: newMediaUrl }) => {
57
+ append({
58
+ id,
59
+ content:
60
+ input +
61
+ (newMediaUrl
62
+ ? '\n\n' + generateInputImageMarkdown(newMediaUrl)
63
+ : ''),
64
+ role: 'user',
65
+ });
66
+ }}
67
  />
68
  </div>
69
+ {/* Scroll to bottom Icon */}
70
+ <Button
71
+ size="icon"
72
+ className={cn(
73
+ 'absolute bottom-3 right-3 transition-opacity duration-300 size-6',
74
+ isVisible ? 'opacity-0' : 'opacity-100',
75
+ )}
76
+ onClick={() => scrollToBottom()}
77
+ >
78
+ <IconArrowDown className="size-3" />
79
+ </Button>
80
  </div>
81
  );
82
  };
components/chat/ChatMessage.tsx CHANGED
@@ -138,23 +138,18 @@ const Markdown: React.FC<{
138
  );
139
  };
140
 
141
- export function ChatMessage({
142
- message,
143
- isLoading,
144
- }: ChatMessageProps) {
145
  const { content } = useMemo(() => {
146
  return getCleanedUpMessages({
147
  content: message.content,
148
  role: message.role,
149
  });
150
  }, [message.content, message.role]);
151
- console.log('[Ming] content:', content);
152
- console.log('[Ming] raw:', message.content);
153
  const [details, setDetails] = useState<string>('');
154
  return (
155
  <div
156
  className={cn(
157
- 'group relative mb-6 flex rounded-md bg-muted px-4 py-6 w-3/5',
158
  message.role === 'user' ? 'ml-auto mr-0 w-3/5' : 'w-4/5',
159
  )}
160
  >
 
138
  );
139
  };
140
 
141
+ export function ChatMessage({ message, isLoading }: ChatMessageProps) {
 
 
 
142
  const { content } = useMemo(() => {
143
  return getCleanedUpMessages({
144
  content: message.content,
145
  role: message.role,
146
  });
147
  }, [message.content, message.role]);
 
 
148
  const [details, setDetails] = useState<string>('');
149
  return (
150
  <div
151
  className={cn(
152
+ 'group relative mb-6 flex rounded-md bg-muted p-4',
153
  message.role === 'user' ? 'ml-auto mr-0 w-3/5' : 'w-4/5',
154
  )}
155
  >
components/chat/Composer.tsx CHANGED
@@ -1,11 +1,8 @@
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 {
@@ -14,85 +11,126 @@ import {
14
  TooltipTrigger,
15
  } from '@/components/ui/Tooltip';
16
  import {
17
- IconArrowDown,
18
  IconArrowElbow,
19
  IconImage,
 
20
  IconRefresh,
21
  IconStop,
 
22
  } from '@/components/ui/Icons';
23
  import { cn } from '@/lib/utils';
24
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
25
  import { Switch } from '../ui/Switch';
26
  import Chip from '../ui/Chip';
 
 
27
 
28
- export interface ComposerProps
29
- extends Pick<
30
- UseChatHelpers,
31
- 'append' | 'isLoading' | 'reload' | 'stop' | 'input' | 'setInput'
32
- > {
33
  id?: string;
34
  title?: string;
35
- messages: MessageBase[];
36
  mediaUrl?: string;
37
  }
38
 
39
- export function Composer({
40
- id,
41
- isLoading,
42
- append,
43
- input,
44
- setInput,
45
- mediaUrl,
46
- // isAtBottom,
47
- }: ComposerProps) {
48
  const { formRef, onKeyDown } = useEnterSubmit();
49
- const inputRef = React.useRef<HTMLTextAreaElement>(null);
50
- React.useEffect(() => {
 
 
 
 
 
 
 
 
 
 
 
 
51
  if (inputRef.current) {
52
  inputRef.current.focus();
53
  }
54
  }, []);
55
 
56
- const mediaName = mediaUrl?.split('/').pop();
57
  return (
58
- <div className="size-full mx-auto max-w-2xl px-6 py-3 space-y-4 bg-zinc-700 rounded-xl relative shadow-lg shadow-zinc-800/40">
59
- {mediaUrl && (
60
- <Tooltip>
61
- <TooltipTrigger>
62
- <Chip>
63
- <div className="flex flex-row items-center">
64
- <IconImage className="size-3 mr-1" />
65
- <p>{mediaName ?? 'media(0)'}</p>
66
- </div>
67
- </Chip>
68
- </TooltipTrigger>
69
- <TooltipContent sideOffset={20}>
70
- <Img
71
- src={mediaUrl}
72
- className="m-1"
73
- quality={100}
74
- alt="zoomed-in-image"
75
- />
76
- </TooltipContent>
77
- </Tooltip>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  )}
79
  <form
80
  onSubmit={async e => {
81
  e.preventDefault();
82
- if (!input?.trim()) {
83
  return;
84
  }
85
- setInput('');
86
- await append({
87
- id,
88
- content:
89
- input +
90
- (mediaUrl ? '\n\n' + generateInputImageMarkdown(mediaUrl) : ''),
91
- role: 'user',
92
- });
93
  }}
94
  ref={formRef}
95
- className="h-full"
96
  >
97
  {/* <div className="border-gray-500 flex overflow-hidden size-full flex flex-row items-center"> */}
98
  <Textarea
@@ -101,78 +139,29 @@ export function Composer({
101
  onKeyDown={onKeyDown}
102
  rows={1}
103
  value={input}
104
- disabled={isLoading}
105
  onChange={e => setInput(e.target.value)}
106
- placeholder={isLoading ? '🤖 ✨ ...' : 'Message Vision Agent'}
 
 
107
  spellCheck={false}
108
- className="w-full grow resize-none bg-transparent focus-within:outline-none text-sm"
109
  />
110
- {/* Stop / Regenerate Icon */}
111
- {/* <div className="absolute bottom-14 right-4">
112
- {isLoading ? (
113
- <Tooltip>
114
- <TooltipTrigger asChild>
115
- <Button
116
- variant="outline"
117
- size="icon"
118
- className="bg-background"
119
- onClick={() => stop()}
120
- >
121
- <IconStop />
122
- </Button>
123
- </TooltipTrigger>
124
- <TooltipContent>Stop generating</TooltipContent>
125
- </Tooltip>
126
- ) : (
127
- messages?.length >= 2 && (
128
- <Tooltip>
129
- <TooltipTrigger asChild>
130
- <Button
131
- variant="outline"
132
- size="icon"
133
- className="bg-background"
134
- onClick={() => reload()}
135
- >
136
- <IconRefresh />
137
- </Button>
138
- </TooltipTrigger>
139
- <TooltipContent>Regenerate response</TooltipContent>
140
- </Tooltip>
141
- )
142
- )}
143
- </div> */}
144
- {/* </div> */}
145
  {/* Submit Icon */}
146
  <Tooltip>
147
  <TooltipTrigger asChild>
148
  <Button
149
  type="submit"
150
  size="icon"
151
- className="size-6 absolute bottom-3 right-3"
152
- disabled={isLoading || input === ''}
153
  >
154
- <IconArrowElbow className="size-3" />
155
  </Button>
156
  </TooltipTrigger>
157
- <TooltipContent>Send message</TooltipContent>
158
  </Tooltip>
159
  </form>
160
- {/* Scroll to bottom Icon */}
161
- {/* <Tooltip>
162
- <TooltipTrigger asChild>
163
- <Button
164
- size="icon"
165
- className={cn(
166
- 'absolute top-1 right-3 transition-opacity duration-300 size-6',
167
- isAtBottom ? 'opacity-0' : 'opacity-100',
168
- )}
169
- onClick={() => scrollToBottom()}
170
- >
171
- <IconArrowDown className="size-3" />
172
- </Button>
173
- </TooltipTrigger>
174
- <TooltipContent>Scroll to bottom</TooltipContent>
175
- </Tooltip> */}
176
  </div>
177
  );
178
  }
 
1
  'use client';
2
 
3
+ import { useState, useEffect, useRef } from 'react';
 
 
4
 
5
  import { Button } from '@/components/ui/Button';
 
6
  import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit';
7
  import Img from '../ui/Img';
8
  import {
 
11
  TooltipTrigger,
12
  } from '@/components/ui/Tooltip';
13
  import {
 
14
  IconArrowElbow,
15
  IconImage,
16
+ IconArrowUp,
17
  IconRefresh,
18
  IconStop,
19
+ IconClose,
20
  } from '@/components/ui/Icons';
21
  import { cn } from '@/lib/utils';
22
  import { generateInputImageMarkdown } from '@/lib/messageUtils';
23
  import { Switch } from '../ui/Switch';
24
  import Chip from '../ui/Chip';
25
+ import Textarea from 'react-textarea-autosize';
26
+ import useMediaUpload from '@/lib/hooks/useMediaUpload';
27
 
28
+ export interface ComposerProps {
29
+ onSubmit: (params: { input: string; mediaUrl: string }) => Promise<void>;
30
+ isLoading?: boolean;
 
 
31
  id?: string;
32
  title?: string;
 
33
  mediaUrl?: string;
34
  }
35
 
36
+ export function Composer({ id, isLoading, onSubmit, mediaUrl }: ComposerProps) {
 
 
 
 
 
 
 
 
37
  const { formRef, onKeyDown } = useEnterSubmit();
38
+ const inputRef = useRef<HTMLTextAreaElement>(null);
39
+ const [localMediaUrl, setLocalMediaUrl] = useState<string | undefined>(
40
+ mediaUrl,
41
+ );
42
+ // For local loading state such as submitting
43
+ const [localLoading, setLocalLoading] = useState<boolean>(false);
44
+ const [input, setInput] = useState('');
45
+ const noMediaValidation = !localMediaUrl && !!input;
46
+ const { getRootProps, getInputProps, isDragActive, isUploading, openUpload } =
47
+ useMediaUpload(uploadUrl => setLocalMediaUrl(uploadUrl));
48
+
49
+ const finalLoading = isLoading || isUploading || localLoading;
50
+
51
+ useEffect(() => {
52
  if (inputRef.current) {
53
  inputRef.current.focus();
54
  }
55
  }, []);
56
 
57
+ const mediaName = localMediaUrl?.split('/').pop();
58
  return (
59
+ <div
60
+ {...getRootProps()}
61
+ className={cn(
62
+ 'w-full mx-auto max-w-2xl px-6 py-4 bg-zinc-700 rounded-xl relative shadow-lg shadow-zinc-700/40',
63
+ isDragActive && 'bg-indigo-700/50',
64
+ )}
65
+ >
66
+ <input {...getInputProps()} />
67
+ <div
68
+ className={cn(
69
+ 'w-1/3 h-1 rounded-full overflow-hidden bg-zinc-700 absolute left-1/2 -translate-x-1/2',
70
+ finalLoading ? 'opacity-100' : 'opacity-0',
71
+ )}
72
+ >
73
+ <div className="h-full bg-primary animate-progress origin-left-right" />
74
+ </div>
75
+ {localMediaUrl ? (
76
+ <Chip className="mb-0.5">
77
+ <div className="flex flex-row items-center space-x-2">
78
+ <Tooltip>
79
+ <TooltipTrigger>
80
+ <div className="flex flex-row items-center space-x-2">
81
+ <IconImage className="size-3" />
82
+ <p>{mediaName ?? 'unnamed_media'}</p>
83
+ </div>
84
+ </TooltipTrigger>
85
+ <TooltipContent sideOffset={8}>
86
+ <Img
87
+ src={localMediaUrl}
88
+ className="m-1"
89
+ quality={100}
90
+ alt="zoomed-in-image"
91
+ />
92
+ </TooltipContent>
93
+ </Tooltip>
94
+ <Button
95
+ size="icon"
96
+ variant="ghost"
97
+ className="size-4"
98
+ onClick={() => setLocalMediaUrl(undefined)}
99
+ >
100
+ <IconClose className="size-3" />
101
+ </Button>
102
+ </div>
103
+ </Chip>
104
+ ) : (
105
+ <Button
106
+ variant="ghost"
107
+ size="sm"
108
+ className={cn(
109
+ 'ml-[-10px] border-2 border-transparent',
110
+ noMediaValidation && 'border-red-500/50 border-2 text-red-500',
111
+ )}
112
+ onClick={openUpload}
113
+ >
114
+ <IconImage className="mr-2 size-4" />
115
+ {noMediaValidation ? 'Select media (required)' : 'Select media'}
116
+ </Button>
117
  )}
118
  <form
119
  onSubmit={async e => {
120
  e.preventDefault();
121
+ if (!input?.trim() || !localMediaUrl) {
122
  return;
123
  }
124
+ setLocalLoading(true);
125
+ try {
126
+ await onSubmit({ input, mediaUrl: localMediaUrl });
127
+ } finally {
128
+ setLocalLoading(false);
129
+ setInput('');
130
+ }
 
131
  }}
132
  ref={formRef}
133
+ className="h-full mt-4"
134
  >
135
  {/* <div className="border-gray-500 flex overflow-hidden size-full flex flex-row items-center"> */}
136
  <Textarea
 
139
  onKeyDown={onKeyDown}
140
  rows={1}
141
  value={input}
142
+ disabled={finalLoading}
143
  onChange={e => setInput(e.target.value)}
144
+ placeholder={
145
+ finalLoading ? '🤖 Agent working ✨' : 'Message Vision Agent'
146
+ }
147
  spellCheck={false}
148
+ className="w-full grow resize-none bg-transparent focus-within:outline-none"
149
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  {/* Submit Icon */}
151
  <Tooltip>
152
  <TooltipTrigger asChild>
153
  <Button
154
  type="submit"
155
  size="icon"
156
+ className={cn('size-6 absolute bottom-3 right-3')}
157
+ disabled={finalLoading || input === '' || noMediaValidation}
158
  >
159
+ <IconArrowUp className="size-3" />
160
  </Button>
161
  </TooltipTrigger>
162
+ <TooltipContent>Message Vision Agent</TooltipContent>
163
  </Tooltip>
164
  </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  </div>
166
  );
167
  }
components/ui/Chip.tsx CHANGED
@@ -17,7 +17,7 @@ const Chip: React.FC<ChipProps> = ({
17
  return (
18
  <div
19
  className={cn(
20
- 'inline-flex items-center rounded-full text-xs mr-2 bg-gray-100 text-gray-500 px-2 py-0.5',
21
  `bg-${color}-100 text-${color}-500`,
22
  className,
23
  )}
 
17
  return (
18
  <div
19
  className={cn(
20
+ 'inline-flex items-center rounded-full text-xs mr-2 bg-gray-100 text-gray-500 px-2 py-1',
21
  `bg-${color}-100 text-${color}-500`,
22
  className,
23
  )}
components/ui/Icons.tsx CHANGED
@@ -128,6 +128,25 @@ function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
128
  );
129
  }
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
132
  return (
133
  <svg
@@ -519,15 +538,15 @@ function IconImage({ className, ...props }: React.ComponentProps<'svg'>) {
519
  <svg
520
  data-testid="geist-icon"
521
  height="16"
522
- stroke-linejoin="round"
523
  viewBox="0 0 16 16"
524
  width="16"
525
  className={cn('size-4', className)}
526
  {...props}
527
  >
528
  <path
529
- fill-rule="evenodd"
530
- clip-rule="evenodd"
531
  d="M14.5 2.5H1.5V9.18933L2.96966 7.71967L3.18933 7.5H3.49999H6.63001H6.93933L6.96966 7.46967L10.4697 3.96967L11.5303 3.96967L14.5 6.93934V2.5ZM8.00066 8.55999L9.53034 10.0897L10.0607 10.62L9.00001 11.6807L8.46968 11.1503L6.31935 9H3.81065L1.53032 11.2803L1.5 11.3106V12.5C1.5 13.0523 1.94772 13.5 2.5 13.5H13.5C14.0523 13.5 14.5 13.0523 14.5 12.5V9.06066L11 5.56066L8.03032 8.53033L8.00066 8.55999ZM4.05312e-06 10.8107V12.5C4.05312e-06 13.8807 1.11929 15 2.5 15H13.5C14.8807 15 16 13.8807 16 12.5V9.56066L16.5607 9L16.0303 8.46967L16 8.43934V2.5V1H14.5H1.5H4.05312e-06V2.5V10.6893L-0.0606689 10.75L4.05312e-06 10.8107Z"
532
  fill="currentColor"
533
  ></path>
@@ -542,6 +561,7 @@ export {
542
  IconSeparator,
543
  IconArrowDown,
544
  IconArrowRight,
 
545
  IconUser,
546
  IconPlus,
547
  IconArrowElbow,
 
128
  );
129
  }
130
 
131
+ function IconArrowUp({ className, ...props }: React.ComponentProps<'svg'>) {
132
+ return (
133
+ <svg
134
+ height="16"
135
+ strokeLinejoin="round"
136
+ viewBox="0 0 16 16"
137
+ className={cn('size-4', className)}
138
+ {...props}
139
+ >
140
+ <path
141
+ fillRule="evenodd"
142
+ clipRule="evenodd"
143
+ d="M8.70711 1.39644C8.31659 1.00592 7.68342 1.00592 7.2929 1.39644L2.21968 6.46966L1.68935 6.99999L2.75001 8.06065L3.28034 7.53032L7.25001 3.56065V14.25V15H8.75001V14.25V3.56065L12.7197 7.53032L13.25 8.06065L14.3107 6.99999L13.7803 6.46966L8.70711 1.39644Z"
144
+ fill="currentColor"
145
+ ></path>
146
+ </svg>
147
+ );
148
+ }
149
+
150
  function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
151
  return (
152
  <svg
 
538
  <svg
539
  data-testid="geist-icon"
540
  height="16"
541
+ strokeLinejoin="round"
542
  viewBox="0 0 16 16"
543
  width="16"
544
  className={cn('size-4', className)}
545
  {...props}
546
  >
547
  <path
548
+ fillRule="evenodd"
549
+ clipRule="evenodd"
550
  d="M14.5 2.5H1.5V9.18933L2.96966 7.71967L3.18933 7.5H3.49999H6.63001H6.93933L6.96966 7.46967L10.4697 3.96967L11.5303 3.96967L14.5 6.93934V2.5ZM8.00066 8.55999L9.53034 10.0897L10.0607 10.62L9.00001 11.6807L8.46968 11.1503L6.31935 9H3.81065L1.53032 11.2803L1.5 11.3106V12.5C1.5 13.0523 1.94772 13.5 2.5 13.5H13.5C14.0523 13.5 14.5 13.0523 14.5 12.5V9.06066L11 5.56066L8.03032 8.53033L8.00066 8.55999ZM4.05312e-06 10.8107V12.5C4.05312e-06 13.8807 1.11929 15 2.5 15H13.5C14.8807 15 16 13.8807 16 12.5V9.56066L16.5607 9L16.0303 8.46967L16 8.43934V2.5V1H14.5H1.5H4.05312e-06V2.5V10.6893L-0.0606689 10.75L4.05312e-06 10.8107Z"
551
  fill="currentColor"
552
  ></path>
 
561
  IconSeparator,
562
  IconArrowDown,
563
  IconArrowRight,
564
+ IconArrowUp,
565
  IconUser,
566
  IconPlus,
567
  IconArrowElbow,
components/ui/Tooltip.tsx CHANGED
@@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
19
  ref={ref}
20
  sideOffset={sideOffset}
21
  className={cn(
22
- 'z-50 overflow-hidden rounded-md bg-muted px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 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',
23
  className,
24
  )}
25
  {...props}
 
19
  ref={ref}
20
  sideOffset={sideOffset}
21
  className={cn(
22
+ 'z-50 overflow-hidden rounded-md bg-muted px-3 py-1.5 text-xs text-primary animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 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',
23
  className,
24
  )}
25
  {...props}
lib/db/functions.ts CHANGED
@@ -150,7 +150,7 @@ export async function dbPostCreateMessage(chatId: string, message: MessageRaw) {
150
  }
151
  : {};
152
 
153
- return prisma.message.create({
154
  data: {
155
  content: message.content,
156
  role: message.role,
@@ -160,6 +160,8 @@ export async function dbPostCreateMessage(chatId: string, message: MessageRaw) {
160
  ...userConnect,
161
  },
162
  });
 
 
163
  }
164
 
165
  export async function dbDeleteChat(chatId: string) {
 
150
  }
151
  : {};
152
 
153
+ await prisma.message.create({
154
  data: {
155
  content: message.content,
156
  role: message.role,
 
160
  ...userConnect,
161
  },
162
  });
163
+
164
+ revalidatePath('/chat');
165
  }
166
 
167
  export async function dbDeleteChat(chatId: string) {
lib/hooks/useImageUpload.ts DELETED
@@ -1,38 +0,0 @@
1
- import { DropzoneOptions, useDropzone } from 'react-dropzone';
2
- // import { toast } from 'react-hot-toast';
3
-
4
- const useImageUpload = (
5
- options?: Partial<DropzoneOptions>,
6
- onDrop?: (files: File[]) => void,
7
- ) => {
8
- const { getRootProps, getInputProps, isDragActive } = useDropzone({
9
- accept: {
10
- 'image/*': ['.jpeg', '.png'],
11
- 'video/mp4': ['.mp4', '.MP4'],
12
- },
13
- multiple: false,
14
- onDrop: onDrop
15
- ? onDrop
16
- : acceptedFiles => {
17
- // if (acceptedFiles.length > 10) {
18
- // toast('You can only upload 10 images max.', {
19
- // icon: '⚠️',
20
- // });
21
- // }
22
- acceptedFiles.forEach(file => {
23
- try {
24
- const reader = new FileReader();
25
- reader.onloadend = () => {};
26
- reader.readAsDataURL(file);
27
- } catch (err) {
28
- console.error(err);
29
- }
30
- });
31
- },
32
- ...options,
33
- });
34
-
35
- return { getRootProps, getInputProps, isDragActive };
36
- };
37
-
38
- export default useImageUpload;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/chat/ImageSelector.tsx → lib/hooks/useMediaUpload.ts RENAMED
@@ -1,28 +1,17 @@
1
- 'use client';
2
-
3
- import React, { useCallback, useState } from 'react';
4
- import useImageUpload from '../../lib/hooks/useImageUpload';
5
- import { cn, fetcher } from '@/lib/utils';
6
- import { SignedPayload, MessageBase } from '@/lib/types';
7
- import { useRouter } from 'next/navigation';
8
- import Loading from '../ui/Loading';
9
- import toast from 'react-hot-toast';
10
  import {
11
  generateVideoThumbnails,
12
  getVideoDurationFromVideoFile,
13
  } from '@rajesh896/video-thumbnails-generator';
14
- import { ChatWithMessages } from '@/lib/db/types';
15
- import { dbPostCreateChat } from '@/lib/db/functions';
16
-
17
- export interface ImageSelectorProps {}
18
-
19
- type Example = {
20
- url: string;
21
- initMessages: MessageBase[];
22
- };
23
 
24
- const ImageSelector: React.FC<ImageSelectorProps> = () => {
25
- const router = useRouter();
 
 
26
  const [isUploading, setUploading] = useState(false);
27
 
28
  const upload = useCallback(async (file: File, chatId?: string) => {
@@ -57,10 +46,15 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
57
  };
58
  }, []);
59
 
60
- const { getRootProps, getInputProps, isDragActive } = useImageUpload(
61
- undefined,
62
- async files => {
63
- const formData = new FormData();
 
 
 
 
 
64
  if (files.length !== 1) {
65
  throw new Error('Only one image can be uploaded at a time');
66
  }
@@ -100,33 +94,20 @@ const ImageSelector: React.FC<ImageSelectorProps> = () => {
100
  return upload(thumbnailFile, resp.id);
101
  });
102
  }
103
- await dbPostCreateChat({
104
- id: resp.id,
105
- mediaUrl: resp.publicUrl,
106
- });
107
  setUploading(false);
108
- router.push(`/chat/${resp.id}`);
109
  };
110
  },
111
- );
112
- return (
113
- <div
114
- {...getRootProps()}
115
- className={cn(
116
- 'dropzone border-2 border-dashed border-gray-400 w-full h-64 flex items-center justify-center rounded-lg mt-4 cursor-pointer',
117
- isDragActive && 'bg-gray-500/50 border-solid',
118
- )}
119
- >
120
- <input {...getInputProps()} />
121
- <div className="text-gray-400 text-md">
122
- {isUploading ? (
123
- <Loading />
124
- ) : (
125
- 'Start using Vision Agent by selecting an image'
126
- )}
127
- </div>
128
- </div>
129
- );
130
  };
131
 
132
- export default ImageSelector;
 
 
 
 
 
 
 
 
 
 
1
  import {
2
  generateVideoThumbnails,
3
  getVideoDurationFromVideoFile,
4
  } from '@rajesh896/video-thumbnails-generator';
5
+ import { useCallback, useState } from 'react';
6
+ import { DropzoneOptions, useDropzone } from 'react-dropzone';
7
+ import { toast } from 'react-hot-toast';
8
+ import { fetcher } from '../utils';
9
+ import { SignedPayload } from '../types';
 
 
 
 
10
 
11
+ const useMediaUpload = (
12
+ onUpload: (uploadUrl: string) => void,
13
+ options?: Partial<DropzoneOptions>,
14
+ ) => {
15
  const [isUploading, setUploading] = useState(false);
16
 
17
  const upload = useCallback(async (file: File, chatId?: string) => {
 
46
  };
47
  }, []);
48
 
49
+ const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
50
+ accept: {
51
+ 'image/*': ['.jpeg', '.png'],
52
+ 'video/mp4': ['.mp4', '.MP4'],
53
+ },
54
+ noClick: true,
55
+ noKeyboard: true,
56
+ multiple: false,
57
+ onDrop: async files => {
58
  if (files.length !== 1) {
59
  throw new Error('Only one image can be uploaded at a time');
60
  }
 
94
  return upload(thumbnailFile, resp.id);
95
  });
96
  }
97
+ onUpload(resp.publicUrl);
 
 
 
98
  setUploading(false);
 
99
  };
100
  },
101
+ ...options,
102
+ });
103
+
104
+ return {
105
+ getRootProps,
106
+ getInputProps,
107
+ isDragActive,
108
+ isUploading,
109
+ openUpload: open,
110
+ };
 
 
 
 
 
 
 
 
 
111
  };
112
 
113
+ export default useMediaUpload;
lib/hooks/useScrollAnchor.tsx CHANGED
@@ -9,8 +9,8 @@ export const useScrollAnchor = () => {
9
  const [isVisible, setIsVisible] = useState(false);
10
 
11
  const scrollToBottom = useCallback(() => {
12
- if (messagesRef.current) {
13
- messagesRef.current.scrollIntoView({
14
  block: 'end',
15
  behavior: 'smooth',
16
  });
@@ -72,7 +72,7 @@ export const useScrollAnchor = () => {
72
  });
73
  },
74
  {
75
- rootMargin: '0px 0px -150px 0px',
76
  },
77
  );
78
 
@@ -89,6 +89,6 @@ export const useScrollAnchor = () => {
89
  scrollRef,
90
  visibilityRef,
91
  scrollToBottom,
92
- isAtBottom: isVisible,
93
  };
94
  };
 
9
  const [isVisible, setIsVisible] = useState(false);
10
 
11
  const scrollToBottom = useCallback(() => {
12
+ if (visibilityRef.current) {
13
+ visibilityRef.current.scrollIntoView({
14
  block: 'end',
15
  behavior: 'smooth',
16
  });
 
72
  });
73
  },
74
  {
75
+ rootMargin: '0px 0px -108px 0px',
76
  },
77
  );
78
 
 
89
  scrollRef,
90
  visibilityRef,
91
  scrollToBottom,
92
+ isVisible,
93
  };
94
  };
lib/hooks/useVisionAgent.ts CHANGED
@@ -53,21 +53,16 @@ const uploadBase64 = async (
53
 
54
  const useVisionAgent = (chat: ChatWithMessages) => {
55
  const { messages: initialMessages, id, mediaUrl } = chat;
56
- const searchParams = useSearchParams();
57
- const reflectionValue = searchParams.get('reflection');
58
 
59
  const {
60
  messages,
61
  append: appendRaw,
62
- reload,
63
- stop,
64
  isLoading,
65
- input,
66
- setInput,
67
- setMessages,
68
- error,
69
  } = useChat({
70
  api: '/api/vision-agent',
 
 
71
  onResponse(response) {
72
  if (response.status !== 200) {
73
  toast.error(response.statusText);
@@ -83,22 +78,14 @@ const useVisionAgent = (chat: ChatWithMessages) => {
83
  body: {
84
  mediaUrl,
85
  id,
86
- enableSelfReflection: reflectionValue === 'true',
87
  },
88
  });
89
 
90
  /**
91
- * If the last message is from the user, reload the chat, this would trigger to get the response from the assistant
92
- * There are 2 scenarios when this might happen
93
- * 1. Navigated from example images, init message only include preset user message
94
- * 2. Last time the assistant message failed or not saved to database.
95
  */
96
  useEffect(() => {
97
- if (
98
- !isLoading &&
99
- messages.length &&
100
- messages[messages.length - 1].role === 'user'
101
- ) {
102
  reload();
103
  }
104
  }, [isLoading, messages, reload]);
@@ -115,10 +102,7 @@ const useVisionAgent = (chat: ChatWithMessages) => {
115
  messages: messages as MessageBase[],
116
  append,
117
  reload,
118
- stop,
119
  isLoading,
120
- input,
121
- setInput,
122
  };
123
  };
124
 
 
53
 
54
  const useVisionAgent = (chat: ChatWithMessages) => {
55
  const { messages: initialMessages, id, mediaUrl } = chat;
 
 
56
 
57
  const {
58
  messages,
59
  append: appendRaw,
 
 
60
  isLoading,
61
+ reload,
 
 
 
62
  } = useChat({
63
  api: '/api/vision-agent',
64
+ // @ts-ignore https://sdk.vercel.ai/docs/troubleshooting/common-issues/use-chat-failed-to-parse-stream
65
+ streamMode: 'text',
66
  onResponse(response) {
67
  if (response.status !== 200) {
68
  toast.error(response.statusText);
 
78
  body: {
79
  mediaUrl,
80
  id,
 
81
  },
82
  });
83
 
84
  /**
85
+ * If case this is first time user navigated with init message, we need to reload the chat for the first response
 
 
 
86
  */
87
  useEffect(() => {
88
+ if (!isLoading && messages.length === 1 && messages[0].role === 'user') {
 
 
 
 
89
  reload();
90
  }
91
  }, [isLoading, messages, reload]);
 
102
  messages: messages as MessageBase[],
103
  append,
104
  reload,
 
105
  isLoading,
 
 
106
  };
107
  };
108
 
package.json CHANGED
@@ -28,7 +28,7 @@
28
  "@radix-ui/react-tooltip": "^1.0.7",
29
  "@rajesh896/video-thumbnails-generator": "^2.3.9",
30
  "@vercel/kv": "^1.0.1",
31
- "ai": "^2.2.31",
32
  "class-variance-authority": "^0.7.0",
33
  "clsx": "^2.1.0",
34
  "date-fns": "^3.6.0",
 
28
  "@radix-ui/react-tooltip": "^1.0.7",
29
  "@rajesh896/video-thumbnails-generator": "^2.3.9",
30
  "@vercel/kv": "^1.0.1",
31
+ "ai": "^3.1.12",
32
  "class-variance-authority": "^0.7.0",
33
  "clsx": "^2.1.0",
34
  "date-fns": "^3.6.0",
pnpm-lock.yaml CHANGED
@@ -51,8 +51,8 @@ importers:
51
  specifier: ^1.0.1
52
  version: 1.0.1
53
  ai:
54
- specifier: ^2.2.31
55
56
  class-variance-authority:
57
  specifier: ^0.7.0
58
  version: 0.7.0
@@ -214,6 +214,19 @@ packages:
214
  resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
215
  engines: {node: '>=0.10.0'}
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  '@alloc/[email protected]':
218
  resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
219
  engines: {node: '>=10'}
@@ -1309,6 +1322,9 @@ packages:
1309
  '@types/[email protected]':
1310
  resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
1311
 
 
 
 
1312
  '@types/[email protected]':
1313
  resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
1314
 
@@ -1463,15 +1479,19 @@ packages:
1463
  resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
1464
  engines: {node: '>= 8.0.0'}
1465
 
1466
- ai@2.2.37:
1467
- resolution: {integrity: sha512-JIYm5N1muGVqBqWnvkt29FmXhESoO5TcDxw74OE41SsM+uIou6NPDDs0XWb/ABcd1gmp6k5zym64KWMPM2xm0A==}
1468
- engines: {node: '>=14.6'}
1469
  peerDependencies:
1470
- react: ^18.2.0
 
1471
  solid-js: ^1.7.7
1472
  svelte: ^3.0.0 || ^4.0.0
1473
  vue: ^3.3.4
 
1474
  peerDependenciesMeta:
 
 
1475
  react:
1476
  optional: true
1477
  solid-js:
@@ -1480,6 +1500,8 @@ packages:
1480
  optional: true
1481
  vue:
1482
  optional: true
 
 
1483
 
1484
1485
  resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1661,6 +1683,10 @@ packages:
1661
  resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
1662
  engines: {node: '>=10'}
1663
 
 
 
 
 
1664
1665
  resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
1666
 
@@ -1837,6 +1863,9 @@ packages:
1837
1838
  resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
1839
 
 
 
 
1840
1841
  resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
1842
  engines: {node: '>=0.3.1'}
@@ -2049,8 +2078,8 @@ packages:
2049
  resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
2050
  engines: {node: '>=0.8.x'}
2051
 
2052
- eventsource-parser@1.0.0:
2053
- resolution: {integrity: sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==}
2054
  engines: {node: '>=14.18'}
2055
 
2056
@@ -2524,6 +2553,9 @@ packages:
2524
2525
  resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
2526
 
 
 
 
2527
2528
  resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
2529
 
@@ -2531,6 +2563,11 @@ packages:
2531
  resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
2532
  hasBin: true
2533
 
 
 
 
 
 
2534
2535
  resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
2536
  engines: {node: '>=4.0'}
@@ -3357,6 +3394,9 @@ packages:
3357
3358
  resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
3359
 
 
 
 
3360
3361
  resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
3362
  hasBin: true
@@ -3837,6 +3877,14 @@ packages:
3837
  resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
3838
  engines: {node: '>=10'}
3839
 
 
 
 
 
 
 
 
 
3840
3841
  resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
3842
 
@@ -3844,6 +3892,19 @@ snapshots:
3844
 
3845
  '@aashutoshrathi/[email protected]': {}
3846
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3847
  '@alloc/[email protected]': {}
3848
 
3849
  '@ampproject/[email protected]':
@@ -5454,6 +5515,8 @@ snapshots:
5454
  dependencies:
5455
  '@types/ms': 0.7.34
5456
 
 
 
5457
  '@types/[email protected]': {}
5458
 
5459
  '@types/[email protected]':
@@ -5643,20 +5706,28 @@ snapshots:
5643
  dependencies:
5644
  humanize-ms: 1.2.1
5645
 
5646
5647
  dependencies:
5648
- eventsource-parser: 1.0.0
 
 
 
 
5649
  nanoid: 3.3.6
 
5650
  solid-swr-store: 0.10.7([email protected])([email protected])
5651
  sswr: 2.0.0([email protected])
5652
  swr: 2.2.0([email protected])
5653
  swr-store: 0.10.6
5654
 
5655
  optionalDependencies:
 
5656
  react: 18.2.0
5657
  solid-js: 1.8.16
5658
  svelte: 4.2.15
5659
  vue: 3.4.23([email protected])
 
5660
 
5661
5662
  dependencies:
@@ -5872,6 +5943,8 @@ snapshots:
5872
  ansi-styles: 4.3.0
5873
  supports-color: 7.2.0
5874
 
 
 
5875
5876
 
5877
@@ -6033,6 +6106,8 @@ snapshots:
6033
 
6034
6035
 
 
 
6036
6037
 
6038
@@ -6381,7 +6456,7 @@ snapshots:
6381
 
6382
6383
 
6384
- eventsource-parser@1.0.0: {}
6385
 
6386
6387
 
@@ -6888,12 +6963,20 @@ snapshots:
6888
 
6889
6890
 
 
 
6891
6892
 
6893
6894
  dependencies:
6895
  minimist: 1.2.8
6896
 
 
 
 
 
 
 
6897
6898
  dependencies:
6899
  array-includes: 3.1.8
@@ -7930,6 +8013,8 @@ snapshots:
7930
  dependencies:
7931
  loose-envify: 1.4.0
7932
 
 
 
7933
7934
 
7935
@@ -8517,4 +8602,10 @@ snapshots:
8517
 
8518
8519
 
 
 
 
 
 
 
8520
 
51
  specifier: ^1.0.1
52
  version: 1.0.1
53
  ai:
54
+ specifier: ^3.1.12
55
56
  class-variance-authority:
57
  specifier: ^0.7.0
58
  version: 0.7.0
 
214
  resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
215
  engines: {node: '>=0.10.0'}
216
 
217
+ '@ai-sdk/[email protected]':
218
+ resolution: {integrity: sha512-JRDrqL2FGDmLh+a4R5qbS8UrWN9Lt7DpDIY1x6owgXjXkz3Umm1czs1X32VlL0M1dpoSxu4hGBFtXd56+kDzXA==}
219
+ engines: {node: '>=18'}
220
+ peerDependencies:
221
+ zod: ^3.0.0
222
+ peerDependenciesMeta:
223
+ zod:
224
+ optional: true
225
+
226
+ '@ai-sdk/[email protected]':
227
+ resolution: {integrity: sha512-+gcMvyPUDfDXV9caN3CG5Le0M5K4CjqTdMV1ODg/AosApQiJW9ByN5imJPdI043zVdt+HS9WG+s0j4am7ca4bg==}
228
+ engines: {node: '>=18'}
229
+
230
  '@alloc/[email protected]':
231
  resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
232
  engines: {node: '>=10'}
 
1322
  '@types/[email protected]':
1323
  resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
1324
 
1325
+ '@types/[email protected]':
1326
+ resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
1327
+
1328
  '@types/[email protected]':
1329
  resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
1330
 
 
1479
  resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
1480
  engines: {node: '>= 8.0.0'}
1481
 
1482
+ ai@3.1.22:
1483
+ resolution: {integrity: sha512-Vgy490Q6p6pZ39VrRzL9ovr2N1YPsR+KvWNs+n73VAQoGBZtU/vwiiWrFU9LHXGhB9X+EBfQD0vixnTDS2dJWA==}
1484
+ engines: {node: '>=18'}
1485
  peerDependencies:
1486
+ openai: ^4.42.0
1487
+ react: ^18 || ^19
1488
  solid-js: ^1.7.7
1489
  svelte: ^3.0.0 || ^4.0.0
1490
  vue: ^3.3.4
1491
+ zod: ^3.0.0
1492
  peerDependenciesMeta:
1493
+ openai:
1494
+ optional: true
1495
  react:
1496
  optional: true
1497
  solid-js:
 
1500
  optional: true
1501
  vue:
1502
  optional: true
1503
+ zod:
1504
+ optional: true
1505
 
1506
1507
  resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
 
1683
  resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
1684
  engines: {node: '>=10'}
1685
 
1686
1687
+ resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
1688
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
1689
+
1690
1691
  resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
1692
 
 
1863
1864
  resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
1865
 
1866
1867
+ resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
1868
+
1869
1870
  resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
1871
  engines: {node: '>=0.3.1'}
 
2078
  resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
2079
  engines: {node: '>=0.8.x'}
2080
 
2081
+ eventsource-parser@1.1.2:
2082
+ resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==}
2083
  engines: {node: '>=14.18'}
2084
 
2085
 
2553
2554
  resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
2555
 
2556
2557
+ resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
2558
+
2559
2560
  resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
2561
 
 
2563
  resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
2564
  hasBin: true
2565
 
2566
2567
+ resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
2568
+ engines: {node: ^18.0.0 || >=20.0.0}
2569
+ hasBin: true
2570
+
2571
2572
  resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
2573
  engines: {node: '>=4.0'}
 
3394
3395
  resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
3396
 
3397
3398
+ resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
3399
+
3400
3401
  resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
3402
  hasBin: true
 
3877
  resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
3878
  engines: {node: '>=10'}
3879
 
3880
3881
+ resolution: {integrity: sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q==}
3882
+ peerDependencies:
3883
+ zod: ^3.22.4
3884
+
3885
3886
+ resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
3887
+
3888
3889
  resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
3890
 
 
3892
 
3893
  '@aashutoshrathi/[email protected]': {}
3894
 
3895
3896
+ dependencies:
3897
+ '@ai-sdk/provider': 0.0.8
3898
+ eventsource-parser: 1.1.2
3899
+ nanoid: 3.3.6
3900
+ secure-json-parse: 2.7.0
3901
+ optionalDependencies:
3902
+ zod: 3.23.8
3903
+
3904
+ '@ai-sdk/[email protected]':
3905
+ dependencies:
3906
+ json-schema: 0.4.0
3907
+
3908
  '@alloc/[email protected]': {}
3909
 
3910
  '@ampproject/[email protected]':
 
5515
  dependencies:
5516
  '@types/ms': 0.7.34
5517
 
5518
+ '@types/[email protected]': {}
5519
+
5520
  '@types/[email protected]': {}
5521
 
5522
  '@types/[email protected]':
 
5706
  dependencies:
5707
  humanize-ms: 1.2.1
5708
 
5709
5710
  dependencies:
5711
+ '@ai-sdk/provider': 0.0.8
5712
+ '@ai-sdk/provider-utils': 0.0.11([email protected])
5713
+ eventsource-parser: 1.1.2
5714
+ json-schema: 0.4.0
5715
+ jsondiffpatch: 0.6.0
5716
  nanoid: 3.3.6
5717
+ secure-json-parse: 2.7.0
5718
  solid-swr-store: 0.10.7([email protected])([email protected])
5719
  sswr: 2.0.0([email protected])
5720
  swr: 2.2.0([email protected])
5721
  swr-store: 0.10.6
5722
5723
+ zod-to-json-schema: 3.22.5([email protected])
5724
  optionalDependencies:
5725
+ openai: 4.38.1
5726
  react: 18.2.0
5727
  solid-js: 1.8.16
5728
  svelte: 4.2.15
5729
  vue: 3.4.23([email protected])
5730
+ zod: 3.23.8
5731
 
5732
5733
  dependencies:
 
5943
  ansi-styles: 4.3.0
5944
  supports-color: 7.2.0
5945
 
5946
5947
+
5948
5949
 
5950
 
6106
 
6107
6108
 
6109
6110
+
6111
6112
 
6113
 
6456
 
6457
6458
 
6459
+ eventsource-parser@1.1.2: {}
6460
 
6461
6462
 
 
6963
 
6964
6965
 
6966
6967
+
6968
6969
 
6970
6971
  dependencies:
6972
  minimist: 1.2.8
6973
 
6974
6975
+ dependencies:
6976
+ '@types/diff-match-patch': 1.0.36
6977
+ chalk: 5.3.0
6978
+ diff-match-patch: 1.0.5
6979
+
6980
6981
  dependencies:
6982
  array-includes: 3.1.8
 
8013
  dependencies:
8014
  loose-envify: 1.4.0
8015
 
8016
8017
+
8018
8019
 
8020
 
8602
 
8603
8604
 
8605
8606
+ dependencies:
8607
+ zod: 3.23.8
8608
+
8609
8610
+
8611
tailwind.config.ts CHANGED
@@ -1,80 +1,89 @@
1
- import type { Config } from "tailwindcss"
2
 
3
  const config = {
4
- darkMode: ["class"],
5
  content: [
6
  './pages/**/*.{ts,tsx}',
7
  './components/**/*.{ts,tsx}',
8
  './app/**/*.{ts,tsx}',
9
  './src/**/*.{ts,tsx}',
10
- ],
11
- prefix: "",
12
  theme: {
13
  container: {
14
  center: true,
15
- padding: "2rem",
16
  screens: {
17
- "2xl": "1400px",
18
  },
19
  },
20
  extend: {
21
  colors: {
22
- border: "hsl(var(--border))",
23
- input: "hsl(var(--input))",
24
- ring: "hsl(var(--ring))",
25
- background: "hsl(var(--background))",
26
- foreground: "hsl(var(--foreground))",
27
  primary: {
28
- DEFAULT: "hsl(var(--primary))",
29
- foreground: "hsl(var(--primary-foreground))",
30
  },
31
  secondary: {
32
- DEFAULT: "hsl(var(--secondary))",
33
- foreground: "hsl(var(--secondary-foreground))",
34
  },
35
  destructive: {
36
- DEFAULT: "hsl(var(--destructive))",
37
- foreground: "hsl(var(--destructive-foreground))",
38
  },
39
  muted: {
40
- DEFAULT: "hsl(var(--muted))",
41
- foreground: "hsl(var(--muted-foreground))",
42
  },
43
  accent: {
44
- DEFAULT: "hsl(var(--accent))",
45
- foreground: "hsl(var(--accent-foreground))",
46
  },
47
  popover: {
48
- DEFAULT: "hsl(var(--popover))",
49
- foreground: "hsl(var(--popover-foreground))",
50
  },
51
  card: {
52
- DEFAULT: "hsl(var(--card))",
53
- foreground: "hsl(var(--card-foreground))",
54
  },
55
  },
56
  borderRadius: {
57
- lg: "var(--radius)",
58
- md: "calc(var(--radius) - 2px)",
59
- sm: "calc(var(--radius) - 4px)",
60
  },
61
  keyframes: {
62
- "accordion-down": {
63
- from: { height: "0" },
64
- to: { height: "var(--radix-accordion-content-height)" },
65
  },
66
- "accordion-up": {
67
- from: { height: "var(--radix-accordion-content-height)" },
68
- to: { height: "0" },
69
  },
 
 
 
 
 
 
 
 
70
  },
71
  animation: {
72
- "accordion-down": "accordion-down 0.2s ease-out",
73
- "accordion-up": "accordion-up 0.2s ease-out",
 
74
  },
75
  },
76
  },
77
- plugins: [require("tailwindcss-animate")],
78
- } satisfies Config
79
 
80
- export default config
 
1
+ import type { Config } from 'tailwindcss';
2
 
3
  const config = {
4
+ darkMode: ['class'],
5
  content: [
6
  './pages/**/*.{ts,tsx}',
7
  './components/**/*.{ts,tsx}',
8
  './app/**/*.{ts,tsx}',
9
  './src/**/*.{ts,tsx}',
10
+ ],
11
+ prefix: '',
12
  theme: {
13
  container: {
14
  center: true,
15
+ padding: '2rem',
16
  screens: {
17
+ '2xl': '1400px',
18
  },
19
  },
20
  extend: {
21
  colors: {
22
+ border: 'hsl(var(--border))',
23
+ input: 'hsl(var(--input))',
24
+ ring: 'hsl(var(--ring))',
25
+ background: 'hsl(var(--background))',
26
+ foreground: 'hsl(var(--foreground))',
27
  primary: {
28
+ DEFAULT: 'hsl(var(--primary))',
29
+ foreground: 'hsl(var(--primary-foreground))',
30
  },
31
  secondary: {
32
+ DEFAULT: 'hsl(var(--secondary))',
33
+ foreground: 'hsl(var(--secondary-foreground))',
34
  },
35
  destructive: {
36
+ DEFAULT: 'hsl(var(--destructive))',
37
+ foreground: 'hsl(var(--destructive-foreground))',
38
  },
39
  muted: {
40
+ DEFAULT: 'hsl(var(--muted))',
41
+ foreground: 'hsl(var(--muted-foreground))',
42
  },
43
  accent: {
44
+ DEFAULT: 'hsl(var(--accent))',
45
+ foreground: 'hsl(var(--accent-foreground))',
46
  },
47
  popover: {
48
+ DEFAULT: 'hsl(var(--popover))',
49
+ foreground: 'hsl(var(--popover-foreground))',
50
  },
51
  card: {
52
+ DEFAULT: 'hsl(var(--card))',
53
+ foreground: 'hsl(var(--card-foreground))',
54
  },
55
  },
56
  borderRadius: {
57
+ lg: 'var(--radius)',
58
+ md: 'calc(var(--radius) - 2px)',
59
+ sm: 'calc(var(--radius) - 4px)',
60
  },
61
  keyframes: {
62
+ 'accordion-down': {
63
+ from: { height: '0' },
64
+ to: { height: 'var(--radix-accordion-content-height)' },
65
  },
66
+ 'accordion-up': {
67
+ from: { height: 'var(--radix-accordion-content-height)' },
68
+ to: { height: '0' },
69
  },
70
+ progress: {
71
+ '0%': { transform: ' translateX(0) scaleX(0)' },
72
+ '40%': { transform: 'translateX(0) scaleX(0.4)' },
73
+ '100%': { transform: 'translateX(100%) scaleX(0.5)' },
74
+ },
75
+ },
76
+ transformOrigin: {
77
+ 'left-right': '0% 50%',
78
  },
79
  animation: {
80
+ 'accordion-down': 'accordion-down 0.2s ease-out',
81
+ 'accordion-up': 'accordion-up 0.2s ease-out',
82
+ progress: 'progress 1s infinite linear',
83
  },
84
  },
85
  },
86
+ plugins: [require('tailwindcss-animate')],
87
+ } satisfies Config;
88
 
89
+ export default config;