fix: misc UI updates (#12)
This commit is contained in:
commit
320db5daeb
15 changed files with 163 additions and 167 deletions
|
|
@ -42,7 +42,7 @@ export const POST = auth(async function POST(req: Request) {
|
|||
if (req.auth?.user?.email == null) {
|
||||
return
|
||||
}
|
||||
const title = json.messages[0].content.substring(0, 20)
|
||||
const title = json.messages[0].content.substring(0, 100)
|
||||
const userId = (req as any).auth?.user?.email
|
||||
const id = json.id ?? nanoid()
|
||||
const createdAt = Date.now()
|
||||
|
|
|
|||
|
|
@ -4,25 +4,24 @@ import { type Message } from 'ai-connector'
|
|||
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { ChatMessage } from '@/components/chat-message'
|
||||
import { EmptyScreen } from '@/components/empty-screen'
|
||||
|
||||
export interface ChatList {
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
export function ChatList({ messages }: ChatList) {
|
||||
if (!messages.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto max-w-2xl px-4">
|
||||
{messages.length > 0 ? (
|
||||
messages.map((message, index) => (
|
||||
<div key={index}>
|
||||
<ChatMessage message={message} />
|
||||
{index < messages.length - 1 && <Separator className="my-8" />}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<EmptyScreen />
|
||||
)}
|
||||
{messages.map((message, index) => (
|
||||
<div key={index}>
|
||||
<ChatMessage message={message} />
|
||||
{index < messages.length - 1 && <Separator className="my-8" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
'use client'
|
||||
|
||||
import { UseChatHelpers } from 'ai-connector'
|
||||
import { type UseChatHelpers } from 'ai-connector'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ExternalLink } from '@/components/external-link'
|
||||
|
|
@ -11,7 +9,13 @@ import { IconRefresh, IconStop } from '@/components/ui/icons'
|
|||
export interface ChatPanelProps
|
||||
extends Pick<
|
||||
UseChatHelpers,
|
||||
'append' | 'isLoading' | 'reload' | 'messages' | 'stop'
|
||||
| 'append'
|
||||
| 'isLoading'
|
||||
| 'reload'
|
||||
| 'messages'
|
||||
| 'stop'
|
||||
| 'input'
|
||||
| 'setInput'
|
||||
> {
|
||||
id?: string
|
||||
}
|
||||
|
|
@ -21,6 +25,8 @@ export function ChatPanel({
|
|||
stop,
|
||||
append,
|
||||
reload,
|
||||
input,
|
||||
setInput,
|
||||
messages
|
||||
}: ChatPanelProps) {
|
||||
return (
|
||||
|
|
@ -58,6 +64,8 @@ export function ChatPanel({
|
|||
role: 'user'
|
||||
})
|
||||
}}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<p className="hidden px-2 text-center text-xs leading-normal text-muted-foreground sm:block">
|
||||
|
|
|
|||
|
|
@ -2,24 +2,32 @@
|
|||
|
||||
import { useChat, type Message } from 'ai-connector'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChatList } from '@/components/chat-list'
|
||||
import { ChatPanel } from '@/components/chat-panel'
|
||||
import { EmptyScreen } from '@/components/empty-screen'
|
||||
|
||||
export interface ChatProps {
|
||||
// create?: (input: string) => Chat | undefined;
|
||||
export interface ChatProps extends React.ComponentProps<'div'> {
|
||||
initialMessages?: Message[]
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function Chat({ id, initialMessages }: ChatProps) {
|
||||
const { messages, append, reload, stop, isLoading } = useChat({
|
||||
initialMessages,
|
||||
id
|
||||
})
|
||||
export function Chat({ id, initialMessages, className }: ChatProps) {
|
||||
const { messages, append, reload, stop, isLoading, input, setInput } =
|
||||
useChat({
|
||||
initialMessages,
|
||||
id
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="pb-[200px] pt-4 md:pt-10">
|
||||
<ChatList messages={messages} />
|
||||
<>
|
||||
<div className={cn('pb-[200px] pt-4 md:pt-10', className)}>
|
||||
{messages.length ? (
|
||||
<ChatList messages={messages} />
|
||||
) : (
|
||||
<EmptyScreen setInput={setInput} />
|
||||
)}
|
||||
</div>
|
||||
<ChatPanel
|
||||
id={id}
|
||||
isLoading={isLoading}
|
||||
|
|
@ -27,7 +35,9 @@ export function Chat({ id, initialMessages }: ChatProps) {
|
|||
append={append}
|
||||
reload={reload}
|
||||
messages={messages}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useChatStore } from '@/lib/hooks/use-chat-store'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ExternalLink } from '@/components/external-link'
|
||||
import { IconArrowRight } from '@/components/ui/icons'
|
||||
import { UseChatHelpers } from 'ai-connector'
|
||||
|
||||
const exampleMessages = [
|
||||
{
|
||||
|
|
@ -19,37 +19,37 @@ const exampleMessages = [
|
|||
}
|
||||
]
|
||||
|
||||
export function EmptyScreen({ className }: React.ComponentProps<'div'>) {
|
||||
const { setDefaultMessage } = useChatStore()
|
||||
|
||||
export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
|
||||
return (
|
||||
<div className={cn('rounded-lg border bg-background p-8', className)}>
|
||||
<h1 className="mb-2 text-lg font-semibold">
|
||||
Welcome to Next.js Chatbot!
|
||||
</h1>
|
||||
<p className="mb-2 leading-normal text-muted-foreground">
|
||||
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="leading-normal text-muted-foreground">
|
||||
You can start a conversation here or try the following examples:
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col items-start space-y-2">
|
||||
{exampleMessages.map((message, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="link"
|
||||
className="h-auto p-0 text-base"
|
||||
onClick={() => setDefaultMessage(message.message)}
|
||||
>
|
||||
<IconArrowRight className="mr-2 text-muted-foreground" />
|
||||
{message.heading}
|
||||
</Button>
|
||||
))}
|
||||
<div className="mx-auto max-w-2xl px-4">
|
||||
<div className="rounded-lg border bg-background p-8">
|
||||
<h1 className="mb-2 text-lg font-semibold">
|
||||
Welcome to Next.js Chatbot!
|
||||
</h1>
|
||||
<p className="mb-2 leading-normal text-muted-foreground">
|
||||
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="leading-normal text-muted-foreground">
|
||||
You can start a conversation here or try the following examples:
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col items-start space-y-2">
|
||||
{exampleMessages.map((message, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="link"
|
||||
className="h-auto p-0 text-base"
|
||||
onClick={() => setInput(message.message)}
|
||||
>
|
||||
<IconArrowRight className="mr-2 text-muted-foreground" />
|
||||
{message.heading}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export async function Header() {
|
|||
<SidebarList session={session} />
|
||||
</Suspense>
|
||||
</Sidebar>
|
||||
<div className="hidden items-center md:flex">
|
||||
<div className="flex items-center">
|
||||
<IconSeparator className="h-6 w-6 text-muted-foreground/50" />
|
||||
<UserMenu session={session} />
|
||||
</div>
|
||||
|
|
@ -33,15 +33,15 @@ export async function Header() {
|
|||
rel="noopener noreferrer"
|
||||
className={cn(buttonVariants({ variant: 'outline' }))}
|
||||
>
|
||||
<IconGitHub className="mr-2 h-4 w-4" />
|
||||
<span>GitHub</span>
|
||||
<IconGitHub />
|
||||
<span className="hidden md:flex ml-2">GitHub</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/vercel/nextjs-ai-chatbot/"
|
||||
target="_blank"
|
||||
className={cn(buttonVariants())}
|
||||
>
|
||||
<IconVercel className="mr-2 h-4 w-4" />
|
||||
<IconVercel className="mr-2" />
|
||||
<span className="hidden sm:block">Deploy to Vercel</span>
|
||||
<span className="sm:hidden">Deploy</span>
|
||||
</a>
|
||||
|
|
|
|||
40
components/login-button.tsx
Normal file
40
components/login-button.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { signIn } from '@auth/nextjs/client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button, type ButtonProps } from '@/components/ui/button'
|
||||
import { IconGitHub, IconSpinner } from '@/components/ui/icons'
|
||||
|
||||
interface LoginButtonProps extends ButtonProps {
|
||||
text?: string
|
||||
}
|
||||
|
||||
export function LoginButton({
|
||||
className,
|
||||
text = 'Login with GitHub',
|
||||
...props
|
||||
}: LoginButtonProps) {
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsLoading(true)
|
||||
signIn('github')
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<IconSpinner className="mr-2 animate-spin" />
|
||||
) : (
|
||||
<IconGitHub className="mr-2" />
|
||||
)}
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
|
||||
import { useChatStore } from '@/lib/hooks/use-chat-store'
|
||||
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
|
|
@ -15,15 +12,20 @@ import {
|
|||
TooltipTrigger
|
||||
} from '@/components/ui/tooltip'
|
||||
import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
|
||||
import { UseChatHelpers } from 'ai-connector'
|
||||
|
||||
export interface PromptProps {
|
||||
export interface PromptProps
|
||||
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
||||
onSubmit: (value: string) => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export function PromptForm({ onSubmit, isLoading }: PromptProps) {
|
||||
const { defaultMessage } = useChatStore()
|
||||
const [input, setInput] = React.useState(defaultMessage)
|
||||
export function PromptForm({
|
||||
onSubmit,
|
||||
input,
|
||||
setInput,
|
||||
isLoading
|
||||
}: PromptProps) {
|
||||
const { formRef, onKeyDown } = useEnterSubmit()
|
||||
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
|
|
@ -33,10 +35,6 @@ export function PromptForm({ onSubmit, isLoading }: PromptProps) {
|
|||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
setInput(defaultMessage)
|
||||
}, [defaultMessage])
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={async e => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,30 @@
|
|||
import { getChats } from '@/app/actions'
|
||||
import { Session } from '@auth/core/types'
|
||||
import { SidebarItem } from './sidebar-item'
|
||||
|
||||
import { SidebarItem } from '@/components/sidebar-item'
|
||||
import { LoginButton } from '@/components/login-button'
|
||||
|
||||
export interface SidebarListProps {
|
||||
session?: Session
|
||||
}
|
||||
|
||||
export async function SidebarList(props: SidebarListProps) {
|
||||
const chats = await getChats(props.session?.user?.email)
|
||||
export async function SidebarList({ session }: SidebarListProps) {
|
||||
if (!session?.user?.email) {
|
||||
return (
|
||||
<div className="flex-1 p-6 flex text-sm items-center flex-col space-y-4">
|
||||
<div className="space-y-1 text-center">
|
||||
<p className="font-medium">You are not logged in!</p>
|
||||
<p className="text-muted-foreground">
|
||||
Please login for chat history.
|
||||
</p>
|
||||
</div>
|
||||
<LoginButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const chats = await getChats(session.user.email)
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{chats?.length ? (
|
||||
|
|
@ -16,7 +33,7 @@ export async function SidebarList(props: SidebarListProps) {
|
|||
<SidebarItem
|
||||
key={chat.id}
|
||||
title={chat.title}
|
||||
userId={props?.session?.user?.email ?? ''}
|
||||
userId={session?.user?.email ?? ''}
|
||||
href={`/chat/${chat.id}`}
|
||||
id={chat.id}
|
||||
/>
|
||||
|
|
@ -24,13 +41,7 @@ export async function SidebarList(props: SidebarListProps) {
|
|||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{props?.session?.user ? (
|
||||
<>No chat history</>
|
||||
) : (
|
||||
<>Login for history</>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">No chat history</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@ export function Sidebar({ session, children }: SidebarProps) {
|
|||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
position="left"
|
||||
className="inset-y-0 flex h-auto w-[300px] flex-col gap-0 rounded-r-lg p-0 lg:inset-y-2"
|
||||
>
|
||||
<SheetContent className="inset-y-0 flex h-auto w-[300px] flex-col p-0">
|
||||
<SheetHeader className="p-4">
|
||||
<SheetTitle className="text-sm">Chat History</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import * as React from 'react'
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { IconClose } from '@/components/ui/icons'
|
||||
|
|
@ -42,33 +41,17 @@ const SheetOverlay = React.forwardRef<
|
|||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 h-full scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg',
|
||||
{
|
||||
variants: {
|
||||
position: {
|
||||
left: 'data-[state=closed]:animate-slide-to-left data-[state=open]:animate-slide-from-left',
|
||||
right: 'h-full animate-in slide-in-from-right duration-300'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
position: 'left'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export interface DialogContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
DialogContentProps
|
||||
>(({ position, className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ position }), className)}
|
||||
className={cn(
|
||||
'fixed z-50 h-full border-r bg-background p-6 opacity-100 shadow-lg data-[state=closed]:animate-slide-to-left data-[state=open]:animate-slide-from-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -85,13 +68,7 @@ const SheetHeader = ({
|
|||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<div className={cn('flex flex-col space-y-2', className)} {...props} />
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,24 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { signIn } from '@auth/nextjs/client'
|
||||
import { type Session } from '@auth/nextjs/types'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { IconSpinner } from '@/components/ui/icons'
|
||||
import { LoginButton } from '@/components/login-button'
|
||||
|
||||
export interface UserMenuProps {
|
||||
session?: Session
|
||||
}
|
||||
|
||||
export function UserMenu({ session }: UserMenuProps) {
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
|
||||
if (!session?.user) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsLoading(true)
|
||||
signIn('github')
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <IconSpinner className="mr-2 animate-spin" />}
|
||||
Login
|
||||
</Button>
|
||||
<LoginButton variant="ghost" className="[&_svg]:hidden" text="Login" />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="px-2 text-sm font-medium">Logged in as {session.user.name}</p>
|
||||
<p className="px-2 text-sm font-medium truncate w-[100px] sm:w-auto">
|
||||
{session.user.name}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { create } from 'zustand'
|
||||
|
||||
interface UseChatStore {
|
||||
defaultMessage: string
|
||||
setDefaultMessage: (message: string) => void
|
||||
}
|
||||
|
||||
export const useChatStore = create<UseChatStore>()(set => ({
|
||||
defaultMessage: '',
|
||||
setDefaultMessage: message => set({ defaultMessage: message })
|
||||
}))
|
||||
|
|
@ -39,8 +39,7 @@
|
|||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"zustand": "^4.3.8"
|
||||
"remark-math": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
|
|
|
|||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
|
|
@ -86,9 +86,6 @@ dependencies:
|
|||
remark-math:
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
zustand:
|
||||
specifier: ^4.3.8
|
||||
version: 4.3.8(react@18.2.0)
|
||||
|
||||
devDependencies:
|
||||
'@tailwindcss/typography':
|
||||
|
|
@ -5070,22 +5067,6 @@ packages:
|
|||
/zod@3.21.4:
|
||||
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
|
||||
|
||||
/zustand@4.3.8(react@18.2.0):
|
||||
resolution: {integrity: sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
peerDependencies:
|
||||
immer: '>=9.0'
|
||||
react: '>=16.8'
|
||||
peerDependenciesMeta:
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
dev: false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue