feat: implement new ui

This commit is contained in:
shadcn 2023-06-07 16:17:59 +04:00
parent 4d5af8c62a
commit 6293470332
28 changed files with 1417 additions and 584 deletions

View file

@ -3,9 +3,7 @@
import { type Message } from 'ai-connector'
import { ChatMessage } from './chat-message'
import { NextChatLogo } from '@/components/ui/nextchat-logo'
import { Plus } from 'lucide-react'
import { EmptyScreen } from './empty'
import { EmptyScreen } from './empty-screen'
export interface ChatList {
messages: Message[]
@ -13,8 +11,8 @@ export interface ChatList {
export function ChatList({ messages }: ChatList) {
return (
<div className="relative h-full dark:bg-zinc-900">
<div className="sticky top-0 border-b w-full bg-black md:hidden text-white font-semibold">
<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" />
@ -26,21 +24,21 @@ export function ChatList({ messages }: ChatList) {
</button>
</div>
</div>
</div>
<div className="h-full w-full overflow-auto">
{messages.length > 0 ? (
<div className="group w-full text-zinc-900 dark:text-white divide-y dark:divide-zinc-800">
{messages.map(message => (
<ChatMessage
key={message.id || message.content}
message={message}
/>
))}
</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>
)}
</div>
)
}

View file

@ -5,115 +5,109 @@ 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 { OpenAI } from '@/components/icons'
export interface ChatMessageProps {
message: Message
}
export function ChatMessage(props: ChatMessageProps) {
export function ChatMessage({ message, ...props }: ChatMessageProps) {
return (
<div
className={cn(
{
'bg-zinc-50': props.message.role === 'assistant'
},
'pr-0 lg:pr-[260px]',
fontMessage.className
'flex items-start space-x-4 mb-4',
fontMessage.className,
message.role === 'user' && 'mt-12 first:mt-0'
)}
{...props}
>
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-xl xl:max-w-3xl">
<div className="flex w-full gap-4 items-start">
{props.message.role === 'user' ? (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 p-2 select-none">
<div
className="font-medium uppercase text-zinc-100"
style={{ fontSize: 6 }}
>
User
</div>
</div>
) : (
<div className="flex h-8 w-8 shrink-0 items-center justify-center select-none bg-zinc-300 rounded-full">
<IconOpenAI className="h-5 w-5" />
</div>
)}
<MemoizedReactMarkdown
className="prose prose-stone prose-base prose-pre:rounded-md w-full flex-1 leading-6 prose-p:leading-[1.8rem] prose-pre:bg-[#282c34] max-w-full"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
code({ node, inline, className, children, ...props }) {
if (children.length) {
if (children[0] == '▍') {
return (
<span className="mt-1 animate-pulse cursor-default">
</span>
)
}
children[0] = (children[0] as string).replace('`▍`', '▍')
<div
className={cn(
'flex h-8 w-8 -ml-2 shrink-0 items-center justify-center rounded-full select-none border',
message.role === 'assistant' && 'bg-primary text-primary-foreground'
)}
>
{message.role === 'user' ? (
<User className="w-4 h-4" />
) : (
<OpenAI className="w-4 h-4" />
)}
</div>
<div
className={cn(
'border rounded-lg py-2 px-4',
message.role === 'assistant' && 'bg-muted/30'
)}
>
<MemoizedReactMarkdown
className="prose dark:prose-invert prose-sm prose-pre:rounded-md w-full flex-1 leading-6 prose-p:leading-[1.8rem] prose-pre:bg-[#282c34] max-w-full"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
p({ children }) {
return (
<p
className={cn(
'mb-2 last:mb-0',
message.role === 'user' && 'font-medium'
)}
>
{children}
</p>
)
},
code({ node, inline, className, children, ...props }) {
if (children.length) {
if (children[0] == '▍') {
return (
<span className="mt-1 animate-pulse cursor-default"></span>
)
}
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
},
table({ children }) {
return (
<table className="border-collapse border border-black px-3 py-1 ">
{children}
</table>
)
},
th({ children }) {
return (
<th className="break-words border border-black bg-gray-500 px-3 py-1 text-white ">
{children}
</th>
)
},
td({ children }) {
return (
<td className="break-words border border-black px-3 py-1">
{children}
</td>
)
children[0] = (children[0] as string).replace('`▍`', '▍')
}
}}
>
{props.message.content}
</MemoizedReactMarkdown>
</div>
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
},
table({ children }) {
return (
<table className="border-collapse border border-black px-3 py-1 ">
{children}
</table>
)
},
th({ children }) {
return (
<th className="break-words border border-black bg-gray-500 px-3 py-1 text-white ">
{children}
</th>
)
},
td({ children }) {
return (
<td className="break-words border border-black px-3 py-1">
{children}
</td>
)
}
}}
>
{message.content}
</MemoizedReactMarkdown>
</div>
</div>
)
}
ChatMessage.displayName = 'ChatMessage'
export function IconOpenAI(props: JSX.IntrinsicElements['svg']) {
return (
<svg
fill="#000000"
width="800px"
height="800px"
viewBox="0 0 24 24"
role="img"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>OpenAI icon</title>
<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" />
</svg>
)
}

View file

@ -4,6 +4,7 @@ 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;
@ -27,24 +28,30 @@ export function Chat({
})
return (
<main className="transition-width relative min-h-full w-full flex-1 overflow-y-auto flex flex-col">
<div className="flex-1">
<ChatList messages={messages} />
<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 className="sticky light-gradient dark:bg-gradient-to-b dark:from-zinc-900 dark:to-zinc-950 bottom-0 left-0 w-full border-t bg-white dark:bg-black md:border-t-0 py-4 md:border-transparent md:!bg-transparent md:dark:border-transparent pr-0 lg:pr-[260px] flex dark:border-transparent items-center justify-center">
<Prompt
onSubmit={value => {
append({
content: value,
role: 'user'
})
}}
onRefresh={messages.length ? reload : undefined}
isLoading={isLoading}
/>
</div>
</main>
</div>
)
}
Chat.displayName = 'Chat'

View file

@ -32,6 +32,7 @@ export default async function ChatPage({ params }: ChatPageProps) {
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[]} />

69
app/empty-screen.tsx Normal file
View file

@ -0,0 +1,69 @@
import { cn } from '@/lib/utils'
import { ExternalLink } from './external-link'
import { ArrowRight } from 'lucide-react'
import { useChatStore } from '@/hooks/use-chat-store'
import { Button } from '@/components/ui/button'
import { OpenAI } from '@/components/icons'
const exampleMessages = [
{
heading: 'Explain technical concepts',
message: `What is a "serverless function"?`
},
{
heading: 'Summarize article',
message: 'Summarize the following article for a 2nd grader: \n'
},
{
heading: 'Draft an email',
message: `Draft an email to my boss about the following: \n`
}
]
export function EmptyScreen({ className }: React.ComponentProps<'div'>) {
const { setDefaultMessage } = useChatStore()
return (
<div className="flex items-start space-x-4 mb-4">
<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'
)}
>
<OpenAI className="w-4 h-4" />
</div>
<div
className={cn(
'border rounded-lg flex-1 p-10 bg-gradient-to-b from-background via-muted/10 to-muted/50',
className
)}
>
<h1 className="font-semibold mb-2">Welcome to Next.js Chatbot!</h1>
<p className="text-muted-foreground text-sm leading-normal">
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>
<p className="text-muted-foreground text-sm leading-normal">
You can start a conversation here or try the following examples:
</p>
<div className="text-sm mt-4 flex flex-col items-start space-y-2">
{exampleMessages.map((message, index) => (
<Button
key={index}
variant="link"
className="p-0 h-auto"
onClick={() => setDefaultMessage(message.message)}
>
<ArrowRight className="w-4 h-4 mr-2 text-muted-foreground" />
{message.heading}
</Button>
))}
</div>
</div>
</div>
)
}

View file

@ -1,56 +0,0 @@
import { cn } from '@/lib/utils'
import { ExternalLink } from './external-link'
import { fontMessage } from '@/lib/fonts'
function ExampleBubble({ children }: { children?: React.ReactNode }) {
return (
<div
className={cn(
'flex-1 flex items-start justify-between p-4 rounded-lg relative bg-zinc-100 text-sm font-medium transition-all cursor-pointer select-none hover:drop-shadow-[0_1px_1px_rgba(0,0,0,0.08)] hover:shadow-[0rem_0rem_0rem_0.0625rem_rgba(0,0,0,.02),0rem_0.125rem_0.25rem_rgba(0,0,0,.08),0rem_0.45rem_1rem_rgba(0,0,0,.06)] hover:bg-white group duration-300',
fontMessage.className
)}
style={{}}
>
<div
className="bg-zinc-100 w-9 h-6 absolute left-0 top-full -mt-[1px] scale-75 origin-top-left group-hover:bg-white transition-all duration-300"
style={{
clipPath:
'path("M31.1838 0C24.9593 10.7604 13.3251 18 0 18C9.94113 18 18 9.94113 18 0H31.1838Z")'
}}
></div>
<p>{children}</p>
</div>
)
}
export function EmptyScreen() {
return (
<div
className={cn(
'w-full h-full flex items-center justify-center pt-4 pb-8 pr-0 lg:pr-[260px]'
)}
>
<div className="p-8 rounded-lg flex flex-col items-center justify-center gap-8 max-w-2xl">
<div className="flex items-center justify-center gap-6">
<div className="text-zinc-500 font-medium">
<p>Welcome to Next.js Chatbot!</p>
<p className="mt-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>
. You can start a conversation here or try the following examples:
</p>
</div>
</div>
<div className="grid gap-x-4 gap-y-6 grid-cols-2 w-full">
<ExampleBubble>Explain technical concepts</ExampleBubble>
<ExampleBubble>Summarize article</ExampleBubble>
<ExampleBubble>Get assistance</ExampleBubble>
<ExampleBubble>Draft an email</ExampleBubble>
</div>
</div>
</div>
)
}

