MingruiZhang commited on
Commit
3ba9c0c
·
unverified ·
0 Parent(s):
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app/actions.ts +129 -0
  2. app/api/chat/route.ts +57 -0
  3. app/layout.tsx +62 -0
  4. app/opengraph-image.png +0 -0
  5. app/twitter-image.png +0 -0
  6. auth.ts +39 -0
  7. components/button-scroll-to-bottom.tsx +34 -0
  8. components/chat-history.tsx +46 -0
  9. components/chat-list.tsx +27 -0
  10. components/chat-message-actions.tsx +40 -0
  11. components/chat-message.tsx +80 -0
  12. components/chat-panel.tsx +103 -0
  13. components/chat-scroll-anchor.tsx +29 -0
  14. components/chat-share-dialog.tsx +106 -0
  15. components/chat.tsx +76 -0
  16. components/clear-history.tsx +77 -0
  17. components/empty-screen.tsx +30 -0
  18. components/external-link.tsx +29 -0
  19. components/header.tsx +73 -0
  20. components/login-button.tsx +42 -0
  21. components/markdown.tsx +9 -0
  22. components/prompt-form.tsx +97 -0
  23. components/providers.tsx +17 -0
  24. components/sidebar-actions.tsx +125 -0
  25. components/sidebar-desktop.tsx +21 -0
  26. components/sidebar-footer.tsx +16 -0
  27. components/sidebar-item.tsx +124 -0
  28. components/sidebar-items.tsx +42 -0
  29. components/sidebar-list.tsx +38 -0
  30. components/sidebar-mobile.tsx +28 -0
  31. components/sidebar-toggle.tsx +24 -0
  32. components/sidebar.tsx +21 -0
  33. components/tailwind-indicator.tsx +14 -0
  34. components/theme-toggle.tsx +31 -0
  35. components/ui/alert-dialog.tsx +141 -0
  36. components/ui/badge.tsx +36 -0
  37. components/ui/button.tsx +57 -0
  38. components/ui/codeblock.tsx +148 -0
  39. components/ui/dialog.tsx +122 -0
  40. components/ui/dropdown-menu.tsx +128 -0
  41. components/ui/icons.tsx +507 -0
  42. components/ui/input.tsx +25 -0
  43. components/ui/label.tsx +26 -0
  44. components/ui/select.tsx +123 -0
  45. components/ui/separator.tsx +31 -0
  46. components/ui/sheet.tsx +140 -0
  47. components/ui/switch.tsx +29 -0
  48. components/ui/textarea.tsx +24 -0
  49. components/ui/tooltip.tsx +30 -0
  50. components/user-menu.tsx +68 -0
