feat: implement new ui
This commit is contained in:
parent
4d5af8c62a
commit
6293470332
28 changed files with 1417 additions and 584 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
43
app/chat.tsx
43
app/chat.tsx
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
69
app/empty-screen.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
52
app/header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
11
app/page.tsx
11
app/page.tsx
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
106
app/sidebar.tsx
106
app/sidebar.tsx
|
|
@ -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 ?? '')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue