feat: reorganize files and implement ui and sidebar

This commit is contained in:
shadcn 2023-06-11 00:23:23 +04:00
parent 2e80864b84
commit 01dc4520d6
36 changed files with 1347 additions and 462 deletions

View file

@ -1,7 +1,40 @@
'use server'
import { kv } from '@vercel/kv'
import { revalidatePath } from 'next/cache'
import { kv } from '@vercel/kv'
import { type Chat } from '@/lib/types'
export async function getChats(userId: string) {
try {
const pipeline = kv.pipeline()
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1)
for (const chat of chats) {
pipeline.hgetall(chat)
}
const results = await pipeline.exec()
return results as Chat[]
} catch (error) {
return []
}
}
export async function getChat(id: string, userId: string) {
const chat = await kv.hgetall<Chat>(`chat:${id}`)
if (!chat) {
throw new Error('Not found')
}
if (userId && chat.userId !== userId) {
throw new Error('Unauthorized')
}
return chat
}
export async function removeChat({
id,

View file

@ -1,2 +1,2 @@
export { GET, POST } from '@/auth'
export const runtime = 'edge'
// export const runtime = 'edge'

View file

@ -4,7 +4,7 @@ import { nanoid } from '@/lib/utils'
import { OpenAIStream, StreamingTextResponse } from 'ai-connector'
import { Configuration, OpenAIApi } from 'openai-edge'
export const runtime = 'edge'
// export const runtime = 'edge'
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY

View file

@ -1,44 +0,0 @@
'use client'
import { type Message } from 'ai-connector'
import { ChatMessage } from './chat-message'
import { EmptyScreen } from './empty-screen'
export interface ChatList {
messages: Message[]
}
export function ChatList({ messages }: ChatList) {
return (
<div className="relative max-w-2xl mx-auto">
{/* <div className="sticky top-0 border-b w-full bg-black md:hidden text-white font-semibold">
<div className="px-4 py-2 flex items-center justify-between">
<div className="flex flex-row gap-2 whitespace-nowrap items-center">
<NextChatLogo className="h-6 w-6" />
<span className="select-none">Next.js Chatbot</span>
</div>
<div>
<button className="text-white p-2 rounded hover:bg-zinc-800 transition duration-100">
<Plus className="h-4 w-4" />
</button>
</div>
</div>
</div> */}
{messages.length > 0 ? (
<div className="group">
{messages.map(message => (
<ChatMessage
key={message.id || message.content}
message={message}
/>
))}
</div>
) : (
<div className="pt-10">
<EmptyScreen />
</div>
)}
</div>
)
}

View file

@ -1,57 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
import { Prompt } from './prompt'
import { useChat, type Message } from 'ai-connector'
import { ChatList } from './chat-list'
import { ExternalLink } from '@/app/external-link'
export interface ChatProps {
// create?: (input: string) => Chat | undefined;
initialMessages?: Message[]
id?: string
}
export function Chat({
id,
// create,
initialMessages
}: ChatProps) {
const router = useRouter()
const { isLoading, messages, reload, append } = useChat({
initialMessages: initialMessages as any[],
id
// onCreate: (id: string) => {
// router.push(`/chat/${id}`);
// },
})
return (
<div className="h-full w-full overflow-auto pb-[200px]">
<ChatList messages={messages} />
<div className="fixed bottom-0 left-1 right-3 p-6 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-lg">
<div className="max-w-2xl mx-auto pl-10">
<Prompt
onSubmit={value => {
append({
content: value,
role: 'user'
})
}}
onRefresh={messages.length ? reload : undefined}
isLoading={isLoading}
/>
<p className="text-muted-foreground text-xs leading-normal text-center pt-2">
This is an open source AI chatbot app built with{' '}
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
<ExternalLink href="https://vercel.com/storage/kv">
Vercel KV
</ExternalLink>
.
</p>
</div>
</div>
</div>
)
}

View file

@ -1,15 +1,12 @@
import { Sidebar } from '@/app/sidebar'
import { auth } from '@/auth'
import { type Metadata } from 'next'
import { Chat } from '@/app/chat'
import { type Chat as ChatType } from '@/lib/types'
import { kv } from '@vercel/kv'
import { Message } from 'ai-connector'
import { auth } from '@/auth'
import { getChat } from '@/app/actions'
import { Chat } from '@/components/chat'
export const runtime = 'edge'
// export const runtime = 'edge'
export const preferredRegion = 'home'
export interface ChatPageProps {
params: {
id: string
@ -30,28 +27,5 @@ export default async function ChatPage({ params }: ChatPageProps) {
const session = await auth()
const chat = await getChat(params.id, session?.user?.email ?? '')
return (
<div className="relative flex h-full w-full overflow-hidden">
{/* @ts-ignore */}
<Sidebar session={session} />
<div className="flex h-full min-w-0 flex-1 flex-col">
<Chat id={chat.id} initialMessages={chat.messages as Message[]} />
</div>
</div>
)
}
ChatPage.displayName = 'ChatPage'
async function getChat(id: string, userId: string) {
const chat = await kv.hgetall<ChatType>(`chat:${id}`)
if (!chat) {
throw new Error('Not found')
}
if (userId && chat.userId !== userId) {
throw new Error('Unauthorized')
}
return chat
return <Chat id={chat.id} initialMessages={chat.messages} />
}

View file

@ -1,52 +0,0 @@
import { GitHub, Vercel } from '@/components/icons'
import './globals.css'
import { Sidebar } from '@/app/sidebar'
import { buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { Plus } from 'lucide-react'
import Link from 'next/link'
import { UserMenu } from '@/components/user-menu'
import { auth } from '@/auth'
export async function Header() {
const session = await auth()
return (
<header className="h-16 shrink-0 z-50 bg-gradient-to-b from-background/30 to-background/50 backdrop-blur-lg px-4 flex sticky top-0 w-full items-center justify-between">
<div className="flex items-center">
{/* @ts-ignore */}
<Sidebar />
<UserMenu session={session} />
</div>
<div className="flex space-x-2 items-center">
<Link href="/" className={cn(buttonVariants({ size: 'sm' }))}>
<Plus className="h-4 w-4 mr-2" />
New Chat
</Link>
<a
href="https://github.com/vercel/nextjs-ai-chatbot/"
target="_blank"
className={cn(
buttonVariants({ size: 'sm', variant: 'outline' }),
'space-x-2'
)}
>
<Vercel className="mr-2" />
Deploy
</a>
<a
target="_blank"
href="https://github.com/vercel/nextjs-ai-chatbot/"
className={cn(
buttonVariants({ size: 'sm', variant: 'ghost' }),
'h-9 w-9 p-0'
)}
>
<GitHub className="w-5 h-5" />
<span className="sr-only">View on GitHub</span>
</a>
</div>
</header>
)
}

View file

@ -1,11 +1,11 @@
import { Metadata } from 'next'
import './globals.css'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { ThemeProvider } from '@/components/theme-provider'
import '@/app/globals.css'
import { fontMono, fontSans } from '@/lib/fonts'
import { cn } from '@/lib/utils'
import { Header } from '@/app/header'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { ThemeProvider } from '@/components/theme-provider'
import { Header } from '@/components/header'
export const metadata: Metadata = {
title: {
@ -43,7 +43,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
<div className="h-screen flex flex-col">
{/* @ts-ignore */}
<Header />
<main className="flex-1 overflow-hidden">{children}</main>
<main className="flex-1">{children}</main>
</div>
<TailwindIndicator />
</ThemeProvider>

View file

@ -1,13 +1,8 @@
import { type Message } from 'ai-connector'
import { Chat } from './chat'
import { Chat } from '@/components/chat'
export const runtime = 'edge'
// export const runtime = 'edge'
export const preferredRegion = 'home'
export default async function IndexPage() {
return (
<div className="h-full overflow-hidden">
<Chat />
</div>
)
return <Chat />
}

View file

@ -1,68 +0,0 @@
'use client'
import { cn } from '@/lib/utils'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { MessageCircleIcon, Trash2Icon, Loader2Icon } from 'lucide-react'
import { removeChat } from './actions'
import { useTransition } from 'react'
export function SidebarItem({
title,
href,
id,
userId
}: {
title: string
href: string
id: string
userId: string
}) {
const pathname = usePathname()
const router = useRouter()
const active = pathname === href
const [isPending, startTransition] = useTransition()
if (!id) return null
return (
<Link
href={href}
className={cn(
'group flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium transition-colors duration-100 hover:bg-zinc-500/10 hover:dark:bg-zinc-300/20',
isPending
? 'text-zinc-400 dark:text-zinc-500'
: 'text-zinc-800 dark:text-zinc-400',
active
? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
: 'bg-transparent'
)}
>
<MessageCircleIcon className="h-4 w-4" />
<div
className="relative max-h-5 flex-1 overflow-hidden text-ellipsis break-all select-none"
title={title}
>
<span className="whitespace-nowrap">{title}</span>
</div>
<button
className="opacity-0 -mr-6 transition-opacity duration-0 hover:bg-zinc-400/50 group-hover:mr-0 group-hover:opacity-80 p-1 -my-1 rounded group-hover:duration-600"
disabled={isPending}
onClick={e => {
e.preventDefault()
startTransition(() => {
removeChat({ id, userId, path: href }).then(() => {
router.push('/')
})
})
}}
>
{isPending ? (
<Loader2Icon className="animate-spin h-4 w-4" />
) : (
<Trash2Icon className="h-4 w-4" />
)}
</button>
</Link>
)
}

View file

@ -1,69 +0,0 @@
import { Chat } from '@/lib/types'
import { type Session } from '@auth/nextjs/types'
import { kv } from '@vercel/kv'
import { Sidebar as SidebarIcon } from 'lucide-react'
import { SidebarItem } from './sidebar-item'
import { auth } from '@/auth'
import { NextChat } from '@/components/icons'
import { Button } from '@/components/ui/button'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
export interface SidebarProps {}
export async function Sidebar({}: SidebarProps) {
const session = await auth()
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" className="w-9 h-9 p-0">
<SidebarIcon className="w-6 h-6" />
<span className="sr-only">Toggle Sidebar</span>
</Button>
</SheetTrigger>
<SheetContent
position="left"
className="w-[300px] top-2 rounded-lg left-2 bottom-2 h-auto"
>
<div className="flex items-center">
<NextChat className="mr-2 w-6 h-6 text-primary" inverted />
<span className="select-none font-bold">Chatbot</span>
</div>
{/* @ts-ignore */}
<SidebarList session={session} />
</SheetContent>
</Sheet>
)
}
async function SidebarList({ session }: { session?: Session }) {
const results: Chat[] = await getChats(session?.user?.email ?? '')
return results.map(c => (
<SidebarItem
key={c.id}
title={c.title}
userId={session?.user?.email ?? ''}
href={`/chat/${c.id}`}
id={c.id}
/>
))
}
async function getChats(userId: string) {
try {
const pipeline = kv.pipeline()
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1)
for (const chat of chats) {
pipeline.hgetall(chat)
}
const results = await pipeline.exec()
return results as Chat[]
} catch (error) {
return []
}
}

28
auth.ts
View file

@ -1,27 +1,27 @@
import NextAuth from "@auth/nextjs";
import GitHub from "@auth/nextjs/providers/github";
import { NextResponse } from "next/server";
import NextAuth from '@auth/nextjs'
import GitHub from '@auth/nextjs/providers/github'
import { NextResponse } from 'next/server'
export const {
handlers: { GET, POST },
auth,
CSRF_experimental,
CSRF_experimental
} = NextAuth({
// @ts-ignore
providers: [GitHub],
session: { strategy: "jwt" },
session: { strategy: 'jwt' },
async authorized({ request, auth }: any) {
const url = request.nextUrl;
const url = request.nextUrl
if (request.method === "POST") {
const { authToken } = (await request.json()) ?? {};
if (request.method === 'POST') {
const { authToken } = (await request.json()) ?? {}
// If the request has a valid auth token, it is authorized
const valid = true;
if (valid) return true;
return NextResponse.json("Invalid auth token", { status: 401 });
const valid = true
if (valid) return true
return NextResponse.json('Invalid auth token', { status: 401 })
}
// Logged in users are authenticated, otherwise redirect to login page
return !!auth;
},
});
return !!auth
}
})

26
components/chat-list.tsx Normal file
View file

@ -0,0 +1,26 @@
'use client'
import { type Message } from 'ai-connector'
import { ChatMessage } from '@/components/chat-message'
import { EmptyScreen } from '@/components/empty-screen'
export interface ChatList {
messages: Message[]
}
export function ChatList({ messages }: ChatList) {
return (
<div className="relative max-w-2xl mx-auto">
{messages.length > 0 ? (
messages.map(message => (
<ChatMessage key={message.id || message.content} message={message} />
))
) : (
<div className="pt-10">
<EmptyScreen />
</div>
)}
</div>
)
}

View file

@ -1,11 +1,12 @@
import CodeBlock from '@/components/ui/codeblock'
import { MemoizedReactMarkdown } from '@/components/markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import { cn } from '@/lib/utils'
import { fontMessage } from '@/lib/fonts'
import { Message } from 'ai-connector'
import { User } from 'lucide-react'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import { cn } from '@/lib/utils'
import { fontMessage } from '@/lib/fonts'
import CodeBlock from '@/components/ui/codeblock'
import { MemoizedReactMarkdown } from '@/components/markdown'
import { OpenAI } from '@/components/icons'
export interface ChatMessageProps {
@ -16,7 +17,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
return (
<div
className={cn(
'flex items-start space-x-4 mb-4',
'flex items-start space-x-4 mb-4 relative -ml-12',
fontMessage.className,
message.role === 'user' && 'mt-12 first:mt-0'
)}
@ -24,7 +25,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
>
<div
className={cn(
'flex h-8 w-8 -ml-2 shrink-0 items-center justify-center rounded-full select-none border',
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full select-none border',
message.role === 'assistant' && 'bg-primary text-primary-foreground'
)}
>

45
components/chat-panel.tsx Normal file
View file

@ -0,0 +1,45 @@
'use client'
import { ExternalLink } from '@/components/external-link'
import { UseChatHelpers } from 'ai-connector'
import { PromptForm } from './prompt-form'
export interface ChatPanelProps
extends Pick<UseChatHelpers, 'append' | 'isLoading' | 'reload' | 'messages'> {
id?: string
}
export function ChatPanel({
id,
append,
isLoading,
reload,
messages
}: ChatPanelProps) {
return (
<div className="fixed bottom-0 left-0 right-0">
<div className="max-w-2xl mx-auto">
<div className="px-4 py-2 border space-y-4 shadow-lg bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-lg rounded-t-xl">
<PromptForm
onSubmit={value => {
append({
content: value,
role: 'user'
})
}}
onRefresh={messages.length ? reload : undefined}
isLoading={isLoading}
/>
<p className="text-muted-foreground text-xs leading-normal text-center pb-1">
This is an open source AI chatbot app built with{' '}
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
<ExternalLink href="https://vercel.com/storage/kv">
Vercel KV
</ExternalLink>
.
</p>
</div>
</div>
</div>
)
}

33
components/chat.tsx Normal file
View file

@ -0,0 +1,33 @@
'use client'
import { useChat, type Message } from 'ai-connector'
import { ChatList } from '@/components/chat-list'
import { ChatPanel } from '@/components/chat-panel'
export interface ChatProps {
// create?: (input: string) => Chat | undefined;
initialMessages?: Message[]
id?: string
}
export function Chat({ id, initialMessages }: ChatProps) {
const { messages, append, reload, isLoading } = useChat({
initialMessages,
id
})
return (
<div className="h-full overflow-hidden">
<div className="h-full w-full overflow-auto pb-[200px]">
<ChatList messages={messages} />
<ChatPanel
id={id}
append={append}
isLoading={isLoading}
reload={reload}
messages={messages}
/>
</div>
</div>
)
}

View file

@ -1,7 +1,8 @@
import { cn } from '@/lib/utils'
import { ExternalLink } from './external-link'
import { ArrowRight } from 'lucide-react'
import { useChatStore } from '@/hooks/use-chat-store'
import { cn } from '@/lib/utils'
import { useChatStore } from '@/lib/hooks/use-chat-store'
import { ExternalLink } from '@/components/external-link'
import { Button } from '@/components/ui/button'
import { OpenAI } from '@/components/icons'
@ -24,10 +25,10 @@ export function EmptyScreen({ className }: React.ComponentProps<'div'>) {
const { setDefaultMessage } = useChatStore()
return (
<div className="flex items-start space-x-4 mb-4">
<div className="flex items-start space-x-4 mb-4 -ml-12">
<div
className={cn(
'flex h-8 w-8 -ml-2 shrink-0 items-center justify-center rounded-full select-none border bg-primary text-primary-foreground'
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full select-none border bg-primary text-primary-foreground'
)}
>
<OpenAI className="w-4 h-4" />

42
components/header.tsx Normal file
View file

@ -0,0 +1,42 @@
import { cn } from '@/lib/utils'
import { auth } from '@/auth'
import { getChats } from '@/app/actions'
import { buttonVariants } from '@/components/ui/button'
import { UserMenu } from '@/components/user-menu'
import { GitHub, Separator, Vercel } from '@/components/icons'
import { Sidebar } from '@/components/sidebar'
export async function Header() {
const session = await auth()
const chats = session?.user?.email ? await getChats(session.user.email) : []
return (
<header className="h-16 shrink-0 z-50 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-lg px-4 flex sticky top-0 w-full items-center justify-between">
<div className="flex items-center w-2/5">
{/* @ts-ignore */}
<Sidebar chats={chats} session={session} />
<Separator className="h-6 w-6 text-muted-foreground/50" />
<UserMenu session={session} />
</div>
<div className="flex space-x-2 items-center justify-end w-2/5">
<a
target="_blank"
href="https://github.com/vercel/nextjs-ai-chatbot/"
rel="noopener noreferrer"
className={cn(buttonVariants({ variant: 'outline' }))}
>
<GitHub className="w-4 h-4 mr-2" />
<span>GitHub</span>
</a>
<a
href="https://github.com/vercel/nextjs-ai-chatbot/"
target="_blank"
className={cn(buttonVariants())}
>
<Vercel className="w-4 h-4 mr-2" />
Deploy to Vercel
</a>
</div>
</header>
)
}

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

View file

@ -1,6 +1,7 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
export function NextChat({
@ -135,3 +136,25 @@ export function GitHub({ className, ...props }: React.ComponentProps<'svg'>) {
</svg>
)
}
export function Separator({
className,
...props
}: React.ComponentProps<'svg'>) {
return (
<svg
fill="none"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1"
viewBox="0 0 24 24"
aria-hidden="true"
className={cn('w-4 h-4', className)}
{...props}
>
<path d="M16.88 3.549L7.12 20.451"></path>
</svg>
)
}

View file

@ -1,21 +1,22 @@
'use client'
import * as React from 'react'
import { CornerDownLeft, RefreshCcw, StopCircle } from 'lucide-react'
import { CornerDownLeft, Plus, RefreshCcw, StopCircle } from 'lucide-react'
import { useState } from 'react'
import Textarea from 'react-textarea-autosize'
import { Button } from '@/components/ui/button'
import { Button, buttonVariants } from '@/components/ui/button'
import { fontMessage } from '@/lib/fonts'
import { useCmdEnterSubmit } from '@/lib/hooks/use-command-enter-submit'
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
import { cn } from '@/lib/utils'
import { useChatStore } from '@/hooks/use-chat-store'
import { useChatStore } from '@/lib/hooks/use-chat-store'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip'
import Link from 'next/link'
export interface PromptProps {
onSubmit: (value: string) => void
@ -24,7 +25,7 @@ export interface PromptProps {
isLoading: boolean
}
export function Prompt({
export function PromptForm({
onSubmit,
onRefresh,
onAbort,
@ -32,7 +33,7 @@ export function Prompt({
}: PromptProps) {
const { defaultMessage } = useChatStore()
const [input, setInput] = useState(defaultMessage)
const { formRef, onKeyDown } = useCmdEnterSubmit()
const { formRef, onKeyDown } = useEnterSubmit()
const inputRef = React.useRef<HTMLTextAreaElement>(null)
React.useEffect(() => {
@ -69,29 +70,25 @@ export function Prompt({
</div>
</Button>
) : null}
{/* <button
id="share-button"
className="btn btn-neutral flex justify-center gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-3 w-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
/>
</svg>
Share
</button> */}
</div>
</div>
<div className="relative flex w-full grow flex-col rounded-md border bg-background overflow-hidden pr-12">
<TooltipProvider>
<div className="relative flex w-full grow flex-col rounded-md border bg-background overflow-hidden px-12">
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/"
className={cn(
buttonVariants({ size: 'sm', variant: 'outline' }),
'w-8 h-8 p-0 rounded-full absolute top-4 left-4 bg-background'
)}
>
<Plus className="h-4 w-4" />
<span className="sr-only">New Chat</span>
</Link>
</TooltipTrigger>
<TooltipContent>New Chat</TooltipContent>
</Tooltip>
<Textarea
ref={inputRef}
tabIndex={0}
@ -102,12 +99,11 @@ export function Prompt({
placeholder="Send a message."
spellCheck={false}
className={cn(
'min-h-[60px] text-sm p-4 bg-transparent focus-within:outline-none w-full resize-none',
'min-h-[60px] text-sm bg-transparent px-4 py-[1.4rem] focus-within:outline-none w-full resize-none',
fontMessage.className
)}
/>
<div className="absolute top-4 right-4">
<TooltipProvider>
{isLoading ? (
<Tooltip>
<TooltipTrigger asChild>
@ -125,21 +121,17 @@ export function Prompt({
) : (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
type="submit"
className="h-8 w-8 p-0"
>
<Button type="submit" className="h-8 w-8 p-0">
<CornerDownLeft className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
)}
</div>
</div>
</TooltipProvider>
</div>
</div>
</div>
</form>
)
}

104
components/sidebar-item.tsx Normal file
View file

@ -0,0 +1,104 @@
'use client'
import { Loader2Icon, MessageSquare, Trash2Icon } from 'lucide-react'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import * as React from 'react'
import { useTransition } from 'react'
import { cn } from '@/lib/utils'
import { removeChat } from '@/app/actions'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { Button, buttonVariants } from '@/components/ui/button'
export function SidebarItem({
title,
href,
id,
userId
}: {
title: string
href: string
id: string
userId: string
}) {
const [open, setIsOpen] = React.useState(false)
const pathname = usePathname()
const router = useRouter()
const isActive = pathname === href
const [isPending, startTransition] = useTransition()
if (!id) return null
return (
<>
<Link
href={href}
className={cn(
buttonVariants({ variant: 'ghost' }),
'group w-full px-2',
isActive && 'bg-accent'
)}
>
<MessageSquare className="h-4 w-4 mr-2" />
<div
className="relative max-h-5 flex-1 overflow-hidden text-ellipsis break-all select-none"
title={title}
>
<span className="whitespace-nowrap">{title}</span>
</div>
{isActive && (
<Button
variant="ghost"
size="icon"
className="w-7 h-7"
disabled={isPending}
onClick={() => setIsOpen(true)}
>
<Trash2Icon className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
)}
</Link>
<AlertDialog open={open} onOpenChange={setIsOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete your chat message and remove your
data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isPending}
onClick={event => {
event.preventDefault()
startTransition(async () => {
await removeChat({ id, userId, path: href })
setIsOpen(false)
router.push('/')
})
}}
>
{isPending && (
<Loader2Icon className="animate-spin h-4 w-4 mr-2" />
)}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}

77
components/sidebar.tsx Normal file
View file

@ -0,0 +1,77 @@
'use client'
import * as React from 'react'
import { Sidebar as SidebarIcon } from 'lucide-react'
import { type Session } from '@auth/nextjs/types'
import { signOut } from '@auth/nextjs/client'
import { type Chat } from '@/lib/types'
import { Button } from '@/components/ui/button'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger
} from '@/components/ui/sheet'
import { SidebarItem } from '@/components/sidebar-item'
import { ThemeToggle } from '@/components/theme-toggle'
export interface SidebarProps {
session?: Session
chats: Chat[]
}
export async function Sidebar({ session, chats }: SidebarProps) {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" className="w-9 h-9 p-0">
<SidebarIcon className="w-6 h-6" />
<span className="sr-only">Toggle Sidebar</span>
</Button>
</SheetTrigger>
<SheetContent
position="left"
className="w-[300px] gap-0 top-2 px-4 rounded-r-lg bottom-2 h-auto flex flex-col"
>
<SheetHeader>
<SheetTitle>Chat History</SheetTitle>
</SheetHeader>
<div className="flex-1">
{chats?.length ? (
<div className="space-y-2 -mx-2 py-4">
{chats.map(chat => (
<SidebarItem
key={chat.id}
title={chat.title}
userId={session?.user?.email ?? ''}
href={`/chat/${chat.id}`}
id={chat.id}
/>
))}
</div>
) : (
<div className="text-center p-8">
<p className="text-sm text-muted-foreground">
{session?.user ? <>No chat history</> : <>Login for history</>}
</p>
</div>
)}
</div>
<div className="flex items-center">
<ThemeToggle />
{session?.user && (
<Button
variant="ghost"
onClick={() => signOut()}
className="ml-auto"
>
Logout
</Button>
)}
</div>
</SheetContent>
</Sheet>
)
}

View file

@ -1,33 +1,31 @@
'use client'
import * as React from 'react'
import { useTheme } from 'next-themes'
import { Moon, Sun } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Moon, Sun } from 'lucide-react'
import { useTransition } from 'react'
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
const [_, startTransition] = useTransition()
const [_, startTransition] = React.useTransition()
return (
<Button
variant="ghost"
size="sm"
className="p-0 leading-4 h-4 font-normal w-full"
size="icon"
onClick={() => {
startTransition(() => {
setTheme(theme === 'light' ? 'dark' : 'light')
})
}}
>
<span className="flex flex-row justify-between text-xs text-zinc-900 dark:text-zinc-400 w-full">
<span className="">Toggle theme</span>
{!theme ? null : theme === 'dark' ? (
<Moon className="h-4 transition-all" />
<Moon className="h-4 w-4 transition-all" />
) : (
<Sun className="h-4 transition-all" />
<Sun className="h-4 w-4 transition-all" />
)}
</span>
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View file

@ -0,0 +1,150 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
</div>
</AlertDialogPrimitive.Portal>
)
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity animate-in fade-in",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View file

@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
'inline-flex items-center justify-center shadow-sm rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
{
variants: {
variant: {
@ -16,13 +16,14 @@ const buttonVariants = cva(
'border border-input hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary'
ghost: 'hover:bg-accent shadow-none hover:text-accent-foreground',
link: 'underline-offset-4 shadow-none hover:underline text-primary'
},
size: {
default: 'h-10 py-2 px-4',
default: 'h-9 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md'
lg: 'h-11 px-8 rounded-md',
icon: 'h-9 w-9 p-0'
}
},
defaultVariants: {

128
components/ui/dialog.tsx Normal file
View file

@ -0,0 +1,128 @@
'use client'
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-sm animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0',
className
)}
{...props}
>
{children}
<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">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-1.5 text-center sm:text-left',
className
)}
{...props}
/>
)
DialogHeader.displayName = 'DialogHeader'
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
)
DialogFooter.displayName = 'DialogFooter'
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-lg font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription
}

View file

@ -0,0 +1,200 @@
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'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',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-sm 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',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'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',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup
}

25
components/ui/input.tsx Normal file
View file

@ -0,0 +1,25 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full shadow-sm rounded-md border border-input bg-transparent px-3 py-2 text-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',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }

View file

@ -66,7 +66,6 @@ const SheetContent = React.forwardRef<
DialogContentProps
>(({ position, className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ position }), className)}

View file

@ -1,29 +1,36 @@
'use client'
import { Button } from '@/components/ui/button'
import * as React from 'react'
import { signIn } from '@auth/nextjs/client'
import { type Session } from '@auth/nextjs/types'
import { Loader2Icon } from 'lucide-react'
import { Button } from '@/components/ui/button'
export interface UserMenuProps {
session: Session
session?: Session
}
export function UserMenu({ session }: UserMenuProps) {
if (!session) {
const [isLoading, setIsLoading] = React.useState(false)
if (!session?.user) {
return (
<Button variant="ghost" size="sm" onClick={() => signIn('github')}>
<Button
variant="ghost"
size="sm"
onClick={() => {
setIsLoading(true)
signIn('github')
}}
disabled={isLoading}
>
{isLoading && <Loader2Icon className="animate-spin w-4 h-4 mr-2" />}
Login
</Button>
)
}
return (
<div className="flex items-center justify-between text-sm">
{session?.user && (
<Button variant="link" size="sm">
{session.user?.name}
</Button>
)}
</div>
)
return <p className="text-sm font-medium">Logged in as {session.user.name}</p>
}

View file

@ -1,7 +1,7 @@
import type { RefObject } from 'react'
import { useRef } from 'react'
export function useCmdEnterSubmit(): {
export function useEnterSubmit(): {
formRef: RefObject<HTMLFormElement>
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
} {
@ -10,8 +10,9 @@ export function useCmdEnterSubmit(): {
const handleKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement>
): void => {
if (event.key === 'Enter') {
if (event.key === 'Enter' && !event.shiftKey) {
formRef.current?.requestSubmit()
event.preventDefault()
}
}

View file

@ -16,6 +16,7 @@
"dependencies": {
"@auth/core": "0.0.0-manual.527fff6c",
"@auth/nextjs": "0.0.0-manual.030e8328",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-slot": "^1.0.2",

345
pnpm-lock.yaml generated
View file

@ -14,6 +14,9 @@ dependencies:
'@auth/nextjs':
specifier: 0.0.0-manual.030e8328
version: 0.0.0-manual.030e8328(next@13.4.5-canary.3)(react@18.2.0)
'@radix-ui/react-alert-dialog':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
@ -74,6 +77,9 @@ dependencies:
react-markdown:
specifier: ^8.0.7
version: 8.0.7(@types/react@18.2.6)(react@18.2.0)
react-scroll-to-bottom:
specifier: ^4.2.0
version: 4.2.0(react@18.2.0)
react-syntax-highlighter:
specifier: ^15.5.0
version: 15.5.0(react@18.2.0)
@ -103,6 +109,9 @@ devDependencies:
'@types/react-dom':
specifier: ^18.0.7
version: 18.2.4
'@types/react-scroll-to-bottom':
specifier: ^4.2.0
version: 4.2.0
'@types/react-syntax-highlighter':
specifier: ^15.5.6
version: 15.5.6
@ -179,12 +188,137 @@ packages:
- nodemailer
dev: false
/@babel/code-frame@7.22.5:
resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.22.5
dev: false
/@babel/helper-module-imports@7.22.5:
resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.22.5
dev: false
/@babel/helper-string-parser@7.22.5:
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
engines: {node: '>=6.9.0'}
dev: false
/@babel/helper-validator-identifier@7.22.5:
resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
engines: {node: '>=6.9.0'}
dev: false
/@babel/highlight@7.22.5:
resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.22.5
chalk: 2.4.2
js-tokens: 4.0.0
dev: false
/@babel/runtime-corejs3@7.22.5:
resolution: {integrity: sha512-TNPDN6aBFaUox2Lu+H/Y1dKKQgr4ucz/FGyCz67RVYLsBpVpUFf1dDngzg+Od8aqbrqwyztkaZjtWCZEUOT8zA==}
engines: {node: '>=6.9.0'}
dependencies:
core-js-pure: 3.30.2
regenerator-runtime: 0.13.11
dev: false
/@babel/runtime@7.21.5:
resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.11
/@babel/types@7.22.5:
resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.22.5
'@babel/helper-validator-identifier': 7.22.5
to-fast-properties: 2.0.0
dev: false
/@emotion/babel-plugin@11.11.0:
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
dependencies:
'@babel/helper-module-imports': 7.22.5
'@babel/runtime': 7.21.5
'@emotion/hash': 0.9.1
'@emotion/memoize': 0.8.1
'@emotion/serialize': 1.1.2
babel-plugin-macros: 3.1.0
convert-source-map: 1.9.0
escape-string-regexp: 4.0.0
find-root: 1.1.0
source-map: 0.5.7
stylis: 4.2.0
dev: false
/@emotion/cache@11.11.0:
resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==}
dependencies:
'@emotion/memoize': 0.8.1
'@emotion/sheet': 1.2.2
'@emotion/utils': 1.2.1
'@emotion/weak-memoize': 0.3.1
stylis: 4.2.0
dev: false
/@emotion/css@11.1.3:
resolution: {integrity: sha512-RSQP59qtCNTf5NWD6xM08xsQdCZmVYnX/panPYvB6LQAPKQB6GL49Njf0EMbS3CyDtrlWsBcmqBtysFvfWT3rA==}
peerDependencies:
'@babel/core': ^7.0.0
peerDependenciesMeta:
'@babel/core':
optional: true
dependencies:
'@emotion/babel-plugin': 11.11.0
'@emotion/cache': 11.11.0
'@emotion/serialize': 1.1.2
'@emotion/sheet': 1.2.2
'@emotion/utils': 1.2.1
dev: false
/@emotion/hash@0.9.1:
resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==}
dev: false
/@emotion/memoize@0.8.1:
resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==}
dev: false
/@emotion/serialize@1.1.2:
resolution: {integrity: sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==}
dependencies:
'@emotion/hash': 0.9.1
'@emotion/memoize': 0.8.1
'@emotion/unitless': 0.8.1
'@emotion/utils': 1.2.1
csstype: 3.1.2
dev: false
/@emotion/sheet@1.2.2:
resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==}
dev: false
/@emotion/unitless@0.8.1:
resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
dev: false
/@emotion/utils@1.2.1:
resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==}
dev: false
/@emotion/weak-memoize@0.3.1:
resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==}
dev: false
/@esbuild/android-arm@0.15.18:
resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==}
engines: {node: '>=12'}
@ -479,6 +613,32 @@ packages:
'@babel/runtime': 7.21.5
dev: false
/@radix-ui/react-alert-dialog@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-jbfBCRlKYlhbitueOAv7z74PXYeIQmWpKwm3jllsdkw7fGWNkxqP3v0nY9WmOzcPqpQuoorNtvViBgL46n5gVg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.21.5
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-arrow@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==}
peerDependencies:
@ -1259,6 +1419,10 @@ packages:
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
dev: true
/@types/parse-json@4.0.0:
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
dev: false
/@types/prop-types@15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
@ -1267,6 +1431,12 @@ packages:
dependencies:
'@types/react': 18.2.6
/@types/react-scroll-to-bottom@4.2.0:
resolution: {integrity: sha512-CE8GnRIFl4CRYlnHikVwJcHj4WOFBbgclbs1LNlqooVGYSBfrRvig3C3aVKDUkiXQYpp0i2p8OPUMO1JvlrZ5Q==}
dependencies:
'@types/react': 18.2.6
dev: true
/@types/react-syntax-highlighter@15.5.6:
resolution: {integrity: sha512-i7wFuLbIAFlabTeD2I1cLjEOrG/xdMa/rpx2zwzAoGHuXJDhSqp9BSfDlMHSh9JSuNfxHk9eEmMX6D55GiyjGg==}
dependencies:
@ -1410,6 +1580,13 @@ packages:
engines: {node: '>=8'}
dev: true
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
dependencies:
color-convert: 1.9.3
dev: false
/ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@ -1539,6 +1716,15 @@ packages:
deep-equal: 2.2.1
dev: true
/babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
dependencies:
'@babel/runtime': 7.21.5
cosmiconfig: 7.1.0
resolve: 1.22.2
dev: false
/bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
dev: false
@ -1623,7 +1809,6 @@ packages:
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
dev: true
/camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
@ -1642,6 +1827,15 @@ packages:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
dev: false
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
dependencies:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
dev: false
/chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@ -1697,6 +1891,10 @@ packages:
typescript: 5.1.3
dev: false
/classnames@2.3.1:
resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==}
dev: false
/cli-color@2.0.3:
resolution: {integrity: sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==}
engines: {node: '>=0.10'}
@ -1717,6 +1915,12 @@ packages:
engines: {node: '>=6'}
dev: false
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
dev: false
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -1724,6 +1928,10 @@ packages:
color-name: 1.1.4
dev: true
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
dev: false
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
@ -1755,11 +1963,37 @@ packages:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
dev: false
/cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
/core-js-pure@3.30.2:
resolution: {integrity: sha512-p/npFUJXXBkCCTIlEGBdghofn00jWG6ZOtdoIXSJmAu2QBvN0IqpZXWweOytcwE6cfx8ZvVUy1vw8zxhe4Y2vg==}
requiresBuild: true
dev: false
/core-js@3.18.3:
resolution: {integrity: sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==}
deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
requiresBuild: true
dev: false
/cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
dependencies:
'@types/parse-json': 4.0.0
import-fresh: 3.3.0
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.2
dev: false
/cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -1970,6 +2204,12 @@ packages:
tapable: 2.2.1
dev: true
/error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
is-arrayish: 0.2.1
dev: false
/es-abstract@1.21.2:
resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==}
engines: {node: '>= 0.4'}
@ -2308,10 +2548,14 @@ packages:
engines: {node: '>=6'}
dev: true
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
dev: false
/escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
dev: true
/escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
@ -2703,6 +2947,10 @@ packages:
to-regex-range: 5.0.1
dev: true
/find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
dev: false
/find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@ -2772,7 +3020,6 @@ packages:
/function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/function.prototype.name@1.1.5:
resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
@ -2946,6 +3193,11 @@ packages:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
dev: false
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@ -2979,7 +3231,6 @@ packages:
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
dev: true
/hast-util-parse-selector@2.2.5:
resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==}
@ -3028,7 +3279,6 @@ packages:
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
dev: true
/imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
@ -3092,6 +3342,10 @@ packages:
is-typed-array: 1.1.10
dev: true
/is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: false
/is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
@ -3127,7 +3381,6 @@ packages:
resolution: {integrity: sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==}
dependencies:
has: 1.0.3
dev: true
/is-date-object@1.0.5:
resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
@ -3337,6 +3590,10 @@ packages:
dreamopt: 0.8.0
dev: true
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: false
/json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
@ -3397,7 +3654,6 @@ packages:
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
@ -3464,6 +3720,10 @@ packages:
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
dev: false
/math-random@2.0.1:
resolution: {integrity: sha512-oIEbWiVDxDpl5tIF4S6zYS9JExhh3bun3uLb3YAinHPTlRtW4g1S66LtJrJ4Npq8dgIa8CLK5iPVah5n4n0s2w==}
dev: false
/mdast-util-definitions@5.1.2:
resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==}
dependencies:
@ -4200,7 +4460,6 @@ packages:
engines: {node: '>=6'}
dependencies:
callsites: 3.1.0
dev: true
/parse-entities@2.0.0:
resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
@ -4213,6 +4472,16 @@ packages:
is-hexadecimal: 1.0.4
dev: false
/parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
dependencies:
'@babel/code-frame': 7.22.5
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
dev: false
/path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@ -4235,12 +4504,10 @@ packages:
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
/path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
dev: true
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@ -4385,6 +4652,14 @@ packages:
engines: {node: '>=6'}
dev: false
/prop-types@15.7.2:
resolution: {integrity: sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==}
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
dev: false
/prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:
@ -4504,6 +4779,23 @@ packages:
use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0)
dev: false
/react-scroll-to-bottom@4.2.0(react@18.2.0):
resolution: {integrity: sha512-1WweuumQc5JLzeAR81ykRdK/cEv9NlCPEm4vSwOGN1qS2qlpGVTyMgdI8Y7ZmaqRmzYBGV5/xPuJQtekYzQFGg==}
peerDependencies:
react: '>= 16.8.6'
dependencies:
'@babel/runtime-corejs3': 7.22.5
'@emotion/css': 11.1.3
classnames: 2.3.1
core-js: 3.18.3
math-random: 2.0.1
prop-types: 15.7.2
react: 18.2.0
simple-update-in: 2.2.0
transitivePeerDependencies:
- '@babel/core'
dev: false
/react-style-singleton@2.2.1(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@ -4630,7 +4922,6 @@ packages:
/resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
dev: true
/resolve@1.22.2:
resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
@ -4639,7 +4930,6 @@ packages:
is-core-module: 2.12.0
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
dev: true
/resolve@2.0.0-next.4:
resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==}
@ -4733,6 +5023,10 @@ packages:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
/simple-update-in@2.2.0:
resolution: {integrity: sha512-FrW41lLiOs82jKxwq39UrE1HDAHOvirKWk4Nv8tqnFFFknVbTxcHZzDS4vt02qqdU/5+KNsQHWzhKHznDBmrww==}
dev: false
/sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: true
@ -4751,6 +5045,11 @@ packages:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
/source-map@0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'}
dev: false
/space-separated-tokens@1.1.5:
resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==}
dev: false
@ -4859,6 +5158,10 @@ packages:
react: 18.2.0
dev: false
/stylis@4.2.0:
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
dev: false
/sucrase@3.32.0:
resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==}
engines: {node: '>=8'}
@ -4873,6 +5176,13 @@ packages:
ts-interface-checker: 0.1.13
dev: true
/supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
dev: false
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -4883,7 +5193,6 @@ packages:
/supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
dev: true
/swr@2.1.5(react@18.2.0):
resolution: {integrity: sha512-/OhfZMcEpuz77KavXST5q6XE9nrOBOVcBLWjMT+oAE/kQHyE3PASrevXCtQDZ8aamntOfFkbVJp7Il9tNBQWrw==}
@ -4984,6 +5293,11 @@ packages:
engines: {node: '>=12'}
dev: true
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
dev: false
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -5327,6 +5641,11 @@ packages:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true
/yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
dev: false
/yaml@2.2.2:
resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==}
engines: {node: '>= 14'}