app/actions.ts ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use server'
2
+
3
+ import { revalidatePath } from 'next/cache'
4
+ import { redirect } from 'next/navigation'
5
+ import { kv } from '@vercel/kv'
6
+
7
+ import { auth } from '@/auth'
8
+ import { type Chat } from '@/lib/types'
9
+
10
+ export async function getChats(userId?: string | null) {
11
+ if (!userId) {
12
+ return []
13
+ }
14
+
15
+ try {
16
+ const pipeline = kv.pipeline()
17
+ const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
18
+ rev: true
19
+ })
20
+
21
+ for (const chat of chats) {
22
+ pipeline.hgetall(chat)
23
+ }
24
+
25
+ const results = await pipeline.exec()
26
+
27
+ return results as Chat[]
28
+ } catch (error) {
29
+ return []
30
+ }
31
+ }
32
+
33
+ export async function getChat(id: string, userId: string) {
34
+ const chat = await kv.hgetall<Chat>(`chat:${id}`)
35
+
36
+ if (!chat || (userId && chat.userId !== userId)) {
37
+ return null
38
+ }
39
+
40
+ return chat
41
+ }
42
+
43
+ export async function removeChat({ id, path }: { id: string; path: string }) {
44
+ const session = await auth()
45
+
46
+ if (!session) {
47
+ return {
48
+ error: 'Unauthorized'
49
+ }
50
+ }
51
+
52
+ //Convert uid to string for consistent comparison with session.user.id
53
+ const uid = String(await kv.hget(`chat:${id}`, 'userId'))
54
+
55
+ if (uid !== session?.user?.id) {
56
+ return {
57
+ error: 'Unauthorized'
58
+ }
59
+ }
60
+
61
+ await kv.del(`chat:${id}`)
62
+ await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)
63
+
64
+ revalidatePath('/')
65
+ return revalidatePath(path)
66
+ }
67
+
68
+ export async function clearChats() {
69
+ const session = await auth()
70
+
71
+ if (!session?.user?.id) {
72
+ return {
73
+ error: 'Unauthorized'
74
+ }
75
+ }
76
+
77
+ const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1)
78
+ if (!chats.length) {
79
+ return redirect('/')
80
+ }
81
+ const pipeline = kv.pipeline()
82
+
83
+ for (const chat of chats) {
84
+ pipeline.del(chat)
85
+ pipeline.zrem(`user:chat:${session.user.id}`, chat)
86
+ }
87
+
88
+ await pipeline.exec()
89
+
90
+ revalidatePath('/')
91
+ return redirect('/')
92
+ }
93
+
94
+ export async function getSharedChat(id: string) {
95
+ const chat = await kv.hgetall<Chat>(`chat:${id}`)
96
+
97
+ if (!chat || !chat.sharePath) {
98
+ return null
99
+ }
100
+
101
+ return chat
102
+ }
103
+
104
+ export async function shareChat(id: string) {
105
+ const session = await auth()
106
+
107
+ if (!session?.user?.id) {
108
+ return {
109
+ error: 'Unauthorized'
110
+ }
111
+ }
112
+
113
+ const chat = await kv.hgetall<Chat>(`chat:${id}`)
114
+
115
+ if (!chat || chat.userId !== session.user.id) {
116
+ return {
117
+ error: 'Something went wrong'
118
+ }
119
+ }
120
+
121
+ const payload = {
122
+ ...chat,
123
+ sharePath: `/share/${chat.id}`
124
+ }
125
+
126
+ await kv.hmset(`chat:${chat.id}`, payload)
127
+
128
+ return payload
129
+ }
app/api/chat/route.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { OpenAIStream, StreamingTextResponse } from 'ai'
2
+ import OpenAI from 'openai'
3
+
4
+ import { auth } from '@/auth'
5
+ import { nanoid } from '@/lib/utils'
6
+
7
+ export const runtime = 'edge'
8
+
9
+ const openai = new OpenAI({
10
+ apiKey: process.env.OPENAI_API_KEY
11
+ })
12
+
13
+ export async function POST(req: Request) {
14
+ const json = await req.json()
15
+ const { messages, previewToken } = json
16
+ const userId = (await auth())?.user.id
17
+
18
+ if (!userId) {
19
+ return new Response('Unauthorized', {
20
+ status: 401
21
+ })
22
+ }
23
+
24
+ if (previewToken) {
25
+ openai.apiKey = previewToken
26
+ }
27
+
28
+ const res = await openai.chat.completions.create({
29
+ model: 'gpt-3.5-turbo',
30
+ messages,
31
+ temperature: 0.7,
32
+ stream: true
33
+ })
34
+
35
+ const stream = OpenAIStream(res, {
36
+ async onCompletion(completion) {
37
+ const title = json.messages[0].content.substring(0, 100)
38
+ const id = json.id ?? nanoid()
39
+ const createdAt = Date.now()
40
+ const payload = {
41
+ id,
42
+ title,
43
+ userId,
44
+ createdAt,
45
+ messages: [
46
+ ...messages,
47
+ {
48
+ content: completion,
49
+ role: 'assistant'
50
+ }
51
+ ]
52
+ }
53
+ }
54
+ })
55
+
56
+ return new StreamingTextResponse(stream)
57
+ }
app/layout.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Toaster } from 'react-hot-toast'
2
+ import { GeistSans } from 'geist/font/sans'
3
+ import { GeistMono } from 'geist/font/mono'
4
+
5
+ import '@/app/globals.css'
6
+ import { cn } from '@/lib/utils'
7
+ import { TailwindIndicator } from '@/components/tailwind-indicator'
8
+ import { Providers } from '@/components/providers'
9
+ import { Header } from '@/components/header'
10
+
11
+ export const metadata = {
12
+ metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
13
+ title: {
14
+ default: 'Next.js AI Chatbot',
15
+ template: `%s - Next.js AI Chatbot`
16
+ },
17
+ description: 'An AI-powered chatbot template built with Next.js and Vercel.',
18
+ icons: {
19
+ icon: '/favicon.ico',
20
+ shortcut: '/favicon-16x16.png',
21
+ apple: '/apple-touch-icon.png'
22
+ }
23
+ }
24
+
25
+ export const viewport = {
26
+ themeColor: [
27
+ { media: '(prefers-color-scheme: light)', color: 'white' },
28
+ { media: '(prefers-color-scheme: dark)', color: 'black' }
29
+ ]
30
+ }
31
+
32
+ interface RootLayoutProps {
33
+ children: React.ReactNode
34
+ }
35
+
36
+ export default function RootLayout({ children }: RootLayoutProps) {
37
+ return (
38
+ <html lang="en" suppressHydrationWarning>
39
+ <body
40
+ className={cn(
41
+ 'font-sans antialiased',
42
+ GeistSans.variable,
43
+ GeistMono.variable
44
+ )}
45
+ >
46
+ <Toaster />
47
+ <Providers
48
+ attribute="class"
49
+ defaultTheme="system"
50
+ enableSystem
51
+ disableTransitionOnChange
52
+ >
53
+ <div className="flex flex-col min-h-screen">
54
+ <Header />
55
+ <main className="flex flex-col flex-1 bg-muted/50">{children}</main>
56
+ </div>
57
+ <TailwindIndicator />
58
+ </Providers>
59
+ </body>
60
+ </html>
61
+ )
62
+ }
app/opengraph-image.png ADDED
app/twitter-image.png ADDED
auth.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import NextAuth, { type DefaultSession } from 'next-auth'
2
+ import GitHub from 'next-auth/providers/github'
3
+
4
+ declare module 'next-auth' {
5
+ interface Session {
6
+ user: {
7
+ /** The user's id. */
8
+ id: string
9
+ } & DefaultSession['user']
10
+ }
11
+ }
12
+
13
+ export const {
14
+ handlers: { GET, POST },
15
+ auth
16
+ } = NextAuth({
17
+ providers: [GitHub],
18
+ callbacks: {
19
+ jwt({ token, profile }) {
20
+ if (profile) {
21
+ token.id = profile.id
22
+ token.image = profile.avatar_url || profile.picture
23
+ }
24
+ return token
25
+ },
26
+ session: ({ session, token }) => {
27
+ if (session?.user && token?.id) {
28
+ session.user.id = String(token.id)
29
+ }
30
+ return session
31
+ },
32
+ authorized({ auth }) {
33
+ return !!auth?.user // this ensures there is a logged in user for -every- request
34
+ }
35
+ },
36
+ pages: {
37
+ signIn: '/sign-in' // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages
38
+ }
39
+ })
components/button-scroll-to-bottom.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { cn } from '@/lib/utils'
6
+ import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7
+ import { Button, type ButtonProps } from '@/components/ui/button'
8
+ import { IconArrowDown } from '@/components/ui/icons'
9
+
10
+ export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
11
+ const isAtBottom = useAtBottom()
12
+
13
+ return (
14
+ <Button
15
+ variant="outline"
16
+ size="icon"
17
+ className={cn(
18
+ 'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
19
+ isAtBottom ? 'opacity-0' : 'opacity-100',
20
+ className
21
+ )}
22
+ onClick={() =>
23
+ window.scrollTo({
24
+ top: document.body.offsetHeight,
25
+ behavior: 'smooth'
26
+ })
27
+ }
28
+ {...props}
29
+ >
30
+ <IconArrowDown />
31
+ <span className="sr-only">Scroll to bottom</span>
32
+ </Button>
33
+ )
34
+ }
components/chat-history.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+
3
+ import Link from 'next/link'
4
+
5
+ import { cn } from '@/lib/utils'
6
+ import { SidebarList } from '@/components/sidebar-list'
7
+ import { buttonVariants } from '@/components/ui/button'
8
+ import { IconPlus } from '@/components/ui/icons'
9
+
10
+ interface ChatHistoryProps {
11
+ userId?: string
12
+ }
13
+
14
+ export async function ChatHistory({ userId }: ChatHistoryProps) {
15
+ return (
16
+ <div className="flex flex-col h-full">
17
+ <div className="px-2 my-4">
18
+ <Link
19
+ href="/"
20
+ className={cn(
21
+ buttonVariants({ variant: 'outline' }),
22
+ 'h-10 w-full justify-start bg-zinc-50 px-4 shadow-none transition-colors hover:bg-zinc-200/40 dark:bg-zinc-900 dark:hover:bg-zinc-300/10'
23
+ )}
24
+ >
25
+ <IconPlus className="-translate-x-2 stroke-2" />
26
+ New Chat
27
+ </Link>
28
+ </div>
29
+ <React.Suspense
30
+ fallback={
31
+ <div className="flex flex-col flex-1 px-4 space-y-4 overflow-auto">
32
+ {Array.from({ length: 10 }).map((_, i) => (
33
+ <div
34
+ key={i}
35
+ className="w-full h-6 rounded-md shrink-0 animate-pulse bg-zinc-200 dark:bg-zinc-800"
36
+ />
37
+ ))}
38
+ </div>
39
+ }
40
+ >
41
+ {/* @ts-ignore */}
42
+ <SidebarList userId={userId} />
43
+ </React.Suspense>
44
+ </div>
45
+ )
46
+ }
components/chat-list.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type Message } from 'ai'
2
+
3
+ import { Separator } from '@/components/ui/separator'
4
+ import { ChatMessage } from '@/components/chat-message'
5
+
6
+ export interface ChatList {
7
+ messages: Message[]
8
+ }
9
+
10
+ export function ChatList({ messages }: ChatList) {
11
+ if (!messages.length) {
12
+ return null
13
+ }
14
+
15
+ return (
16
+ <div className="relative mx-auto max-w-2xl px-4">
17
+ {messages.map((message, index) => (
18
+ <div key={index}>
19
+ <ChatMessage message={message} />
20
+ {index < messages.length - 1 && (
21
+ <Separator className="my-4 md:my-8" />
22
+ )}
23
+ </div>
24
+ ))}
25
+ </div>
26
+ )
27
+ }
components/chat-message-actions.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { type Message } from 'ai'
4
+
5
+ import { Button } from '@/components/ui/button'
6
+ import { IconCheck, IconCopy } from '@/components/ui/icons'
7
+ import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
8
+ import { cn } from '@/lib/utils'
9
+
10
+ interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
11
+ message: Message
12
+ }
13
+
14
+ export function ChatMessageActions({
15
+ message,
16
+ className,
17
+ ...props
18
+ }: ChatMessageActionsProps) {
19
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
20
+
21
+ const onCopy = () => {
22
+ if (isCopied) return
23
+ copyToClipboard(message.content)
24
+ }
25
+
26
+ return (
27
+ <div
28
+ className={cn(
29
+ 'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ <Button variant="ghost" size="icon" onClick={onCopy}>
35
+ {isCopied ? <IconCheck /> : <IconCopy />}
36
+ <span className="sr-only">Copy message</span>
37
+ </Button>
38
+ </div>
39
+ )
40
+ }
components/chat-message.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Inspired by Chatbot-UI and modified to fit the needs of this project
2
+ // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3
+
4
+ import { Message } from 'ai'
5
+ import remarkGfm from 'remark-gfm'
6
+ import remarkMath from 'remark-math'
7
+
8
+ import { cn } from '@/lib/utils'
9
+ import { CodeBlock } from '@/components/ui/codeblock'
10
+ import { MemoizedReactMarkdown } from '@/components/markdown'
11
+ import { IconOpenAI, IconUser } from '@/components/ui/icons'
12
+ import { ChatMessageActions } from '@/components/chat-message-actions'
13
+
14
+ export interface ChatMessageProps {
15
+ message: Message
16
+ }
17
+
18
+ export function ChatMessage({ message, ...props }: ChatMessageProps) {
19
+ return (
20
+ <div
21
+ className={cn('group relative mb-4 flex items-start md:-ml-12')}
22
+ {...props}
23
+ >
24
+ <div
25
+ className={cn(
26
+ 'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
27
+ message.role === 'user'
28
+ ? 'bg-background'
29
+ : 'bg-primary text-primary-foreground'
30
+ )}
31
+ >
32
+ {message.role === 'user' ? <IconUser /> : <IconOpenAI />}
33
+ </div>
34
+ <div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden">
35
+ <MemoizedReactMarkdown
36
+ className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0"
37
+ remarkPlugins={[remarkGfm, remarkMath]}
38
+ components={{
39
+ p({ children }) {
40
+ return <p className="mb-2 last:mb-0">{children}</p>
41
+ },
42
+ code({ node, inline, className, children, ...props }) {
43
+ if (children.length) {
44
+ if (children[0] == '▍') {
45
+ return (
46
+ <span className="mt-1 cursor-default animate-pulse">▍</span>
47
+ )
48
+ }
49
+
50
+ children[0] = (children[0] as string).replace('`▍`', '▍')
51
+ }
52
+
53
+ const match = /language-(\w+)/.exec(className || '')
54
+
55
+ if (inline) {
56
+ return (
57
+ <code className={className} {...props}>
58
+ {children}
59
+ </code>
60
+ )
61
+ }
62
+
63
+ return (
64
+ <CodeBlock
65
+ key={Math.random()}
66
+ language={(match && match[1]) || ''}
67
+ value={String(children).replace(/\n$/, '')}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+ }}
73
+ >
74
+ {message.content}
75
+ </MemoizedReactMarkdown>
76
+ <ChatMessageActions message={message} />
77
+ </div>
78
+ </div>
79
+ )
80
+ }
components/chat-panel.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { type UseChatHelpers } from 'ai/react'
3
+
4
+ import { shareChat } from '@/app/actions'
5
+ import { Button } from '@/components/ui/button'
6
+ import { PromptForm } from '@/components/prompt-form'
7
+ import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
8
+ import { IconRefresh, IconShare, IconStop } from '@/components/ui/icons'
9
+ import { ChatShareDialog } from '@/components/chat-share-dialog'
10
+
11
+ export interface ChatPanelProps
12
+ extends Pick<
13
+ UseChatHelpers,
14
+ | 'append'
15
+ | 'isLoading'
16
+ | 'reload'
17
+ | 'messages'
18
+ | 'stop'
19
+ | 'input'
20
+ | 'setInput'
21
+ > {
22
+ id?: string
23
+ title?: string
24
+ }
25
+
26
+ export function ChatPanel({
27
+ id,
28
+ title,
29
+ isLoading,
30
+ stop,
31
+ append,
32
+ reload,
33
+ input,
34
+ setInput,
35
+ messages
36
+ }: ChatPanelProps) {
37
+ const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
38
+
39
+ return (
40
+ <div className="fixed inset-x-0 bottom-0 w-full bg-gradient-to-b from-muted/30 from-0% to-muted/30 to-50% animate-in duration-300 ease-in-out dark:from-background/10 dark:from-10% dark:to-background/80 peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]">
41
+ <ButtonScrollToBottom />
42
+ <div className="mx-auto sm:max-w-2xl sm:px-4">
43
+ <div className="flex items-center justify-center h-12">
44
+ {isLoading ? (
45
+ <Button
46
+ variant="outline"
47
+ onClick={() => stop()}
48
+ className="bg-background"
49
+ >
50
+ <IconStop className="mr-2" />
51
+ Stop generating
52
+ </Button>
53
+ ) : (
54
+ messages?.length >= 2 && (
55
+ <div className="flex space-x-2">
56
+ <Button variant="outline" onClick={() => reload()}>
57
+ <IconRefresh className="mr-2" />
58
+ Regenerate response
59
+ </Button>
60
+ {id && title ? (
61
+ <>
62
+ <Button
63
+ variant="outline"
64
+ onClick={() => setShareDialogOpen(true)}
65
+ >
66
+ <IconShare className="mr-2" />
67
+ Share
68
+ </Button>
69
+ <ChatShareDialog
70
+ open={shareDialogOpen}
71
+ onOpenChange={setShareDialogOpen}
72
+ onCopy={() => setShareDialogOpen(false)}
73
+ shareChat={shareChat}
74
+ chat={{
75
+ id,
76
+ title,
77
+ messages
78
+ }}
79
+ />
80
+ </>
81
+ ) : null}
82
+ </div>
83
+ )
84
+ )}
85
+ </div>
86
+ <div className="px-4 py-2 space-y-4 border-t shadow-lg bg-background sm:rounded-t-xl sm:border md:py-4">
87
+ <PromptForm
88
+ onSubmit={async value => {
89
+ await append({
90
+ id,
91
+ content: value,
92
+ role: 'user'
93
+ })
94
+ }}
95
+ input={input}
96
+ setInput={setInput}
97
+ isLoading={isLoading}
98
+ />
99
+ </div>
100
+ </div>
101
+ </div>
102
+ )
103
+ }
components/chat-scroll-anchor.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useInView } from 'react-intersection-observer'
5
+
6
+ import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7
+
8
+ interface ChatScrollAnchorProps {
9
+ trackVisibility?: boolean
10
+ }
11
+
12
+ export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
13
+ const isAtBottom = useAtBottom()
14
+ const { ref, entry, inView } = useInView({
15
+ trackVisibility,
16
+ delay: 100,
17
+ rootMargin: '0px 0px -150px 0px'
18
+ })
19
+
20
+ React.useEffect(() => {
21
+ if (isAtBottom && trackVisibility && !inView) {
22
+ entry?.target.scrollIntoView({
23
+ block: 'start'
24
+ })
25
+ }
26
+ }, [inView, entry, isAtBottom, trackVisibility])
27
+
28
+ return <div ref={ref} className="h-px w-full" />
29
+ }
components/chat-share-dialog.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { type DialogProps } from '@radix-ui/react-dialog'
5
+ import { toast } from 'react-hot-toast'
6
+
7
+ import { ServerActionResult, type Chat } from '@/lib/types'
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle
16
+ } from '@/components/ui/dialog'
17
+ import { IconSpinner } from '@/components/ui/icons'
18
+ import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
19
+
20
+ interface ChatShareDialogProps extends DialogProps {
21
+ chat: Pick<Chat, 'id' | 'title' | 'messages'>
22
+ shareChat: (id: string) => ServerActionResult<Chat>
23
+ onCopy: () => void
24
+ }
25
+
26
+ export function ChatShareDialog({
27
+ chat,
28
+ shareChat,
29
+ onCopy,
30
+ ...props
31
+ }: ChatShareDialogProps) {
32
+ const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 })
33
+ const [isSharePending, startShareTransition] = React.useTransition()
34
+
35
+ const copyShareLink = React.useCallback(
36
+ async (chat: Chat) => {
37
+ if (!chat.sharePath) {
38
+ return toast.error('Could not copy share link to clipboard')
39
+ }
40
+
41
+ const url = new URL(window.location.href)
42
+ url.pathname = chat.sharePath
43
+ copyToClipboard(url.toString())
44
+ onCopy()
45
+ toast.success('Share link copied to clipboard', {
46
+ style: {
47
+ borderRadius: '10px',
48
+ background: '#333',
49
+ color: '#fff',
50
+ fontSize: '14px'
51
+ },
52
+ iconTheme: {
53
+ primary: 'white',
54
+ secondary: 'black'
55
+ }
56
+ })
57
+ },
58
+ [copyToClipboard, onCopy]
59
+ )
60
+
61
+ return (
62
+ <Dialog {...props}>
63
+ <DialogContent>
64
+ <DialogHeader>
65
+ <DialogTitle>Share link to chat</DialogTitle>
66
+ <DialogDescription>
67
+ Anyone with the URL will be able to view the shared chat.
68
+ </DialogDescription>
69
+ </DialogHeader>
70
+ <div className="p-4 space-y-1 text-sm border rounded-md">
71
+ <div className="font-medium">{chat.title}</div>
72
+ <div className="text-muted-foreground">
73
+ {chat.messages.length} messages
74
+ </div>
75
+ </div>
76
+ <DialogFooter className="items-center">
77
+ <Button
78
+ disabled={isSharePending}
79
+ onClick={() => {
80
+ // @ts-ignore
81
+ startShareTransition(async () => {
82
+ const result = await shareChat(chat.id)
83
+
84
+ if (result && 'error' in result) {
85
+ toast.error(result.error)
86
+ return
87
+ }
88
+
89
+ copyShareLink(result)
90
+ })
91
+ }}
92
+ >
93
+ {isSharePending ? (
94
+ <>
95
+ <IconSpinner className="mr-2 animate-spin" />
96
+ Copying...
97
+ </>
98
+ ) : (
99
+ <>Copy link</>
100
+ )}
101
+ </Button>
102
+ </DialogFooter>
103
+ </DialogContent>
104
+ </Dialog>
105
+ )
106
+ }
components/chat.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useChat, type Message } from 'ai/react'
4
+
5
+ import { cn } from '@/lib/utils'
6
+ import { ChatList } from '@/components/chat-list'
7
+ import { ChatPanel } from '@/components/chat-panel'
8
+ import { EmptyScreen } from '@/components/empty-screen'
9
+ import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
10
+ import { useLocalStorage } from '@/lib/hooks/use-local-storage'
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogDescription,
15
+ DialogFooter,
16
+ DialogHeader,
17
+ DialogTitle
18
+ } from '@/components/ui/dialog'
19
+ import { useState } from 'react'
20
+ import { Button } from './ui/button'
21
+ import { Input } from './ui/input'
22
+ import { toast } from 'react-hot-toast'
23
+ import { usePathname, useRouter } from 'next/navigation'
24
+
25
+ export interface ChatProps extends React.ComponentProps<'div'> {
26
+ initialMessages?: Message[]
27
+ id?: string
28
+ }
29
+
30
+ export function Chat({ id, initialMessages, className }: ChatProps) {
31
+ const router = useRouter()
32
+ const path = usePathname()
33
+ const [previewToken, setPreviewToken] = useLocalStorage<string | null>(
34
+ 'ai-token',
35
+ null
36
+ )
37
+ const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '')
38
+ const { messages, append, reload, stop, isLoading, input, setInput } =
39
+ useChat({
40
+ initialMessages,
41
+ id,
42
+ body: {
43
+ id,
44
+ previewToken
45
+ },
46
+ onResponse(response) {
47
+ if (response.status === 401) {
48
+ toast.error(response.statusText)
49
+ }
50
+ }
51
+ })
52
+ return (
53
+ <>
54
+ <div className={cn('pb-[200px] pt-4 md:pt-10', className)}>
55
+ {messages.length ? (
56
+ <>
57
+ <ChatList messages={messages} />
58
+ <ChatScrollAnchor trackVisibility={isLoading} />
59
+ </>
60
+ ) : (
61
+ <EmptyScreen setInput={setInput} />
62
+ )}
63
+ </div>
64
+ <ChatPanel
65
+ id={id}
66
+ isLoading={isLoading}
67
+ stop={stop}
68
+ append={append}
69
+ reload={reload}
70
+ messages={messages}
71
+ input={input}
72
+ setInput={setInput}
73
+ />
74
+ </>
75
+ )
76
+ }
components/clear-history.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { toast } from 'react-hot-toast'
6
+
7
+ import { ServerActionResult } from '@/lib/types'
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ AlertDialog,
11
+ AlertDialogAction,
12
+ AlertDialogCancel,
13
+ AlertDialogContent,
14
+ AlertDialogDescription,
15
+ AlertDialogFooter,
16
+ AlertDialogHeader,
17
+ AlertDialogTitle,
18
+ AlertDialogTrigger
19
+ } from '@/components/ui/alert-dialog'
20
+ import { IconSpinner } from '@/components/ui/icons'
21
+
22
+ interface ClearHistoryProps {
23
+ isEnabled: boolean
24
+ clearChats: () => ServerActionResult<void>
25
+ }
26
+
27
+ export function ClearHistory({
28
+ isEnabled = false,
29
+ clearChats
30
+ }: ClearHistoryProps) {
31
+ const [open, setOpen] = React.useState(false)
32
+ const [isPending, startTransition] = React.useTransition()
33
+ const router = useRouter()
34
+
35
+ return (
36
+ <AlertDialog open={open} onOpenChange={setOpen}>
37
+ <AlertDialogTrigger asChild>
38
+ <Button variant="ghost" disabled={!isEnabled || isPending}>
39
+ {isPending && <IconSpinner className="mr-2" />}
40
+ Clear history
41
+ </Button>
42
+ </AlertDialogTrigger>
43
+ <AlertDialogContent>
44
+ <AlertDialogHeader>
45
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
46
+ <AlertDialogDescription>
47
+ This will permanently delete your chat history and remove your data
48
+ from our servers.
49
+ </AlertDialogDescription>
50
+ </AlertDialogHeader>
51
+ <AlertDialogFooter>
52
+ <AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
53
+ <AlertDialogAction
54
+ disabled={isPending}
55
+ onClick={event => {
56
+ event.preventDefault()
57
+ startTransition(() => {
58
+ clearChats().then(result => {
59
+ if (result && 'error' in result) {
60
+ toast.error(result.error)
61
+ return
62
+ }
63
+
64
+ setOpen(false)
65
+ router.push('/')
66
+ })
67
+ })
68
+ }}
69
+ >
70
+ {isPending && <IconSpinner className="mr-2 animate-spin" />}
71
+ Delete
72
+ </AlertDialogAction>
73
+ </AlertDialogFooter>
74
+ </AlertDialogContent>
75
+ </AlertDialog>
76
+ )
77
+ }
components/empty-screen.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { UseChatHelpers } from 'ai/react'
2
+
3
+ import { Button } from '@/components/ui/button'
4
+ import { ExternalLink } from '@/components/external-link'
5
+ import { IconArrowRight } from '@/components/ui/icons'
6
+
7
+ const exampleMessages = [
8
+ {
9
+ heading: 'Explain technical concepts',
10
+ message: `What is a "serverless function"?`
11
+ },
12
+ {
13
+ heading: 'Summarize an article',
14
+ message: 'Summarize the following article for a 2nd grader: \n'
15
+ },
16
+ {
17
+ heading: 'Draft an email',
18
+ message: `Draft an email to my boss about the following: \n`
19
+ }
20
+ ]
21
+
22
+ export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
23
+ return (
24
+ <div className="mx-auto max-w-2xl px-4">
25
+ <div className="rounded-lg border bg-background p-8">
26
+ <h1 className="text-lg font-semibold">Welcome to Vision Agent</h1>
27
+ </div>
28
+ </div>
29
+ )
30
+ }
components/external-link.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function ExternalLink({
2
+ href,
3
+ children
4
+ }: {
5
+ href: string
6
+ children: React.ReactNode
7
+ }) {
8
+ return (
9
+ <a
10
+ href={href}
11
+ target="_blank"
12
+ className="inline-flex flex-1 justify-center gap-1 leading-4 hover:underline"
13
+ >
14
+ <span>{children}</span>
15
+ <svg
16
+ aria-hidden="true"
17
+ height="7"
18
+ viewBox="0 0 6 6"
19
+ width="7"
20
+ className="opacity-70"
21
+ >
22
+ <path
23
+ d="M1.25215 5.54731L0.622742 4.9179L3.78169 1.75597H1.3834L1.38936 0.890915H5.27615V4.78069H4.40513L4.41109 2.38538L1.25215 5.54731Z"
24
+ fill="currentColor"
25
+ ></path>
26
+ </svg>
27
+ </a>
28
+ )
29
+ }
components/header.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import Link from 'next/link'
3
+
4
+ import { cn } from '@/lib/utils'
5
+ import { auth } from '@/auth'
6
+ import { Button, buttonVariants } from '@/components/ui/button'
7
+ import {
8
+ IconGitHub,
9
+ IconNextChat,
10
+ IconSeparator,
11
+ IconVercel
12
+ } from '@/components/ui/icons'
13
+ import { UserMenu } from '@/components/user-menu'
14
+ import { SidebarMobile } from './sidebar-mobile'
15
+ import { SidebarToggle } from './sidebar-toggle'
16
+ // import { ChatHistory } from './chat-history'
17
+
18
+ async function UserOrLogin() {
19
+ const session = await auth()
20
+ return (
21
+ <>
22
+ {/* {session?.user ? (
23
+ <>
24
+ <SidebarMobile>
25
+ <ChatHistory userId={session.user.id} />
26
+ </SidebarMobile>
27
+ <SidebarToggle />
28
+ </>
29
+ ) : ( */}
30
+ {/* <Link href="/" target="_blank" rel="nofollow">
31
+ <IconNextChat className="size-6 mr-2 dark:hidden" inverted />
32
+ <IconNextChat className="hidden size-6 mr-2 dark:block" />
33
+ </Link> */}
34
+ {/* )} */}
35
+ <div className="flex items-center">
36
+ {/* <IconSeparator className="size-6 text-muted-foreground/50" /> */}
37
+ {session?.user ? (
38
+ <UserMenu user={session.user} />
39
+ ) : (
40
+ <Button variant="link" asChild className="-ml-2">
41
+ <Link href="/sign-in?callbackUrl=/">Login</Link>
42
+ </Button>
43
+ )}
44
+ </div>
45
+ </>
46
+ )
47
+ }
48
+
49
+ export function Header() {
50
+ return (
51
+ <header className="sticky top-0 z-50 flex items-center justify-between w-full h-16 px-8 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
52
+ <div className="flex items-center">
53
+ {/* <React.Suspense fallback={<div className="flex-1 overflow-auto" />}>
54
+ <UserOrLogin />
55
+ </React.Suspense> */}
56
+ </div>
57
+ <div className="flex items-center justify-end space-x-2">
58
+ <React.Suspense fallback={<div className="flex-1 overflow-auto" />}>
59
+ <UserOrLogin />
60
+ </React.Suspense>
61
+ {/* <a
62
+ target="_blank"
63
+ href="https://github.com/vercel/nextjs-ai-chatbot/"
64
+ rel="noopener noreferrer"
65
+ className={cn(buttonVariants({ variant: 'outline' }))}
66
+ >
67
+ <IconGitHub />
68
+ <span className="hidden ml-2 md:flex">GitHub</span>
69
+ </a> */}
70
+ </div>
71
+ </header>
72
+ )
73
+ }
components/login-button.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { signIn } from 'next-auth/react'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { Button, type ButtonProps } from '@/components/ui/button'
8
+ import { IconGitHub, IconSpinner } from '@/components/ui/icons'
9
+
10
+ interface LoginButtonProps extends ButtonProps {
11
+ showGithubIcon?: boolean
12
+ text?: string
13
+ }
14
+
15
+ export function LoginButton({
16
+ text = 'Login with GitHub',
17
+ showGithubIcon = true,
18
+ className,
19
+ ...props
20
+ }: LoginButtonProps) {
21
+ const [isLoading, setIsLoading] = React.useState(false)
22
+ return (
23
+ <Button
24
+ variant="outline"
25
+ onClick={() => {
26
+ setIsLoading(true)
27
+ // next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
28
+ signIn('github', { callbackUrl: `/` })
29
+ }}
30
+ disabled={isLoading}
31
+ className={cn(className)}
32
+ {...props}
33
+ >
34
+ {isLoading ? (
35
+ <IconSpinner className="mr-2 animate-spin" />
36
+ ) : showGithubIcon ? (
37
+ <IconGitHub className="mr-2" />
38
+ ) : null}
39
+ {text}
40
+ </Button>
41
+ )
42
+ }
components/markdown.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { FC, memo } from 'react'
2
+ import ReactMarkdown, { Options } from 'react-markdown'
3
+
4
+ export const MemoizedReactMarkdown: FC<Options> = memo(
5
+ ReactMarkdown,
6
+ (prevProps, nextProps) =>
7
+ prevProps.children === nextProps.children &&
8
+ prevProps.className === nextProps.className
9
+ )
components/prompt-form.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import Textarea from 'react-textarea-autosize'
3
+ import { UseChatHelpers } from 'ai/react'
4
+ import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
5
+ import { cn } from '@/lib/utils'
6
+ import { Button, buttonVariants } from '@/components/ui/button'
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipTrigger
11
+ } from '@/components/ui/tooltip'
12
+ import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
13
+ import { useRouter } from 'next/navigation'
14
+
15
+ export interface PromptProps
16
+ extends Pick<UseChatHelpers, 'input' | 'setInput'> {
17
+ onSubmit: (value: string) => void
18
+ isLoading: boolean
19
+ }
20
+
21
+ export function PromptForm({
22
+ onSubmit,
23
+ input,
24
+ setInput,
25
+ isLoading
26
+ }: PromptProps) {
27
+ const { formRef, onKeyDown } = useEnterSubmit()
28
+ const inputRef = React.useRef<HTMLTextAreaElement>(null)
29
+ const router = useRouter()
30
+ React.useEffect(() => {
31
+ if (inputRef.current) {
32
+ inputRef.current.focus()
33
+ }
34
+ }, [])
35
+
36
+ return (
37
+ <form
38
+ onSubmit={async e => {
39
+ e.preventDefault()
40
+ if (!input?.trim()) {
41
+ return
42
+ }
43
+ setInput('')
44
+ await onSubmit(input)
45
+ }}
46
+ ref={formRef}
47
+ >
48
+ <div className="relative flex flex-col w-full px-8 pl-2 overflow-hidden max-h-60 grow bg-background sm:rounded-md sm:border sm:px-12 sm:pl-2">
49
+ {/* <Tooltip>
50
+ <TooltipTrigger asChild>
51
+ <button
52
+ onClick={e => {
53
+ e.preventDefault()
54
+ router.refresh()
55
+ router.push('/')
56
+ }}
57
+ className={cn(
58
+ buttonVariants({ size: 'sm', variant: 'outline' }),
59
+ 'absolute left-0 top-4 size-8 rounded-full bg-background p-0 sm:left-4'
60
+ )}
61
+ >
62
+ <IconPlus />
63
+ <span className="sr-only">New Chat</span>
64
+ </button>
65
+ </TooltipTrigger>
66
+ <TooltipContent>New Chat</TooltipContent>
67
+ </Tooltip> */}
68
+ <Textarea
69
+ ref={inputRef}
70
+ tabIndex={0}
71
+ onKeyDown={onKeyDown}
72
+ rows={1}
73
+ value={input}
74
+ onChange={e => setInput(e.target.value)}
75
+ placeholder="Send a message."
76
+ spellCheck={false}
77
+ className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
78
+ />
79
+ <div className="absolute right-0 top-4 sm:right-4">
80
+ <Tooltip>
81
+ <TooltipTrigger asChild>
82
+ <Button
83
+ type="submit"
84
+ size="icon"
85
+ disabled={isLoading || input === ''}
86
+ >
87
+ <IconArrowElbow />
88
+ <span className="sr-only">Send message</span>
89
+ </Button>
90
+ </TooltipTrigger>
91
+ <TooltipContent>Send message</TooltipContent>
92
+ </Tooltip>
93
+ </div>
94
+ </div>
95
+ </form>
96
+ )
97
+ }
components/providers.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { ThemeProvider as NextThemesProvider } from 'next-themes'
5
+ import { ThemeProviderProps } from 'next-themes/dist/types'
6
+ import { SidebarProvider } from '@/lib/hooks/use-sidebar'
7
+ import { TooltipProvider } from '@/components/ui/tooltip'
8
+
9
+ export function Providers({ children, ...props }: ThemeProviderProps) {
10
+ return (
11
+ <NextThemesProvider {...props}>
12
+ <SidebarProvider>
13
+ <TooltipProvider>{children}</TooltipProvider>
14
+ </SidebarProvider>
15
+ </NextThemesProvider>
16
+ )
17
+ }
components/sidebar-actions.tsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useRouter } from 'next/navigation'
4
+ import * as React from 'react'
5
+ import { toast } from 'react-hot-toast'
6
+
7
+ import { ServerActionResult, type Chat } from '@/lib/types'
8
+ import {
9
+ AlertDialog,
10
+ AlertDialogAction,
11
+ AlertDialogCancel,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogHeader,
16
+ AlertDialogTitle
17
+ } from '@/components/ui/alert-dialog'
18
+ import { Button } from '@/components/ui/button'
19
+ import { IconShare, IconSpinner, IconTrash } from '@/components/ui/icons'
20
+ import { ChatShareDialog } from '@/components/chat-share-dialog'
21
+ import {
22
+ Tooltip,
23
+ TooltipContent,
24
+ TooltipTrigger
25
+ } from '@/components/ui/tooltip'
26
+
27
+ interface SidebarActionsProps {
28
+ chat: Chat
29
+ removeChat: (args: { id: string; path: string }) => ServerActionResult<void>
30
+ shareChat: (id: string) => ServerActionResult<Chat>
31
+ }
32
+
33
+ export function SidebarActions({
34
+ chat,
35
+ removeChat,
36
+ shareChat
37
+ }: SidebarActionsProps) {
38
+ const router = useRouter()
39
+ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
40
+ const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
41
+ const [isRemovePending, startRemoveTransition] = React.useTransition()
42
+
43
+ return (
44
+ <>
45
+ <div className="space-x-1">
46
+ <Tooltip>
47
+ <TooltipTrigger asChild>
48
+ <Button
49
+ variant="ghost"
50
+ className="size-6 p-0 hover:bg-background"
51
+ onClick={() => setShareDialogOpen(true)}
52
+ >
53
+ <IconShare />
54
+ <span className="sr-only">Share</span>
55
+ </Button>
56
+ </TooltipTrigger>
57
+ <TooltipContent>Share chat</TooltipContent>
58
+ </Tooltip>
59
+ <Tooltip>
60
+ <TooltipTrigger asChild>
61
+ <Button
62
+ variant="ghost"
63
+ className="size-6 p-0 hover:bg-background"
64
+ disabled={isRemovePending}
65
+ onClick={() => setDeleteDialogOpen(true)}
66
+ >
67
+ <IconTrash />
68
+ <span className="sr-only">Delete</span>
69
+ </Button>
70
+ </TooltipTrigger>
71
+ <TooltipContent>Delete chat</TooltipContent>
72
+ </Tooltip>
73
+ </div>
74
+ <ChatShareDialog
75
+ chat={chat}
76
+ shareChat={shareChat}
77
+ open={shareDialogOpen}
78
+ onOpenChange={setShareDialogOpen}
79
+ onCopy={() => setShareDialogOpen(false)}
80
+ />
81
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
82
+ <AlertDialogContent>
83
+ <AlertDialogHeader>
84
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
85
+ <AlertDialogDescription>
86
+ This will permanently delete your chat message and remove your
87
+ data from our servers.
88
+ </AlertDialogDescription>
89
+ </AlertDialogHeader>
90
+ <AlertDialogFooter>
91
+ <AlertDialogCancel disabled={isRemovePending}>
92
+ Cancel
93
+ </AlertDialogCancel>
94
+ <AlertDialogAction
95
+ disabled={isRemovePending}
96
+ onClick={event => {
97
+ event.preventDefault()
98
+ // @ts-ignore
99
+ startRemoveTransition(async () => {
100
+ const result = await removeChat({
101
+ id: chat.id,
102
+ path: chat.path
103
+ })
104
+
105
+ if (result && 'error' in result) {
106
+ toast.error(result.error)
107
+ return
108
+ }
109
+
110
+ setDeleteDialogOpen(false)
111
+ router.refresh()
112
+ router.push('/')
113
+ toast.success('Chat deleted')
114
+ })
115
+ }}
116
+ >
117
+ {isRemovePending && <IconSpinner className="mr-2 animate-spin" />}
118
+ Delete
119
+ </AlertDialogAction>
120
+ </AlertDialogFooter>
121
+ </AlertDialogContent>
122
+ </AlertDialog>
123
+ </>
124
+ )
125
+ }
components/sidebar-desktop.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Sidebar } from '@/components/sidebar'
2
+
3
+ import { auth } from '@/auth'
4
+ import { ChatHistory } from '@/components/chat-history'
5
+
6
+ export async function SidebarDesktop() {
7
+ const session = await auth()
8
+
9
+ if (!session?.user?.id) {
10
+ return null
11
+ }
12
+
13
+ return null
14
+
15
+ // return (
16
+ // <Sidebar className="peer absolute inset-y-0 z-30 hidden -translate-x-full border-r bg-muted duration-300 ease-in-out data-[state=open]:translate-x-0 lg:flex lg:w-[250px] xl:w-[300px]">
17
+ // {/* @ts-ignore */}
18
+ // <ChatHistory userId={session.user.id} />
19
+ // </Sidebar>
20
+ // )
21
+ }
components/sidebar-footer.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from '@/lib/utils'
2
+
3
+ export function SidebarFooter({
4
+ children,
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<'div'>) {
8
+ return (
9
+ <div
10
+ className={cn('flex items-center justify-between p-4', className)}
11
+ {...props}
12
+ >
13
+ {children}
14
+ </div>
15
+ )
16
+ }
components/sidebar-item.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import Link from 'next/link'
6
+ import { usePathname } from 'next/navigation'
7
+
8
+ import { motion } from 'framer-motion'
9
+
10
+ import { buttonVariants } from '@/components/ui/button'
11
+ import { IconMessage, IconUsers } from '@/components/ui/icons'
12
+ import {
13
+ Tooltip,
14
+ TooltipContent,
15
+ TooltipTrigger
16
+ } from '@/components/ui/tooltip'
17
+ import { useLocalStorage } from '@/lib/hooks/use-local-storage'
18
+ import { type Chat } from '@/lib/types'
19
+ import { cn } from '@/lib/utils'
20
+
21
+ interface SidebarItemProps {
22
+ index: number
23
+ chat: Chat
24
+ children: React.ReactNode
25
+ }
26
+
27
+ export function SidebarItem({ index, chat, children }: SidebarItemProps) {
28
+ const pathname = usePathname()
29
+
30
+ const isActive = pathname === chat.path
31
+ const [newChatId, setNewChatId] = useLocalStorage('newChatId', null)
32
+ const shouldAnimate = index === 0 && isActive && newChatId
33
+
34
+ if (!chat?.id) return null
35
+
36
+ return (
37
+ <motion.div
38
+ className="relative h-8"
39
+ variants={{
40
+ initial: {
41
+ height: 0,
42
+ opacity: 0
43
+ },
44
+ animate: {
45
+ height: 'auto',
46
+ opacity: 1
47
+ }
48
+ }}
49
+ initial={shouldAnimate ? 'initial' : undefined}
50
+ animate={shouldAnimate ? 'animate' : undefined}
51
+ transition={{
52
+ duration: 0.25,
53
+ ease: 'easeIn'
54
+ }}
55
+ >
56
+ <div className="absolute left-2 top-1 flex size-6 items-center justify-center">
57
+ {chat.sharePath ? (
58
+ <Tooltip delayDuration={1000}>
59
+ <TooltipTrigger
60
+ tabIndex={-1}
61
+ className="focus:bg-muted focus:ring-1 focus:ring-ring"
62
+ >
63
+ <IconUsers className="mr-2" />
64
+ </TooltipTrigger>
65
+ <TooltipContent>This is a shared chat.</TooltipContent>
66
+ </Tooltip>
67
+ ) : (
68
+ <IconMessage className="mr-2" />
69
+ )}
70
+ </div>
71
+ <Link
72
+ href={chat.path}
73
+ className={cn(
74
+ buttonVariants({ variant: 'ghost' }),
75
+ 'group w-full px-8 transition-colors hover:bg-zinc-200/40 dark:hover:bg-zinc-300/10',
76
+ isActive && 'bg-zinc-200 pr-16 font-semibold dark:bg-zinc-800'
77
+ )}
78
+ >
79
+ <div
80
+ className="relative max-h-5 flex-1 select-none overflow-hidden text-ellipsis break-all"
81
+ title={chat.title}
82
+ >
83
+ <span className="whitespace-nowrap">
84
+ {shouldAnimate ? (
85
+ chat.title.split('').map((character, index) => (
86
+ <motion.span
87
+ key={index}
88
+ variants={{
89
+ initial: {
90
+ opacity: 0,
91
+ x: -100
92
+ },
93
+ animate: {
94
+ opacity: 1,
95
+ x: 0
96
+ }
97
+ }}
98
+ initial={shouldAnimate ? 'initial' : undefined}
99
+ animate={shouldAnimate ? 'animate' : undefined}
100
+ transition={{
101
+ duration: 0.25,
102
+ ease: 'easeIn',
103
+ delay: index * 0.05,
104
+ staggerChildren: 0.05
105
+ }}
106
+ onAnimationComplete={() => {
107
+ if (index === chat.title.length - 1) {
108
+ setNewChatId(null)
109
+ }
110
+ }}
111
+ >
112
+ {character}
113
+ </motion.span>
114
+ ))
115
+ ) : (
116
+ <span>{chat.title}</span>
117
+ )}
118
+ </span>
119
+ </div>
120
+ </Link>
121
+ {isActive && <div className="absolute right-2 top-1">{children}</div>}
122
+ </motion.div>
123
+ )
124
+ }
components/sidebar-items.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { Chat } from '@/lib/types'
4
+ import { AnimatePresence, motion } from 'framer-motion'
5
+
6
+ import { removeChat, shareChat } from '@/app/actions'
7
+
8
+ import { SidebarActions } from '@/components/sidebar-actions'
9
+ import { SidebarItem } from '@/components/sidebar-item'
10
+
11
+ interface SidebarItemsProps {
12
+ chats?: Chat[]
13
+ }
14
+
15
+ export function SidebarItems({ chats }: SidebarItemsProps) {
16
+ if (!chats?.length) return null
17
+
18
+ return (
19
+ <AnimatePresence>
20
+ {chats.map(
21
+ (chat, index) =>
22
+ chat && (
23
+ <motion.div
24
+ key={chat?.id}
25
+ exit={{
26
+ opacity: 0,
27
+ height: 0
28
+ }}
29
+ >
30
+ <SidebarItem index={index} chat={chat}>
31
+ <SidebarActions
32
+ chat={chat}
33
+ removeChat={removeChat}
34
+ shareChat={shareChat}
35
+ />
36
+ </SidebarItem>
37
+ </motion.div>
38
+ )
39
+ )}
40
+ </AnimatePresence>
41
+ )
42
+ }
components/sidebar-list.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { clearChats, getChats } from '@/app/actions'
2
+ import { ClearHistory } from '@/components/clear-history'
3
+ import { SidebarItems } from '@/components/sidebar-items'
4
+ import { ThemeToggle } from '@/components/theme-toggle'
5
+ import { cache } from 'react'
6
+
7
+ interface SidebarListProps {
8
+ userId?: string
9
+ children?: React.ReactNode
10
+ }
11
+
12
+ const loadChats = cache(async (userId?: string) => {
13
+ return await getChats(userId)
14
+ })
15
+
16
+ export async function SidebarList({ userId }: SidebarListProps) {
17
+ const chats = await loadChats(userId)
18
+
19
+ return (
20
+ <div className="flex flex-1 flex-col overflow-hidden">
21
+ <div className="flex-1 overflow-auto">
22
+ {chats?.length ? (
23
+ <div className="space-y-2 px-2">
24
+ <SidebarItems chats={chats} />
25
+ </div>
26
+ ) : (
27
+ <div className="p-8 text-center">
28
+ <p className="text-sm text-muted-foreground">No chat history</p>
29
+ </div>
30
+ )}
31
+ </div>
32
+ <div className="flex items-center justify-between p-4">
33
+ <ThemeToggle />
34
+ <ClearHistory clearChats={clearChats} isEnabled={chats?.length > 0} />
35
+ </div>
36
+ </div>
37
+ )
38
+ }
components/sidebar-mobile.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
4
+
5
+ import { Sidebar } from '@/components/sidebar'
6
+ import { Button } from '@/components/ui/button'
7
+
8
+ import { IconSidebar } from '@/components/ui/icons'
9
+
10
+ interface SidebarMobileProps {
11
+ children: React.ReactNode
12
+ }
13
+
14
+ export function SidebarMobile({ children }: SidebarMobileProps) {
15
+ return (
16
+ <Sheet>
17
+ <SheetTrigger asChild>
18
+ <Button variant="ghost" className="-ml-2 flex size-9 p-0 lg:hidden">
19
+ <IconSidebar className="size-6" />
20
+ <span className="sr-only">Toggle Sidebar</span>
21
+ </Button>
22
+ </SheetTrigger>
23
+ <SheetContent className="inset-y-0 flex h-auto w-[300px] flex-col p-0">
24
+ <Sidebar className="flex">{children}</Sidebar>
25
+ </SheetContent>
26
+ </Sheet>
27
+ )
28
+ }
components/sidebar-toggle.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { useSidebar } from '@/lib/hooks/use-sidebar'
6
+ import { Button } from '@/components/ui/button'
7
+ import { IconSidebar } from '@/components/ui/icons'
8
+
9
+ export function SidebarToggle() {
10
+ const { toggleSidebar } = useSidebar()
11
+
12
+ return (
13
+ <Button
14
+ variant="ghost"
15
+ className="-ml-2 hidden size-9 p-0 lg:flex"
16
+ onClick={() => {
17
+ toggleSidebar()
18
+ }}
19
+ >
20
+ <IconSidebar className="size-6" />
21
+ <span className="sr-only">Toggle Sidebar</span>
22
+ </Button>
23
+ )
24
+ }
components/sidebar.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { useSidebar } from '@/lib/hooks/use-sidebar'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ export interface SidebarProps extends React.ComponentProps<'div'> {}
9
+
10
+ export function Sidebar({ className, children }: SidebarProps) {
11
+ const { isSidebarOpen, isLoading } = useSidebar()
12
+
13
+ return (
14
+ <div
15
+ data-state={isSidebarOpen && !isLoading ? 'open' : 'closed'}
16
+ className={cn(className, 'h-full flex-col dark:bg-zinc-950')}
17
+ >
18
+ {children}
19
+ </div>
20
+ )
21
+ }
components/tailwind-indicator.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function TailwindIndicator() {
2
+ if (process.env.NODE_ENV === 'production') return null
3
+
4
+ return (
5
+ <div className="fixed bottom-1 left-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
6
+ <div className="block sm:hidden">xs</div>
7
+ <div className="hidden sm:block md:hidden">sm</div>
8
+ <div className="hidden md:block lg:hidden">md</div>
9
+ <div className="hidden lg:block xl:hidden">lg</div>
10
+ <div className="hidden xl:block 2xl:hidden">xl</div>
11
+ <div className="hidden 2xl:block">2xl</div>
12
+ </div>
13
+ )
14
+ }
components/theme-toggle.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useTheme } from 'next-themes'
5
+
6
+ import { Button } from '@/components/ui/button'
7
+ import { IconMoon, IconSun } from '@/components/ui/icons'
8
+
9
+ export function ThemeToggle() {
10
+ const { setTheme, theme } = useTheme()
11
+ const [_, startTransition] = React.useTransition()
12
+
13
+ return (
14
+ <Button
15
+ variant="ghost"
16
+ size="icon"
17
+ onClick={() => {
18
+ startTransition(() => {
19
+ setTheme(theme === 'light' ? 'dark' : 'light')
20
+ })
21
+ }}
22
+ >
23
+ {!theme ? null : theme === 'dark' ? (
24
+ <IconMoon className="transition-all" />
25
+ ) : (
26
+ <IconSun className="transition-all" />
27
+ )}
28
+ <span className="sr-only">Toggle theme</span>
29
+ </Button>
30
+ )
31
+ }
components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { buttonVariants } from '@/components/ui/button'
8
+
9
+ const AlertDialog = AlertDialogPrimitive.Root
10
+
11
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12
+
13
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
14
+
15
+ const AlertDialogOverlay = React.forwardRef<
16
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
17
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
18
+ >(({ className, ...props }, ref) => (
19
+ <AlertDialogPrimitive.Overlay
20
+ className={cn(
21
+ 'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
22
+ className
23
+ )}
24
+ {...props}
25
+ ref={ref}
26
+ />
27
+ ))
28
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29
+
30
+ const AlertDialogContent = React.forwardRef<
31
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
33
+ >(({ className, ...props }, ref) => (
34
+ <AlertDialogPortal>
35
+ <AlertDialogOverlay />
36
+ <AlertDialogPrimitive.Content
37
+ ref={ref}
38
+ className={cn(
39
+ 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ </AlertDialogPortal>
45
+ ))
46
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47
+
48
+ const AlertDialogHeader = ({
49
+ className,
50
+ ...props
51
+ }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn(
54
+ 'flex flex-col space-y-2 text-center sm:text-left',
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ )
60
+ AlertDialogHeader.displayName = 'AlertDialogHeader'
61
+
62
+ const AlertDialogFooter = ({
63
+ className,
64
+ ...props
65
+ }: React.HTMLAttributes<HTMLDivElement>) => (
66
+ <div
67
+ className={cn(
68
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
69
+ className
70
+ )}
71
+ {...props}
72
+ />
73
+ )
74
+ AlertDialogFooter.displayName = 'AlertDialogFooter'
75
+
76
+ const AlertDialogTitle = React.forwardRef<
77
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
78
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
79
+ >(({ className, ...props }, ref) => (
80
+ <AlertDialogPrimitive.Title
81
+ ref={ref}
82
+ className={cn('text-lg font-semibold', className)}
83
+ {...props}
84
+ />
85
+ ))
86
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87
+
88
+ const AlertDialogDescription = React.forwardRef<
89
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
90
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
91
+ >(({ className, ...props }, ref) => (
92
+ <AlertDialogPrimitive.Description
93
+ ref={ref}
94
+ className={cn('text-sm text-muted-foreground', className)}
95
+ {...props}
96
+ />
97
+ ))
98
+ AlertDialogDescription.displayName =
99
+ AlertDialogPrimitive.Description.displayName
100
+
101
+ const AlertDialogAction = React.forwardRef<
102
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
103
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
104
+ >(({ className, ...props }, ref) => (
105
+ <AlertDialogPrimitive.Action
106
+ ref={ref}
107
+ className={cn(buttonVariants(), className)}
108
+ {...props}
109
+ />
110
+ ))
111
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112
+
113
+ const AlertDialogCancel = React.forwardRef<
114
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
115
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
116
+ >(({ className, ...props }, ref) => (
117
+ <AlertDialogPrimitive.Cancel
118
+ ref={ref}
119
+ className={cn(
120
+ buttonVariants({ variant: 'outline' }),
121
+ 'mt-2 sm:mt-0',
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ ))
127
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128
+
129
+ export {
130
+ AlertDialog,
131
+ AlertDialogPortal,
132
+ AlertDialogOverlay,
133
+ AlertDialogTrigger,
134
+ AlertDialogContent,
135
+ AlertDialogHeader,
136
+ AlertDialogFooter,
137
+ AlertDialogTitle,
138
+ AlertDialogDescription,
139
+ AlertDialogAction,
140
+ AlertDialogCancel
141
+ }
components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const badgeVariants = cva(
7
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13
+ secondary:
14
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15
+ destructive:
16
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17
+ outline: 'text-foreground'
18
+ }
19
+ },
20
+ defaultVariants: {
21
+ variant: 'default'
22
+ }
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
components/ui/button.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14
+ destructive:
15
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16
+ outline:
17
+ 'border border-input hover:bg-accent hover:text-accent-foreground',
18
+ secondary:
19
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20
+ ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21
+ link: 'text-primary underline-offset-4 shadow-none hover:underline'
22
+ },
23
+ size: {
24
+ default: 'h-8 px-4 py-2',
25
+ sm: 'h-8 rounded-md px-3',
26
+ lg: 'h-11 rounded-md px-8',
27
+ icon: 'size-8 p-0'
28
+ }
29
+ },
30
+ defaultVariants: {
31
+ variant: 'default',
32
+ size: 'default'
33
+ }
34
+ }
35
+ )
36
+
37
+ export interface ButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean
41
+ }
42
+
43
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : 'button'
46
+ return (
47
+ <Comp
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ ref={ref}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+ )
55
+ Button.displayName = 'Button'
56
+
57
+ export { Button, buttonVariants }
components/ui/codeblock.tsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Inspired by Chatbot-UI and modified to fit the needs of this project
2
+ // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
3
+
4
+ 'use client'
5
+
6
+ import { FC, memo } from 'react'
7
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
8
+ import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
9
+
10
+ import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
11
+ import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
12
+ import { Button } from '@/components/ui/button'
13
+
14
+ interface Props {
15
+ language: string
16
+ value: string
17
+ }
18
+
19
+ interface languageMap {
20
+ [key: string]: string | undefined
21
+ }
22
+
23
+ export const programmingLanguages: languageMap = {
24
+ javascript: '.js',
25
+ python: '.py',
26
+ java: '.java',
27
+ c: '.c',
28
+ cpp: '.cpp',
29
+ 'c++': '.cpp',
30
+ 'c#': '.cs',
31
+ ruby: '.rb',
32
+ php: '.php',
33
+ swift: '.swift',
34
+ 'objective-c': '.m',
35
+ kotlin: '.kt',
36
+ typescript: '.ts',
37
+ go: '.go',
38
+ perl: '.pl',
39
+ rust: '.rs',
40
+ scala: '.scala',
41
+ haskell: '.hs',
42
+ lua: '.lua',
43
+ shell: '.sh',
44
+ sql: '.sql',
45
+ html: '.html',
46
+ css: '.css'
47
+ // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48
+ }
49
+
50
+ export const generateRandomString = (length: number, lowercase = false) => {
51
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
52
+ let result = ''
53
+ for (let i = 0; i < length; i++) {
54
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
55
+ }
56
+ return lowercase ? result.toLowerCase() : result
57
+ }
58
+
59
+ const CodeBlock: FC<Props> = memo(({ language, value }) => {
60
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
61
+
62
+ const downloadAsFile = () => {
63
+ if (typeof window === 'undefined') {
64
+ return
65
+ }
66
+ const fileExtension = programmingLanguages[language] || '.file'
67
+ const suggestedFileName = `file-${generateRandomString(
68
+ 3,
69
+ true
70
+ )}${fileExtension}`
71
+ const fileName = window.prompt('Enter file name' || '', suggestedFileName)
72
+
73
+ if (!fileName) {
74
+ // User pressed cancel on prompt.
75
+ return
76
+ }
77
+
78
+ const blob = new Blob([value], { type: 'text/plain' })
79
+ const url = URL.createObjectURL(blob)
80
+ const link = document.createElement('a')
81
+ link.download = fileName
82
+ link.href = url
83
+ link.style.display = 'none'
84
+ document.body.appendChild(link)
85
+ link.click()
86
+ document.body.removeChild(link)
87
+ URL.revokeObjectURL(url)
88
+ }
89
+
90
+ const onCopy = () => {
91
+ if (isCopied) return
92
+ copyToClipboard(value)
93
+ }
94
+
95
+ return (
96
+ <div className="relative w-full font-sans codeblock bg-zinc-950">
97
+ <div className="flex items-center justify-between w-full px-6 py-2 pr-4 bg-zinc-800 text-zinc-100">
98
+ <span className="text-xs lowercase">{language}</span>
99
+ <div className="flex items-center space-x-1">
100
+ <Button
101
+ variant="ghost"
102
+ className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
103
+ onClick={downloadAsFile}
104
+ size="icon"
105
+ >
106
+ <IconDownload />
107
+ <span className="sr-only">Download</span>
108
+ </Button>
109
+ <Button
110
+ variant="ghost"
111
+ size="icon"
112
+ className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
113
+ onClick={onCopy}
114
+ >
115
+ {isCopied ? <IconCheck /> : <IconCopy />}
116
+ <span className="sr-only">Copy code</span>
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ <SyntaxHighlighter
121
+ language={language}
122
+ style={coldarkDark}
123
+ PreTag="div"
124
+ showLineNumbers
125
+ customStyle={{
126
+ margin: 0,
127
+ width: '100%',
128
+ background: 'transparent',
129
+ padding: '1.5rem 1rem'
130
+ }}
131
+ lineNumberStyle={{
132
+ userSelect: "none",
133
+ }}
134
+ codeTagProps={{
135
+ style: {
136
+ fontSize: '0.9rem',
137
+ fontFamily: 'var(--font-mono)'
138
+ }
139
+ }}
140
+ >
141
+ {value}
142
+ </SyntaxHighlighter>
143
+ </div>
144
+ )
145
+ })
146
+ CodeBlock.displayName = 'CodeBlock'
147
+
148
+ export { CodeBlock }
components/ui/dialog.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { IconClose } from '@/components/ui/icons'
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = DialogPrimitive.Portal
14
+
15
+ const DialogClose = DialogPrimitive.Close
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ 'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ ))
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
+ <IconClose className="size-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ))
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName
55
+
56
+ const DialogHeader = ({
57
+ className,
58
+ ...props
59
+ }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div
61
+ className={cn(
62
+ 'flex flex-col space-y-1.5 text-center sm:text-left',
63
+ className
64
+ )}
65
+ {...props}
66
+ />
67
+ )
68
+ DialogHeader.displayName = 'DialogHeader'
69
+
70
+ const DialogFooter = ({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) => (
74
+ <div
75
+ className={cn(
76
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ DialogFooter.displayName = 'DialogFooter'
83
+
84
+ const DialogTitle = React.forwardRef<
85
+ React.ElementRef<typeof DialogPrimitive.Title>,
86
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87
+ >(({ className, ...props }, ref) => (
88
+ <DialogPrimitive.Title
89
+ ref={ref}
90
+ className={cn(
91
+ 'text-lg font-semibold leading-none tracking-tight',
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
98
+
99
+ const DialogDescription = React.forwardRef<
100
+ React.ElementRef<typeof DialogPrimitive.Description>,
101
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102
+ >(({ className, ...props }, ref) => (
103
+ <DialogPrimitive.Description
104
+ ref={ref}
105
+ className={cn('text-sm text-muted-foreground', className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
110
+
111
+ export {
112
+ Dialog,
113
+ DialogPortal,
114
+ DialogOverlay,
115
+ DialogClose,
116
+ DialogTrigger,
117
+ DialogContent,
118
+ DialogHeader,
119
+ DialogFooter,
120
+ DialogTitle,
121
+ DialogDescription
122
+ }
components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const DropdownMenu = DropdownMenuPrimitive.Root
9
+
10
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
11
+
12
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
13
+
14
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
15
+
16
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
17
+
18
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
19
+
20
+ const DropdownMenuSubContent = React.forwardRef<
21
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
22
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
23
+ >(({ className, ...props }, ref) => (
24
+ <DropdownMenuPrimitive.SubContent
25
+ ref={ref}
26
+ className={cn(
27
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
28
+ className
29
+ )}
30
+ {...props}
31
+ />
32
+ ))
33
+ DropdownMenuSubContent.displayName =
34
+ DropdownMenuPrimitive.SubContent.displayName
35
+
36
+ const DropdownMenuContent = React.forwardRef<
37
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
38
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
39
+ >(({ className, sideOffset = 4, ...props }, ref) => (
40
+ <DropdownMenuPrimitive.Portal>
41
+ <DropdownMenuPrimitive.Content
42
+ ref={ref}
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </DropdownMenuPrimitive.Portal>
51
+ ))
52
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
53
+
54
+ const DropdownMenuItem = React.forwardRef<
55
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
56
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
57
+ inset?: boolean
58
+ }
59
+ >(({ className, inset, ...props }, ref) => (
60
+ <DropdownMenuPrimitive.Item
61
+ ref={ref}
62
+ className={cn(
63
+ 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
64
+ inset && 'pl-8',
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ ))
70
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
71
+
72
+ const DropdownMenuLabel = React.forwardRef<
73
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
74
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
75
+ inset?: boolean
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <DropdownMenuPrimitive.Label
79
+ ref={ref}
80
+ className={cn(
81
+ 'px-2 py-1.5 text-sm font-semibold',
82
+ inset && 'pl-8',
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ ))
88
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
89
+
90
+ const DropdownMenuSeparator = React.forwardRef<
91
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
92
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
93
+ >(({ className, ...props }, ref) => (
94
+ <DropdownMenuPrimitive.Separator
95
+ ref={ref}
96
+ className={cn('-mx-1 my-1 h-px bg-muted', className)}
97
+ {...props}
98
+ />
99
+ ))
100
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
101
+
102
+ const DropdownMenuShortcut = ({
103
+ className,
104
+ ...props
105
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
106
+ return (
107
+ <span
108
+ className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
109
+ {...props}
110
+ />
111
+ )
112
+ }
113
+ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
114
+
115
+ export {
116
+ DropdownMenu,
117
+ DropdownMenuTrigger,
118
+ DropdownMenuContent,
119
+ DropdownMenuItem,
120
+ DropdownMenuLabel,
121
+ DropdownMenuSeparator,
122
+ DropdownMenuShortcut,
123
+ DropdownMenuGroup,
124
+ DropdownMenuPortal,
125
+ DropdownMenuSub,
126
+ DropdownMenuSubContent,
127
+ DropdownMenuRadioGroup
128
+ }
components/ui/icons.tsx ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ function IconNextChat({
8
+ className,
9
+ inverted,
10
+ ...props
11
+ }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12
+ const id = React.useId()
13
+
14
+ return (
15
+ <svg
16
+ viewBox="0 0 17 17"
17
+ fill="none"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ className={cn('size-4', className)}
20
+ {...props}
21
+ >
22
+ <defs>
23
+ <linearGradient
24
+ id={`gradient-${id}-1`}
25
+ x1="10.6889"
26
+ y1="10.3556"
27
+ x2="13.8445"
28
+ y2="14.2667"
29
+ gradientUnits="userSpaceOnUse"
30
+ >
31
+ <stop stopColor={inverted ? 'white' : 'black'} />
32
+ <stop
33
+ offset={1}
34
+ stopColor={inverted ? 'white' : 'black'}
35
+ stopOpacity={0}
36
+ />
37
+ </linearGradient>
38
+ <linearGradient
39
+ id={`gradient-${id}-2`}
40
+ x1="11.7555"
41
+ y1="4.8"
42
+ x2="11.7376"
43
+ y2="9.50002"
44
+ gradientUnits="userSpaceOnUse"
45
+ >
46
+ <stop stopColor={inverted ? 'white' : 'black'} />
47
+ <stop
48
+ offset={1}
49
+ stopColor={inverted ? 'white' : 'black'}
50
+ stopOpacity={0}
51
+ />
52
+ </linearGradient>
53
+ </defs>
54
+ <path
55
+ d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
56
+ fill={inverted ? 'black' : 'white'}
57
+ stroke={inverted ? 'black' : 'white'}
58
+ strokeWidth={2}
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ />
62
+ <mask
63
+ id="mask0_91_2047"
64
+ style={{ maskType: 'alpha' }}
65
+ maskUnits="userSpaceOnUse"
66
+ x={1}
67
+ y={0}
68
+ width={16}
69
+ height={16}
70
+ >
71
+ <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
72
+ </mask>
73
+ <g mask="url(#mask0_91_2047)">
74
+ <circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
75
+ <path
76
+ d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
77
+ fill={`url(#gradient-${id}-1)`}
78
+ />
79
+ <rect
80
+ x="11.2222"
81
+ y="4.8"
82
+ width="1.06667"
83
+ height="6.4"
84
+ fill={`url(#gradient-${id}-2)`}
85
+ />
86
+ </g>
87
+ </svg>
88
+ )
89
+ }
90
+
91
+ function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92
+ return (
93
+ <svg
94
+ fill="currentColor"
95
+ viewBox="0 0 24 24"
96
+ role="img"
97
+ xmlns="http://www.w3.org/2000/svg"
98
+ className={cn('size-4', className)}
99
+ {...props}
100
+ >
101
+ <title>OpenAI icon</title>
102
+ <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
103
+ </svg>
104
+ )
105
+ }
106
+
107
+ function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
108
+ return (
109
+ <svg
110
+ aria-label="Vercel logomark"
111
+ role="img"
112
+ viewBox="0 0 74 64"
113
+ className={cn('size-4', className)}
114
+ {...props}
115
+ >
116
+ <path
117
+ d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
118
+ fill="currentColor"
119
+ ></path>
120
+ </svg>
121
+ )
122
+ }
123
+
124
+ function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
125
+ return (
126
+ <svg
127
+ role="img"
128
+ viewBox="0 0 24 24"
129
+ xmlns="http://www.w3.org/2000/svg"
130
+ fill="currentColor"
131
+ className={cn('size-4', className)}
132
+ {...props}
133
+ >
134
+ <title>GitHub</title>
135
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
136
+ </svg>
137
+ )
138
+ }
139
+
140
+ function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
141
+ return (
142
+ <svg
143
+ fill="none"
144
+ shapeRendering="geometricPrecision"
145
+ stroke="currentColor"
146
+ strokeLinecap="round"
147
+ strokeLinejoin="round"
148
+ strokeWidth="1"
149
+ viewBox="0 0 24 24"
150
+ aria-hidden="true"
151
+ className={cn('size-4', className)}
152
+ {...props}
153
+ >
154
+ <path d="M16.88 3.549L7.12 20.451"></path>
155
+ </svg>
156
+ )
157
+ }
158
+
159
+ function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
160
+ return (
161
+ <svg
162
+ xmlns="http://www.w3.org/2000/svg"
163
+ viewBox="0 0 256 256"
164
+ fill="currentColor"
165
+ className={cn('size-4', className)}
166
+ {...props}
167
+ >
168
+ <path d="m205.66 149.66-72 72a8 8 0 0 1-11.32 0l-72-72a8 8 0 0 1 11.32-11.32L120 196.69V40a8 8 0 0 1 16 0v156.69l58.34-58.35a8 8 0 0 1 11.32 11.32Z" />
169
+ </svg>
170
+ )
171
+ }
172
+
173
+ function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
174
+ return (
175
+ <svg
176
+ xmlns="http://www.w3.org/2000/svg"
177
+ viewBox="0 0 256 256"
178
+ fill="currentColor"
179
+ className={cn('size-4', className)}
180
+ {...props}
181
+ >
182
+ <path d="m221.66 133.66-72 72a8 8 0 0 1-11.32-11.32L196.69 136H40a8 8 0 0 1 0-16h156.69l-58.35-58.34a8 8 0 0 1 11.32-11.32l72 72a8 8 0 0 1 0 11.32Z" />
183
+ </svg>
184
+ )
185
+ }
186
+
187
+ function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
188
+ return (
189
+ <svg
190
+ xmlns="http://www.w3.org/2000/svg"
191
+ viewBox="0 0 256 256"
192
+ fill="currentColor"
193
+ className={cn('size-4', className)}
194
+ {...props}
195
+ >
196
+ <path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z" />
197
+ </svg>
198
+ )
199
+ }
200
+
201
+ function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
202
+ return (
203
+ <svg
204
+ xmlns="http://www.w3.org/2000/svg"
205
+ viewBox="0 0 256 256"
206
+ fill="currentColor"
207
+ className={cn('size-4', className)}
208
+ {...props}
209
+ >
210
+ <path d="M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8Z" />
211
+ </svg>
212
+ )
213
+ }
214
+
215
+ function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
216
+ return (
217
+ <svg
218
+ xmlns="http://www.w3.org/2000/svg"
219
+ viewBox="0 0 256 256"
220
+ fill="currentColor"
221
+ className={cn('size-4', className)}
222
+ {...props}
223
+ >
224
+ <path d="M200 32v144a8 8 0 0 1-8 8H67.31l34.35 34.34a8 8 0 0 1-11.32 11.32l-48-48a8 8 0 0 1 0-11.32l48-48a8 8 0 0 1 11.32 11.32L67.31 168H184V32a8 8 0 0 1 16 0Z" />
225
+ </svg>
226
+ )
227
+ }
228
+
229
+ function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
230
+ return (
231
+ <svg
232
+ xmlns="http://www.w3.org/2000/svg"
233
+ viewBox="0 0 256 256"
234
+ fill="currentColor"
235
+ className={cn('size-4 animate-spin', className)}
236
+ {...props}
237
+ >
238
+ <path d="M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128Z" />
239
+ </svg>
240
+ )
241
+ }
242
+
243
+ function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
244
+ return (
245
+ <svg
246
+ xmlns="http://www.w3.org/2000/svg"
247
+ viewBox="0 0 256 256"
248
+ fill="currentColor"
249
+ className={cn('size-4', className)}
250
+ {...props}
251
+ >
252
+ <path d="M216 48H40a16 16 0 0 0-16 16v160a15.84 15.84 0 0 0 9.25 14.5A16.05 16.05 0 0 0 40 240a15.89 15.89 0 0 0 10.25-3.78.69.69 0 0 0 .13-.11L82.5 208H216a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16ZM40 224Zm176-32H82.5a16 16 0 0 0-10.3 3.75l-.12.11L40 224V64h176Z" />
253
+ </svg>
254
+ )
255
+ }
256
+
257
+ function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
258
+ return (
259
+ <svg
260
+ xmlns="http://www.w3.org/2000/svg"
261
+ viewBox="0 0 256 256"
262
+ fill="currentColor"
263
+ className={cn('size-4', className)}
264
+ {...props}
265
+ >
266
+ <path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16ZM96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Zm48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0Z" />
267
+ </svg>
268
+ )
269
+ }
270
+
271
+ function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
272
+ return (
273
+ <svg
274
+ xmlns="http://www.w3.org/2000/svg"
275
+ viewBox="0 0 256 256"
276
+ fill="currentColor"
277
+ className={cn('size-4', className)}
278
+ {...props}
279
+ >
280
+ <path d="M197.67 186.37a8 8 0 0 1 0 11.29C196.58 198.73 170.82 224 128 224c-37.39 0-64.53-22.4-80-39.85V208a8 8 0 0 1-16 0v-48a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16H55.44C67.76 183.35 93 208 128 208c36 0 58.14-21.46 58.36-21.68a8 8 0 0 1 11.31.05ZM216 40a8 8 0 0 0-8 8v23.85C192.53 54.4 165.39 32 128 32c-42.82 0-68.58 25.27-69.66 26.34a8 8 0 0 0 11.3 11.34C69.86 69.46 92 48 128 48c35 0 60.24 24.65 72.56 40H168a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8Z" />
281
+ </svg>
282
+ )
283
+ }
284
+
285
+ function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
286
+ return (
287
+ <svg
288
+ xmlns="http://www.w3.org/2000/svg"
289
+ viewBox="0 0 256 256"
290
+ fill="currentColor"
291
+ className={cn('size-4', className)}
292
+ {...props}
293
+ >
294
+ <path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm0 192a88 88 0 1 1 88-88 88.1 88.1 0 0 1-88 88Zm24-120h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8Zm-8 48h-32v-32h32Z" />
295
+ </svg>
296
+ )
297
+ }
298
+
299
+ function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
300
+ return (
301
+ <svg
302
+ xmlns="http://www.w3.org/2000/svg"
303
+ viewBox="0 0 256 256"
304
+ fill="currentColor"
305
+ className={cn('size-4', className)}
306
+ {...props}
307
+ >
308
+ <path d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16ZM40 56h40v144H40Zm176 144H96V56h120v144Z" />
309
+ </svg>
310
+ )
311
+ }
312
+
313
+ function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
314
+ return (
315
+ <svg
316
+ xmlns="http://www.w3.org/2000/svg"
317
+ viewBox="0 0 256 256"
318
+ fill="currentColor"
319
+ className={cn('size-4', className)}
320
+ {...props}
321
+ >
322
+ <path d="M233.54 142.23a8 8 0 0 0-8-2 88.08 88.08 0 0 1-109.8-109.8 8 8 0 0 0-10-10 104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.09 103.09 0 0 0 62.52-20.88 104.84 104.84 0 0 0 37-52.91 8 8 0 0 0-1.98-7.98Zm-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104 106 106 0 0 0 14.92-1.06 89 89 0 0 1-26.02 31.4Z" />
323
+ </svg>
324
+ )
325
+ }
326
+
327
+ function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
328
+ return (
329
+ <svg
330
+ xmlns="http://www.w3.org/2000/svg"
331
+ viewBox="0 0 256 256"
332
+ fill="currentColor"
333
+ className={cn('size-4', className)}
334
+ {...props}
335
+ >
336
+ <path d="M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0Zm72 88a64 64 0 1 1-64-64 64.07 64.07 0 0 1 64 64Zm-16 0a48 48 0 1 0-48 48 48.05 48.05 0 0 0 48-48ZM58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32ZM192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72Zm5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8Zm80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8Zm112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16Z" />
337
+ </svg>
338
+ )
339
+ }
340
+
341
+ function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
342
+ return (
343
+ <svg
344
+ xmlns="http://www.w3.org/2000/svg"
345
+ viewBox="0 0 256 256"
346
+ fill="currentColor"
347
+ className={cn('size-4', className)}
348
+ {...props}
349
+ >
350
+ <path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
351
+ </svg>
352
+ )
353
+ }
354
+
355
+ function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
356
+ return (
357
+ <svg
358
+ xmlns="http://www.w3.org/2000/svg"
359
+ viewBox="0 0 256 256"
360
+ fill="currentColor"
361
+ className={cn('size-4', className)}
362
+ {...props}
363
+ >
364
+ <path d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z" />
365
+ </svg>
366
+ )
367
+ }
368
+
369
+ function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
370
+ return (
371
+ <svg
372
+ xmlns="http://www.w3.org/2000/svg"
373
+ viewBox="0 0 256 256"
374
+ fill="currentColor"
375
+ className={cn('size-4', className)}
376
+ {...props}
377
+ >
378
+ <path d="M224 152v56a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-56a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0Zm-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 132.69V40a8 8 0 0 0-16 0v92.69l-26.34-26.35a8 8 0 0 0-11.32 11.32Z" />
379
+ </svg>
380
+ )
381
+ }
382
+
383
+ function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
384
+ return (
385
+ <svg
386
+ xmlns="http://www.w3.org/2000/svg"
387
+ viewBox="0 0 256 256"
388
+ fill="currentColor"
389
+ className={cn('size-4', className)}
390
+ {...props}
391
+ >
392
+ <path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128 50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z" />
393
+ </svg>
394
+ )
395
+ }
396
+
397
+ function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
398
+ return (
399
+ <svg
400
+ xmlns="http://www.w3.org/2000/svg"
401
+ fill="none"
402
+ viewBox="0 0 24 24"
403
+ strokeWidth={1.5}
404
+ stroke="currentColor"
405
+ className={cn('size-4', className)}
406
+ {...props}
407
+ >
408
+ <path
409
+ strokeLinecap="round"
410
+ strokeLinejoin="round"
411
+ d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
412
+ />
413
+ </svg>
414
+ )
415
+ }
416
+
417
+ function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
418
+ return (
419
+ <svg
420
+ xmlns="http://www.w3.org/2000/svg"
421
+ fill="currentColor"
422
+ className={cn('size-4', className)}
423
+ viewBox="0 0 256 256"
424
+ {...props}
425
+ >
426
+ <path d="m237.66 106.35-80-80A8 8 0 0 0 144 32v40.35c-25.94 2.22-54.59 14.92-78.16 34.91-28.38 24.08-46.05 55.11-49.76 87.37a12 12 0 0 0 20.68 9.58c11-11.71 50.14-48.74 107.24-52V192a8 8 0 0 0 13.66 5.65l80-80a8 8 0 0 0 0-11.3ZM160 172.69V144a8 8 0 0 0-8-8c-28.08 0-55.43 7.33-81.29 21.8a196.17 196.17 0 0 0-36.57 26.52c5.8-23.84 20.42-46.51 42.05-64.86C99.41 99.77 127.75 88 152 88a8 8 0 0 0 8-8V51.32L220.69 112Z" />
427
+ </svg>
428
+ )
429
+ }
430
+
431
+ function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
432
+ return (
433
+ <svg
434
+ xmlns="http://www.w3.org/2000/svg"
435
+ fill="currentColor"
436
+ className={cn('size-4', className)}
437
+ viewBox="0 0 256 256"
438
+ {...props}
439
+ >
440
+ <path d="M117.25 157.92a60 60 0 1 0-66.5 0 95.83 95.83 0 0 0-47.22 37.71 8 8 0 1 0 13.4 8.74 80 80 0 0 1 134.14 0 8 8 0 0 0 13.4-8.74 95.83 95.83 0 0 0-47.22-37.71ZM40 108a44 44 0 1 1 44 44 44.05 44.05 0 0 1-44-44Zm210.14 98.7a8 8 0 0 1-11.07-2.33A79.83 79.83 0 0 0 172 168a8 8 0 0 1 0-16 44 44 0 1 0-16.34-84.87 8 8 0 1 1-5.94-14.85 60 60 0 0 1 55.53 105.64 95.83 95.83 0 0 1 47.22 37.71 8 8 0 0 1-2.33 11.07Z" />
441
+ </svg>
442
+ )
443
+ }
444
+
445
+ function IconExternalLink({
446
+ className,
447
+ ...props
448
+ }: React.ComponentProps<'svg'>) {
449
+ return (
450
+ <svg
451
+ xmlns="http://www.w3.org/2000/svg"
452
+ fill="currentColor"
453
+ className={cn('size-4', className)}
454
+ viewBox="0 0 256 256"
455
+ {...props}
456
+ >
457
+ <path d="M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8Z" />
458
+ </svg>
459
+ )
460
+ }
461
+
462
+ function IconChevronUpDown({
463
+ className,
464
+ ...props
465
+ }: React.ComponentProps<'svg'>) {
466
+ return (
467
+ <svg
468
+ xmlns="http://www.w3.org/2000/svg"
469
+ fill="currentColor"
470
+ className={cn('size-4', className)}
471
+ viewBox="0 0 256 256"
472
+ {...props}
473
+ >
474
+ <path d="M181.66 170.34a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-48-48a8 8 0 0 1 11.32-11.32L128 212.69l42.34-42.35a8 8 0 0 1 11.32 0Zm-96-84.68L128 43.31l42.34 42.35a8 8 0 0 0 11.32-11.32l-48-48a8 8 0 0 0-11.32 0l-48 48a8 8 0 0 0 11.32 11.32Z" />
475
+ </svg>
476
+ )
477
+ }
478
+
479
+ export {
480
+ IconEdit,
481
+ IconNextChat,
482
+ IconOpenAI,
483
+ IconVercel,
484
+ IconGitHub,
485
+ IconSeparator,
486
+ IconArrowDown,
487
+ IconArrowRight,
488
+ IconUser,
489
+ IconPlus,
490
+ IconArrowElbow,
491
+ IconSpinner,
492
+ IconMessage,
493
+ IconTrash,
494
+ IconRefresh,
495
+ IconStop,
496
+ IconSidebar,
497
+ IconMoon,
498
+ IconSun,
499
+ IconCopy,
500
+ IconCheck,
501
+ IconDownload,
502
+ IconClose,
503
+ IconShare,
504
+ IconUsers,
505
+ IconExternalLink,
506
+ IconChevronUpDown
507
+ }
components/ui/input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+ )
23
+ Input.displayName = 'Input'
24
+
25
+ export { Input }
components/ui/label.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const labelVariants = cva(
10
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11
+ )
12
+
13
+ const Label = React.forwardRef<
14
+ React.ElementRef<typeof LabelPrimitive.Root>,
15
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
16
+ VariantProps<typeof labelVariants>
17
+ >(({ className, ...props }, ref) => (
18
+ <LabelPrimitive.Root
19
+ ref={ref}
20
+ className={cn(labelVariants(), className)}
21
+ {...props}
22
+ />
23
+ ))
24
+ Label.displayName = LabelPrimitive.Root.displayName
25
+
26
+ export { Label }
components/ui/select.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as SelectPrimitive from '@radix-ui/react-select'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import {
8
+ IconArrowDown,
9
+ IconCheck,
10
+ IconChevronUpDown
11
+ } from '@/components/ui/icons'
12
+
13
+ const Select = SelectPrimitive.Root
14
+
15
+ const SelectGroup = SelectPrimitive.Group
16
+
17
+ const SelectValue = SelectPrimitive.Value
18
+
19
+ const SelectTrigger = React.forwardRef<
20
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
21
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
22
+ >(({ className, children, ...props }, ref) => (
23
+ <SelectPrimitive.Trigger
24
+ ref={ref}
25
+ className={cn(
26
+ 'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
27
+ className
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ <SelectPrimitive.Icon asChild>
33
+ <IconChevronUpDown className="opacity-50" />
34
+ </SelectPrimitive.Icon>
35
+ </SelectPrimitive.Trigger>
36
+ ))
37
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
38
+
39
+ const SelectContent = React.forwardRef<
40
+ React.ElementRef<typeof SelectPrimitive.Content>,
41
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
42
+ >(({ className, children, position = 'popper', ...props }, ref) => (
43
+ <SelectPrimitive.Portal>
44
+ <SelectPrimitive.Content
45
+ ref={ref}
46
+ className={cn(
47
+ 'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80',
48
+ position === 'popper' && 'translate-y-1',
49
+ className
50
+ )}
51
+ position={position}
52
+ {...props}
53
+ >
54
+ <SelectPrimitive.Viewport
55
+ className={cn(
56
+ 'p-1',
57
+ position === 'popper' &&
58
+ 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
59
+ )}
60
+ >
61
+ {children}
62
+ </SelectPrimitive.Viewport>
63
+ </SelectPrimitive.Content>
64
+ </SelectPrimitive.Portal>
65
+ ))
66
+ SelectContent.displayName = SelectPrimitive.Content.displayName
67
+
68
+ const SelectLabel = React.forwardRef<
69
+ React.ElementRef<typeof SelectPrimitive.Label>,
70
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
71
+ >(({ className, ...props }, ref) => (
72
+ <SelectPrimitive.Label
73
+ ref={ref}
74
+ className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
75
+ {...props}
76
+ />
77
+ ))
78
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
79
+
80
+ const SelectItem = React.forwardRef<
81
+ React.ElementRef<typeof SelectPrimitive.Item>,
82
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
83
+ >(({ className, children, ...props }, ref) => (
84
+ <SelectPrimitive.Item
85
+ ref={ref}
86
+ className={cn(
87
+ 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
88
+ className
89
+ )}
90
+ {...props}
91
+ >
92
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
93
+ <SelectPrimitive.ItemIndicator>
94
+ <IconCheck className="size-4" />
95
+ </SelectPrimitive.ItemIndicator>
96
+ </span>
97
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
98
+ </SelectPrimitive.Item>
99
+ ))
100
+ SelectItem.displayName = SelectPrimitive.Item.displayName
101
+
102
+ const SelectSeparator = React.forwardRef<
103
+ React.ElementRef<typeof SelectPrimitive.Separator>,
104
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
105
+ >(({ className, ...props }, ref) => (
106
+ <SelectPrimitive.Separator
107
+ ref={ref}
108
+ className={cn('-mx-1 my-1 h-px bg-muted', className)}
109
+ {...props}
110
+ />
111
+ ))
112
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
113
+
114
+ export {
115
+ Select,
116
+ SelectGroup,
117
+ SelectValue,
118
+ SelectTrigger,
119
+ SelectContent,
120
+ SelectLabel,
121
+ SelectItem,
122
+ SelectSeparator
123
+ }
components/ui/separator.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as SeparatorPrimitive from '@radix-ui/react-separator'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const Separator = React.forwardRef<
9
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
+ >(
12
+ (
13
+ { className, orientation = 'horizontal', decorative = true, ...props },
14
+ ref
15
+ ) => (
16
+ <SeparatorPrimitive.Root
17
+ ref={ref}
18
+ decorative={decorative}
19
+ orientation={orientation}
20
+ className={cn(
21
+ 'shrink-0 bg-border',
22
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ )
29
+ Separator.displayName = SeparatorPrimitive.Root.displayName
30
+
31
+ export { Separator }
components/ui/sheet.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as SheetPrimitive from '@radix-ui/react-dialog'
5
+ import { cva, type VariantProps } from 'class-variance-authority'
6
+ import { IconClose } from '@/components/ui/icons'
7
+
8
+ import { cn } from '@/lib/utils'
9
+
10
+ const Sheet = SheetPrimitive.Root
11
+
12
+ const SheetTrigger = SheetPrimitive.Trigger
13
+
14
+ const SheetClose = SheetPrimitive.Close
15
+
16
+ const SheetPortal = SheetPrimitive.Portal
17
+
18
+ const SheetOverlay = React.forwardRef<
19
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <SheetPrimitive.Overlay
23
+ className={cn(
24
+ 'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
25
+ className
26
+ )}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ ))
31
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32
+
33
+ const sheetVariants = cva(
34
+ 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
35
+ {
36
+ variants: {
37
+ side: {
38
+ top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
39
+ bottom:
40
+ 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
41
+ left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
42
+ right:
43
+ 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm'
44
+ }
45
+ },
46
+ defaultVariants: {
47
+ side: 'right'
48
+ }
49
+ }
50
+ )
51
+
52
+ interface SheetContentProps
53
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54
+ VariantProps<typeof sheetVariants> {}
55
+
56
+ const SheetContent = React.forwardRef<
57
+ React.ElementRef<typeof SheetPrimitive.Content>,
58
+ SheetContentProps
59
+ >(({ side = 'left', className, children, ...props }, ref) => (
60
+ <SheetPortal>
61
+ <SheetOverlay />
62
+ <SheetPrimitive.Content
63
+ ref={ref}
64
+ className={cn(sheetVariants({ side }), className)}
65
+ {...props}
66
+ >
67
+ {children}
68
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
69
+ <IconClose className="size-4" />
70
+ <span className="sr-only">Close</span>
71
+ </SheetPrimitive.Close>
72
+ </SheetPrimitive.Content>
73
+ </SheetPortal>
74
+ ))
75
+ SheetContent.displayName = SheetPrimitive.Content.displayName
76
+
77
+ const SheetHeader = ({
78
+ className,
79
+ ...props
80
+ }: React.HTMLAttributes<HTMLDivElement>) => (
81
+ <div
82
+ className={cn(
83
+ 'flex flex-col space-y-2 text-center sm:text-left',
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ )
89
+ SheetHeader.displayName = 'SheetHeader'
90
+
91
+ const SheetFooter = ({
92
+ className,
93
+ ...props
94
+ }: React.HTMLAttributes<HTMLDivElement>) => (
95
+ <div
96
+ className={cn(
97
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ )
103
+ SheetFooter.displayName = 'SheetFooter'
104
+
105
+ const SheetTitle = React.forwardRef<
106
+ React.ElementRef<typeof SheetPrimitive.Title>,
107
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
108
+ >(({ className, ...props }, ref) => (
109
+ <SheetPrimitive.Title
110
+ ref={ref}
111
+ className={cn('text-lg font-semibold text-foreground', className)}
112
+ {...props}
113
+ />
114
+ ))
115
+ SheetTitle.displayName = SheetPrimitive.Title.displayName
116
+
117
+ const SheetDescription = React.forwardRef<
118
+ React.ElementRef<typeof SheetPrimitive.Description>,
119
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
120
+ >(({ className, ...props }, ref) => (
121
+ <SheetPrimitive.Description
122
+ ref={ref}
123
+ className={cn('text-sm text-muted-foreground', className)}
124
+ {...props}
125
+ />
126
+ ))
127
+ SheetDescription.displayName = SheetPrimitive.Description.displayName
128
+
129
+ export {
130
+ Sheet,
131
+ SheetPortal,
132
+ SheetOverlay,
133
+ SheetTrigger,
134
+ SheetClose,
135
+ SheetContent,
136
+ SheetHeader,
137
+ SheetFooter,
138
+ SheetTitle,
139
+ SheetDescription
140
+ }
components/ui/switch.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as SwitchPrimitives from '@radix-ui/react-switch'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const Switch = React.forwardRef<
9
+ React.ElementRef<typeof SwitchPrimitives.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <SwitchPrimitives.Root
13
+ className={cn(
14
+ 'peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
15
+ className
16
+ )}
17
+ {...props}
18
+ ref={ref}
19
+ >
20
+ <SwitchPrimitives.Thumb
21
+ className={cn(
22
+ 'pointer-events-none block size-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
23
+ )}
24
+ />
25
+ </SwitchPrimitives.Root>
26
+ ))
27
+ Switch.displayName = SwitchPrimitives.Root.displayName
28
+
29
+ export { Switch }
components/ui/textarea.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ export interface TextareaProps
6
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
7
+
8
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
9
+ ({ className, ...props }, ref) => {
10
+ return (
11
+ <textarea
12
+ className={cn(
13
+ 'flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
14
+ className
15
+ )}
16
+ ref={ref}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+ )
22
+ Textarea.displayName = 'Textarea'
23
+
24
+ export { Textarea }
components/ui/tooltip.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5
+
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Content
19
+ ref={ref}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
29
+
30
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
components/user-menu.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import Image from 'next/image'
4
+ import { type Session } from 'next-auth'
5
+ import { signOut } from 'next-auth/react'
6
+
7
+ import { Button } from '@/components/ui/button'
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger
14
+ } from '@/components/ui/dropdown-menu'
15
+ import { IconExternalLink } from '@/components/ui/icons'
16
+
17
+ export interface UserMenuProps {
18
+ user: Session['user']
19
+ }
20
+
21
+ function getUserInitials(name: string) {
22
+ const [firstName, lastName] = name.split(' ')
23
+ return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
24
+ }
25
+
26
+ export function UserMenu({ user }: UserMenuProps) {
27
+ return (
28
+ <div className="flex items-center justify-between">
29
+ <DropdownMenu>
30
+ <DropdownMenuTrigger asChild>
31
+ <Button variant="ghost">
32
+ {user?.image ? (
33
+ <Image
34
+ className="size-6 transition-opacity duration-300 rounded-full select-none ring-1 ring-zinc-100/10 hover:opacity-80"
35
+ src={user?.image ? `${user.image}&s=60` : ''}
36
+ alt={user.name ?? 'Avatar'}
37
+ height={48}
38
+ width={48}
39
+ />
40
+ ) : (
41
+ <div className="flex items-center justify-center text-xs font-medium uppercase rounded-full select-none size-7 shrink-0 bg-muted/50 text-muted-foreground">
42
+ {user?.name ? getUserInitials(user?.name) : null}
43
+ </div>
44
+ )}
45
+ <span className="ml-2">{user?.name}</span>
46
+ </Button>
47
+ </DropdownMenuTrigger>
48
+ <DropdownMenuContent sideOffset={8} align="start" className="w-[180px]">
49
+ <DropdownMenuItem className="flex-col items-start">
50
+ <div className="text-xs font-medium">{user?.name}</div>
51
+ <div className="text-xs text-zinc-500">{user?.email}</div>
52
+ </DropdownMenuItem>
53
+ <DropdownMenuSeparator />
54
+ <DropdownMenuItem
55
+ onClick={() =>
56
+ signOut({
57
+ callbackUrl: '/'
58
+ })
59
+ }
60
+ className="text-xs"
61
+ >
62
+ Log Out
63
+ </DropdownMenuItem>
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ </div>
67
+ )
68
+ }