View file

@ -3,14 +3,80 @@
@tailwind utilities;
@layer base {
#__next,
#root {
height: 100%;
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
body,
html {
height: 100%;
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--popover: 224 71% 4%;
--popover-foreground: 215 20.2% 65.1%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: 'rlig' 1, 'calt' 1;
}
}

52
app/header.tsx Normal file
View file

@ -0,0 +1,52 @@
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,9 +1,11 @@
import './globals.css'
import { Metadata } from 'next'
import './globals.css'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { ThemeProvider } from '@/components/theme-provider'
import { fontMono, fontSans } from '@/lib/fonts'
import { cn } from '@/lib/utils'
import { Header } from '@/app/header'
export const metadata: Metadata = {
title: {
@ -28,22 +30,24 @@ interface RootLayoutProps {
export default function RootLayout({ children }: RootLayoutProps) {
return (
<>
<html lang="en" suppressHydrationWarning>
<head />
<body
className={cn(
'font-sans antialiased',
fontSans.variable,
fontMono.variable
)}
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
{/* <TailwindIndicator /> */}
</ThemeProvider>
</body>
</html>
</>
<html lang="en" suppressHydrationWarning>
<head />
<body
className={cn(
'font-sans antialiased',
fontSans.variable,
fontMono.variable
)}
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="h-screen flex flex-col">
{/* @ts-ignore */}
<Header />
<main className="flex-1 overflow-hidden">{children}</main>
</div>
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
)
}

View file

@ -1,18 +1,13 @@
import { type Message } from 'ai-connector'
import { Chat } from './chat'
import { Sidebar } from './sidebar'
import { auth } from '@/auth'
export const runtime = 'edge'
export const preferredRegion = 'home'
export default async function IndexPage() {
const session = await auth()
return (
<div className="relative flex h-full w-full overflow-hidden">
<Sidebar session={session} newChat />
<div className="flex h-full min-w-0 flex-1 flex-col">
<Chat />
</div>
<div className="h-full overflow-hidden">
<Chat />
</div>
)
}

View file

@ -1,5 +1,6 @@
'use client'
import * as React from 'react'
import { CornerDownLeft, RefreshCcw, StopCircle } from 'lucide-react'
import { useState } from 'react'
import Textarea from 'react-textarea-autosize'
@ -8,6 +9,13 @@ import { Button } from '@/components/ui/button'
import { fontMessage } from '@/lib/fonts'
import { useCmdEnterSubmit } from '@/lib/hooks/use-command-enter-submit'
import { cn } from '@/lib/utils'
import { useChatStore } from '@/hooks/use-chat-store'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip'
export interface PromptProps {
onSubmit: (value: string) => void
@ -22,8 +30,21 @@ export function Prompt({
onAbort,
isLoading
}: PromptProps) {
const [input, setInput] = useState('')
const { defaultMessage } = useChatStore()
const [input, setInput] = useState(defaultMessage)
const { formRef, onKeyDown } = useCmdEnterSubmit()
const inputRef = React.useRef<HTMLTextAreaElement>(null)
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
React.useEffect(() => {
setInput(defaultMessage)
}, [defaultMessage])
return (
<form
onSubmit={async e => {
@ -32,7 +53,6 @@ export function Prompt({
await onSubmit(input)
}}
ref={formRef}
className="stretch flex w-full flex-row gap-3 md:max-w-2xl lg:max-w-xl xl:max-w-3xl mx-auto px-4"
>
<div className="relative flex h-full flex-1 flex-row-reverse items-stretch md:flex-col">
<div>
@ -71,8 +91,9 @@ export function Prompt({
</button> */}
</div>
</div>
<div className="relative flex w-full grow flex-col rounded-md dark:focus:border-white border border-zinc/10 bg-white shadow-[0_10px_20px_-10px_rgba(0,0,0,0.20)] dark:border-zinc-700 dark:bg-zinc-950 dark:text-white dark:shadow-[0_5px_15px_rgba(0,0,0,0.10)] focus-within:border-zinc-800 focus-within:shadow-[0_15px_10px_-12px_rgba(0,0,0,0.22)] transition-all duration-200 focus-within:duration-1000 ease-in-out">
<div className="relative flex w-full grow flex-col rounded-md border bg-background overflow-hidden pr-12">
<Textarea
ref={inputRef}
tabIndex={0}
onKeyDown={onKeyDown}
rows={1}
@ -81,34 +102,44 @@ export function Prompt({
placeholder="Send a message."
spellCheck={false}
className={cn(
'm-0 max-h-[200px] w-full resize-none border-0 bg-transparent p-0 py-[13px] pl-4 pr-7 outline-none ring-0 focus:ring-0 focus-visible:ring-0 dark:bg-transparent',
'min-h-[60px] text-sm p-4 bg-transparent focus-within:outline-none w-full resize-none',
fontMessage.className
)}
style={{
height: 46,
overflowY: 'hidden'
}}
/>
{isLoading ? (
<button
type="button"
onClick={onAbort}
className="absolute top-0 bottom-0 m-auto h-8 w-8 flex items-center justify-center right-2 rounded p-2 text-zinc-500 hover:bg-zinc-100 disabled:opacity-40 disabled:hover:bg-transparent dark:hover:bg-zinc-900 enabled:dark:hover:text-zinc-400 dark:disabled:hover:bg-transparent"
>
<StopCircle className="h-4 w-4" />
</button>
) : (
<button
type="submit"
className="absolute top-0 bottom-0 m-auto h-8 w-8 flex items-center justify-center right-2 rounded p-2 text-zinc-500 hover:bg-zinc-100 disabled:opacity-40 disabled:hover:bg-transparent dark:hover:bg-zinc-900 enabled:dark:hover:text-zinc-400 dark:disabled:hover:bg-transparent"
>
<CornerDownLeft className="h-4 w-4" />
</button>
)}
<div className="absolute top-4 right-4">
<TooltipProvider>
{isLoading ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
type="button"
onClick={onAbort}
className="h-8 w-8 p-0"
>
<StopCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Stop generating</TooltipContent>
</Tooltip>
) : (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
type="submit"
className="h-8 w-8 p-0"
>
<CornerDownLeft className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
)}
</TooltipProvider>
</div>
</div>
</div>
</form>
)
}
Prompt.displayName = 'Prompt'

View file

@ -1,90 +1,42 @@
import { Login } from '@/components/ui/login'
import { NextChatLogo } from '@/components/ui/nextchat-logo'
import { UserMenu } from '@/components/ui/user-menu'
import { kv } from '@vercel/kv'
import { Chat } from '@/lib/types'
import { cn } from '@/lib/utils'
import { type Session } from '@auth/nextjs/types'
import { Plus } from 'lucide-react'
import Link from 'next/link'
import { ExternalLink } from './external-link'
import { kv } from '@vercel/kv'
import { Sidebar as SidebarIcon } from 'lucide-react'
import { SidebarItem } from './sidebar-item'
export interface SidebarProps {
session?: Session
newChat?: boolean
}
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()
export function Sidebar({ session, newChat }: SidebarProps) {
return (
<div className="hidden shrink-0 bg-zinc-200/80 dark:bg-black md:flex md:w-[260px] md:flex-col">
<div className="flex h-full min-h-0 flex-col ">
<div className="scrollbar-trigger relative h-full w-full flex-1 items-start">
<aside className="flex h-full w-full flex-col p-2 shadow-lg ring-1 ring-zinc-900/10 dark:ring-zinc-200/10 relative z-10">
<div className="flex flex-row gap-1 items-center justify-between text-base font-semibold tracking-tight antialiased bg-black text-white px-4 py-4 -m-2 mb-2">
<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>
{session?.user ? <UserMenu session={session} /> : <Login />}
</div>
</div>
<Link
href="/"
className={cn(
'flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm text-zinc-800 dark:text-zinc-200 font-medium transition-colors duration-300 dark:hover:bg-zinc-300/20 select-none',
newChat
? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
: 'hover:bg-zinc-500/10'
)}
>
<Plus className="h-4 w-4" />
New Chat
</Link>
<div className="mt-2 pt-2 border-t border-zinc-300 dark:border-zinc-700 flex-1 flex-col overflow-y-auto overflow-x-hidden transition-opacity duration-500">
<div className="flex flex-col pb-2 text-sm text-zinc-100">
{/* @ts-ignore */}
<SidebarList session={session} />
</div>
</div>
<div>
<div className="flex gap-4 items-center justify-center text-sm font-medium leading-4 whitespace-nowrap">
<a
href="https://github.com/vercel/nextjs-ai-chatbot/"
target="_blank"
className="inline-flex gap-2 px-3 py-2 items-center border border-zinc-300 rounded-md hover:border-zinc-600 transition-colors"
>
<svg
aria-label="Vercel logomark"
height="13"
role="img"
className="w-auto overflow-hidden"
viewBox="0 0 74 64"
>
<path
d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
fill="var(--geist-foreground)"
></path>
</svg>
<span>Deploy</span>
</a>
<ExternalLink href="https://github.com/vercel/nextjs-ai-chatbot/">
View on GitHub
</ExternalLink>
</div>
</div>
</aside>
<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>
</div>
</div>
{/* @ts-ignore */}
<SidebarList session={session} />
</SheetContent>
</Sheet>
)
}
Sidebar.displayName = 'Sidebar'
async function SidebarList({ session }: { session?: Session }) {
const results: Chat[] = await getChats(session?.user?.email ?? '')