Spaces:
Running
Running
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 +2 -3
- app/chat/page.tsx +31 -84
- app/layout.tsx +1 -1
- components/Header.tsx +29 -0
- components/chat/ChatClient.tsx +39 -13
- components/chat/ChatMessage.tsx +2 -7
- components/chat/Composer.tsx +100 -111
- components/ui/Chip.tsx +1 -1
- components/ui/Icons.tsx +23 -3
- components/ui/Tooltip.tsx +1 -1
- lib/db/functions.ts +3 -1
- lib/hooks/useImageUpload.ts +0 -38
- components/chat/ImageSelector.tsx → lib/hooks/useMediaUpload.ts +30 -49
- lib/hooks/useScrollAnchor.tsx +4 -4
- lib/hooks/useVisionAgent.ts +5 -21
- package.json +1 -1
- pnpm-lock.yaml +102 -11
- tailwind.config.ts +48 -39
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
|
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&
|
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
|
|
|
|
|
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-
|
57 |
-
<
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
</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-
|
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 {
|
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,
|
19 |
-
useVisionAgent(chat);
|
20 |
|
21 |
-
const { messagesRef, scrollRef, visibilityRef,
|
22 |
useScrollAnchor();
|
23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
return (
|
25 |
<div
|
26 |
-
className="h-full overflow-auto mx-auto
|
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-
|
40 |
</div>
|
41 |
-
<div className="
|
42 |
<Composer
|
43 |
id={id}
|
44 |
mediaUrl={mediaUrl}
|
45 |
isLoading={isLoading}
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
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 |
-
|
30 |
-
|
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 =
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
if (inputRef.current) {
|
52 |
inputRef.current.focus();
|
53 |
}
|
54 |
}, []);
|
55 |
|
56 |
-
const mediaName =
|
57 |
return (
|
58 |
-
<div
|
59 |
-
{
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
)}
|
79 |
<form
|
80 |
onSubmit={async e => {
|
81 |
e.preventDefault();
|
82 |
-
if (!input?.trim()) {
|
83 |
return;
|
84 |
}
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
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={
|
105 |
onChange={e => setInput(e.target.value)}
|
106 |
-
placeholder={
|
|
|
|
|
107 |
spellCheck={false}
|
108 |
-
className="w-full grow resize-none bg-transparent focus-within:outline-none
|
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=
|
152 |
-
disabled={
|
153 |
>
|
154 |
-
<
|
155 |
</Button>
|
156 |
</TooltipTrigger>
|
157 |
-
<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-
|
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 |
-
|
523 |
viewBox="0 0 16 16"
|
524 |
width="16"
|
525 |
className={cn('size-4', className)}
|
526 |
{...props}
|
527 |
>
|
528 |
<path
|
529 |
-
|
530 |
-
|
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
|
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 |
-
|
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 {
|
15 |
-
import {
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
type Example = {
|
20 |
-
url: string;
|
21 |
-
initMessages: MessageBase[];
|
22 |
-
};
|
23 |
|
24 |
-
const
|
25 |
-
|
|
|
|
|
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 } =
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
104 |
-
id: resp.id,
|
105 |
-
mediaUrl: resp.publicUrl,
|
106 |
-
});
|
107 |
setUploading(false);
|
108 |
-
router.push(`/chat/${resp.id}`);
|
109 |
};
|
110 |
},
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
13 |
-
|
14 |
block: 'end',
|
15 |
behavior: 'smooth',
|
16 |
});
|
@@ -72,7 +72,7 @@ export const useScrollAnchor = () => {
|
|
72 |
});
|
73 |
},
|
74 |
{
|
75 |
-
rootMargin: '0px 0px -
|
76 |
},
|
77 |
);
|
78 |
|
@@ -89,6 +89,6 @@ export const useScrollAnchor = () => {
|
|
89 |
scrollRef,
|
90 |
visibilityRef,
|
91 |
scrollToBottom,
|
92 |
-
|
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 |
-
|
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
|
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": "^
|
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: ^
|
55 |
-
version:
|
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@
|
1467 |
-
resolution: {integrity: sha512-
|
1468 |
-
engines: {node: '>=
|
1469 |
peerDependencies:
|
1470 |
-
|
|
|
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.
|
2053 |
-
resolution: {integrity: sha512-
|
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 |
-
ai@
|
5647 |
dependencies:
|
5648 |
-
|
|
|
|
|
|
|
|
|
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 |
swrv: 1.0.4([email protected]([email protected]))
|
|
|
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.
|
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 |
+
version: 3.1.22([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])
|
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 |
+
'@ai-sdk/[email protected]([email protected])':
|
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 |
swrv: 1.0.4([email protected]([email protected]))
|
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 |
+
[email protected]: {}
|
5947 |
+
|
5948 | |
5949 |
|
5950 | |
|
|
6106 |
|
6107 | |
6108 |
|
6109 |
+
[email protected]: {}
|
6110 |
+
|
6111 | |
6112 |
|
6113 | |
|
|
6456 |
|
6457 | |
6458 |
|
6459 |
+
eventsource-parser@1.1.2: {}
|
6460 |
|
6461 | |
6462 |
|
|
|
6963 |
|
6964 | |
6965 |
|
6966 |
+
[email protected]: {}
|
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 |
+
[email protected]: {}
|
8017 |
+
|
8018 | |
8019 |
|
8020 | |
|
|
8602 |
|
8603 | |
8604 |
|
8605 | |
8606 |
+
dependencies:
|
8607 |
+
zod: 3.23.8
|
8608 |
+
|
8609 |
+
[email protected]: {}
|
8610 |
+
|
8611 |
tailwind.config.ts
CHANGED
@@ -1,80 +1,89 @@
|
|
1 |
-
import type { Config } from
|
2 |
|
3 |
const config = {
|
4 |
-
darkMode: [
|
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:
|
16 |
screens: {
|
17 |
-
|
18 |
},
|
19 |
},
|
20 |
extend: {
|
21 |
colors: {
|
22 |
-
border:
|
23 |
-
input:
|
24 |
-
ring:
|
25 |
-
background:
|
26 |
-
foreground:
|
27 |
primary: {
|
28 |
-
DEFAULT:
|
29 |
-
foreground:
|
30 |
},
|
31 |
secondary: {
|
32 |
-
DEFAULT:
|
33 |
-
foreground:
|
34 |
},
|
35 |
destructive: {
|
36 |
-
DEFAULT:
|
37 |
-
foreground:
|
38 |
},
|
39 |
muted: {
|
40 |
-
DEFAULT:
|
41 |
-
foreground:
|
42 |
},
|
43 |
accent: {
|
44 |
-
DEFAULT:
|
45 |
-
foreground:
|
46 |
},
|
47 |
popover: {
|
48 |
-
DEFAULT:
|
49 |
-
foreground:
|
50 |
},
|
51 |
card: {
|
52 |
-
DEFAULT:
|
53 |
-
foreground:
|
54 |
},
|
55 |
},
|
56 |
borderRadius: {
|
57 |
-
lg:
|
58 |
-
md:
|
59 |
-
sm:
|
60 |
},
|
61 |
keyframes: {
|
62 |
-
|
63 |
-
from: { height:
|
64 |
-
to: { height:
|
65 |
},
|
66 |
-
|
67 |
-
from: { height:
|
68 |
-
to: { height:
|
69 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
},
|
71 |
animation: {
|
72 |
-
|
73 |
-
|
|
|
74 |
},
|
75 |
},
|
76 |
},
|
77 |
-
plugins: [require(
|
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;
|