Spaces:
Sleeping
Sleeping
Commit
·
3ba9c0c
unverified
·
0
Parent(s):
init
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- app/actions.ts +129 -0
- app/api/chat/route.ts +57 -0
- app/layout.tsx +62 -0
- app/opengraph-image.png +0 -0
- app/twitter-image.png +0 -0
- auth.ts +39 -0
- components/button-scroll-to-bottom.tsx +34 -0
- components/chat-history.tsx +46 -0
- components/chat-list.tsx +27 -0
- components/chat-message-actions.tsx +40 -0
- components/chat-message.tsx +80 -0
- components/chat-panel.tsx +103 -0
- components/chat-scroll-anchor.tsx +29 -0
- components/chat-share-dialog.tsx +106 -0
- components/chat.tsx +76 -0
- components/clear-history.tsx +77 -0
- components/empty-screen.tsx +30 -0
- components/external-link.tsx +29 -0
- components/header.tsx +73 -0
- components/login-button.tsx +42 -0
- components/markdown.tsx +9 -0
- components/prompt-form.tsx +97 -0
- components/providers.tsx +17 -0
- components/sidebar-actions.tsx +125 -0
- components/sidebar-desktop.tsx +21 -0
- components/sidebar-footer.tsx +16 -0
- components/sidebar-item.tsx +124 -0
- components/sidebar-items.tsx +42 -0
- components/sidebar-list.tsx +38 -0
- components/sidebar-mobile.tsx +28 -0
- components/sidebar-toggle.tsx +24 -0
- components/sidebar.tsx +21 -0
- components/tailwind-indicator.tsx +14 -0
- components/theme-toggle.tsx +31 -0
- components/ui/alert-dialog.tsx +141 -0
- components/ui/badge.tsx +36 -0
- components/ui/button.tsx +57 -0
- components/ui/codeblock.tsx +148 -0
- components/ui/dialog.tsx +122 -0
- components/ui/dropdown-menu.tsx +128 -0
- components/ui/icons.tsx +507 -0
- components/ui/input.tsx +25 -0
- components/ui/label.tsx +26 -0
- components/ui/select.tsx +123 -0
- components/ui/separator.tsx +31 -0
- components/ui/sheet.tsx +140 -0
- components/ui/switch.tsx +29 -0
- components/ui/textarea.tsx +24 -0
- components/ui/tooltip.tsx +30 -0
- 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 |
+
}
|