feat: update chat display and fix issue with regeneration
This commit is contained in:
parent
e06f34b0f1
commit
8f64c55896
17 changed files with 266 additions and 231 deletions
|
|
@ -5,68 +5,66 @@
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 47.4% 11.2%;
|
--foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 240 4.8% 95.9%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 47.4% 11.2%;
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 47.4% 11.2%;
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 240 5.9% 90%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 240 5.9% 90%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 240 5.9% 10%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 240 4.8% 95.9%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 240 4.8% 95.9%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: ;
|
||||||
|
|
||||||
--destructive: 0 100% 50%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--ring: 215 20.2% 65.1%;
|
--ring: 240 5% 64.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 224 71% 4%;
|
--background: 240 10% 3.9%;
|
||||||
--foreground: 213 31% 91%;
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
--muted: 223 47% 11%;
|
--muted: 240 3.7% 15.9%;
|
||||||
--muted-foreground: 215.4 16.3% 56.9%;
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
|
||||||
--popover: 224 71% 4%;
|
--popover: 240 10% 3.9%;
|
||||||
--popover-foreground: 215 20.2% 65.1%;
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--card: 224 71% 4%;
|
--card: 240 10% 3.9%;
|
||||||
--card-foreground: 213 31% 91%;
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--border: 216 34% 17%;
|
--border: 240 3.7% 15.9%;
|
||||||
--input: 216 34% 17%;
|
--input: 240 3.7% 15.9%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 222.2 47.4% 1.2%;
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--secondary: 222.2 47.4% 11.2%;
|
--secondary: 240 3.7% 15.9%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--accent: 216 34% 17%;
|
--accent: 240 3.7% 15.9%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: ;
|
||||||
|
|
||||||
--destructive: 0 63% 31%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
|
||||||
--ring: 216 34% 17%;
|
--ring: 240 3.7% 15.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,10 +74,5 @@
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-feature-settings: 'rlig' 1, 'calt' 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pre:has(div.codeblock) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { type Message } from 'ai-connector'
|
||||||
|
|
||||||
import { ChatMessage } from '@/components/chat-message'
|
import { ChatMessage } from '@/components/chat-message'
|
||||||
import { EmptyScreen } from '@/components/empty-screen'
|
import { EmptyScreen } from '@/components/empty-screen'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
export interface ChatList {
|
export interface ChatList {
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
|
|
@ -11,15 +12,16 @@ export interface ChatList {
|
||||||
|
|
||||||
export function ChatList({ messages }: ChatList) {
|
export function ChatList({ messages }: ChatList) {
|
||||||
return (
|
return (
|
||||||
<div className="relative max-w-2xl mx-auto">
|
<div className="relative max-w-2xl px-4 mx-auto">
|
||||||
{messages.length > 0 ? (
|
{messages.length > 0 ? (
|
||||||
messages.map(message => (
|
messages.map((message, index) => (
|
||||||
<ChatMessage key={message.id || message.content} message={message} />
|
<div key={index}>
|
||||||
|
<ChatMessage message={message} />
|
||||||
|
{index < messages.length - 1 && <Separator className="my-8" />}
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="pt-10">
|
<EmptyScreen />
|
||||||
<EmptyScreen />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import remarkMath from 'remark-math'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { fontMessage } from '@/lib/fonts'
|
import { fontMessage } from '@/lib/fonts'
|
||||||
import CodeBlock from '@/components/ui/codeblock'
|
import { CodeBlock } from '@/components/ui/codeblock'
|
||||||
import { MemoizedReactMarkdown } from '@/components/markdown'
|
import { MemoizedReactMarkdown } from '@/components/markdown'
|
||||||
import { OpenAI } from '@/components/icons'
|
import { OpenAI } from '@/components/icons'
|
||||||
|
|
||||||
|
|
@ -17,16 +17,17 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start space-x-4 mb-4 relative -ml-12',
|
'flex items-start mb-4 relative md:-ml-12',
|
||||||
fontMessage.className,
|
fontMessage.className
|
||||||
message.role === 'user' && 'mt-12 first:mt-0'
|
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full select-none border',
|
'md:flex h-8 w-8 shrink-0 hidden items-center justify-center rounded-full select-none border',
|
||||||
message.role === 'assistant' && 'bg-primary text-primary-foreground'
|
message.role === 'user'
|
||||||
|
? 'bg-background'
|
||||||
|
: 'bg-primary text-primary-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.role === 'user' ? (
|
{message.role === 'user' ? (
|
||||||
|
|
@ -35,14 +36,9 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
||||||
<OpenAI className="w-4 h-4" />
|
<OpenAI className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="md:ml-4">
|
||||||
className={cn(
|
|
||||||
'border rounded-lg py-2 px-4',
|
|
||||||
message.role === 'assistant' && 'bg-muted/30'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MemoizedReactMarkdown
|
<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"
|
className="prose dark:prose-invert prose-pre:rounded-md leading-6 prose-p:leading-[1.8rem] prose-pre:bg-[#282c34]"
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
components={{
|
components={{
|
||||||
p({ children }) {
|
p({ children }) {
|
||||||
|
|
@ -50,7 +46,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'mb-2 last:mb-0',
|
'mb-2 last:mb-0',
|
||||||
message.role === 'user' && 'font-medium'
|
message.role === 'user' && 'font-semibold'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,54 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { ExternalLink } from '@/components/external-link'
|
|
||||||
import { UseChatHelpers } from 'ai-connector'
|
import { UseChatHelpers } from 'ai-connector'
|
||||||
import { PromptForm } from './prompt-form'
|
import { RefreshCcw, StopCircle } from 'lucide-react'
|
||||||
|
|
||||||
|
import { ExternalLink } from '@/components/external-link'
|
||||||
|
import { PromptForm } from '@/components/prompt-form'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
export interface ChatPanelProps
|
export interface ChatPanelProps
|
||||||
extends Pick<UseChatHelpers, 'append' | 'isLoading' | 'reload' | 'messages'> {
|
extends Pick<
|
||||||
|
UseChatHelpers,
|
||||||
|
'append' | 'isLoading' | 'reload' | 'messages' | 'stop'
|
||||||
|
> {
|
||||||
id?: string
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatPanel({
|
export function ChatPanel({
|
||||||
id,
|
|
||||||
append,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
|
stop,
|
||||||
|
append,
|
||||||
reload,
|
reload,
|
||||||
messages
|
messages
|
||||||
}: ChatPanelProps) {
|
}: ChatPanelProps) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0">
|
<div className="fixed bottom-0 left-0 right-0 bg-gradient-to-b from-muted/10 from-10% to-muted/30 to-50%">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="sm:max-w-2xl sm:px-4 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">
|
<div className="flex items-center justify-center py-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => stop()}
|
||||||
|
className="bg-background"
|
||||||
|
>
|
||||||
|
<StopCircle className="h-4 w-4 mr-2" />
|
||||||
|
Stop generating
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
messages?.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => reload()}
|
||||||
|
className="bg-background"
|
||||||
|
>
|
||||||
|
<RefreshCcw className="h-4 w-4 mr-2" />
|
||||||
|
Regenerate response
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="sm:p-4 border-t sm:border bg-background space-y-4 shadow-lg sm:rounded-t-xl">
|
||||||
<PromptForm
|
<PromptForm
|
||||||
onSubmit={value => {
|
onSubmit={value => {
|
||||||
append({
|
append({
|
||||||
|
|
@ -27,11 +56,10 @@ export function ChatPanel({
|
||||||
role: 'user'
|
role: 'user'
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onRefresh={messages.length ? reload : undefined}
|
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted-foreground text-xs leading-normal text-center pb-1">
|
<p className="hidden sm:block text-muted-foreground text-xs leading-normal text-center px-2">
|
||||||
This is an open source AI chatbot app built with{' '}
|
Open source AI chatbot app built with{' '}
|
||||||
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
||||||
<ExternalLink href="https://vercel.com/storage/kv">
|
<ExternalLink href="https://vercel.com/storage/kv">
|
||||||
Vercel KV
|
Vercel KV
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,22 @@ export interface ChatProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat({ id, initialMessages }: ChatProps) {
|
export function Chat({ id, initialMessages }: ChatProps) {
|
||||||
const { messages, append, reload, isLoading } = useChat({
|
const { messages, append, reload, stop, isLoading } = useChat({
|
||||||
initialMessages,
|
initialMessages,
|
||||||
id
|
id
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-hidden">
|
<div className="h-full w-full overflow-auto pt-4 md:pt-10 bg-muted/50 pb-[200px]">
|
||||||
<div className="h-full w-full overflow-auto pb-[200px]">
|
<ChatList messages={messages} />
|
||||||
<ChatList messages={messages} />
|
<ChatPanel
|
||||||
<ChatPanel
|
id={id}
|
||||||
id={id}
|
isLoading={isLoading}
|
||||||
append={append}
|
stop={stop}
|
||||||
isLoading={isLoading}
|
append={append}
|
||||||
reload={reload}
|
reload={reload}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { ArrowRight } from 'lucide-react'
|
import { ArrowRight } from 'lucide-react'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { fontMessage } from '@/lib/fonts'
|
||||||
import { useChatStore } from '@/lib/hooks/use-chat-store'
|
import { useChatStore } from '@/lib/hooks/use-chat-store'
|
||||||
import { ExternalLink } from '@/components/external-link'
|
import { ExternalLink } from '@/components/external-link'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { OpenAI } from '@/components/icons'
|
|
||||||
|
|
||||||
const exampleMessages = [
|
const exampleMessages = [
|
||||||
{
|
{
|
||||||
|
|
@ -25,45 +25,39 @@ export function EmptyScreen({ className }: React.ComponentProps<'div'>) {
|
||||||
const { setDefaultMessage } = useChatStore()
|
const { setDefaultMessage } = useChatStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start space-x-4 mb-4 -ml-12">
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
'border bg-background p-8 rounded-lg',
|
||||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full select-none border bg-primary text-primary-foreground'
|
fontMessage.className,
|
||||||
)}
|
className
|
||||||
>
|
)}
|
||||||
<OpenAI className="w-4 h-4" />
|
>
|
||||||
</div>
|
<h1 className="font-semibold text-lg mb-2">
|
||||||
<div
|
Welcome to Next.js Chatbot!
|
||||||
className={cn(
|
</h1>
|
||||||
'border rounded-lg flex-1 p-10 bg-gradient-to-b from-background via-muted/10 to-muted/50',
|
<p className="text-muted-foreground leading-normal mb-2">
|
||||||
className
|
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">
|
||||||
<h1 className="font-semibold mb-2">Welcome to Next.js Chatbot!</h1>
|
Vercel KV
|
||||||
<p className="text-muted-foreground text-sm leading-normal">
|
</ExternalLink>
|
||||||
This is an open source AI chatbot app built with{' '}
|
.
|
||||||
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
</p>
|
||||||
<ExternalLink href="https://vercel.com/storage/kv">
|
<p className="text-muted-foreground leading-normal">
|
||||||
Vercel KV
|
You can start a conversation here or try the following examples:
|
||||||
</ExternalLink>
|
</p>
|
||||||
.
|
<div className="mt-4 flex flex-col items-start space-y-2">
|
||||||
</p>
|
{exampleMessages.map((message, index) => (
|
||||||
<p className="text-muted-foreground text-sm leading-normal">
|
<Button
|
||||||
You can start a conversation here or try the following examples:
|
key={index}
|
||||||
</p>
|
variant="link"
|
||||||
<div className="text-sm mt-4 flex flex-col items-start space-y-2">
|
className="p-0 h-auto text-base"
|
||||||
{exampleMessages.map((message, index) => (
|
onClick={() => setDefaultMessage(message.message)}
|
||||||
<Button
|
>
|
||||||
key={index}
|
<ArrowRight className="w-4 h-4 mr-2 text-muted-foreground" />
|
||||||
variant="link"
|
{message.heading}
|
||||||
className="p-0 h-auto"
|
</Button>
|
||||||
onClick={() => setDefaultMessage(message.message)}
|
))}
|
||||||
>
|
|
||||||
<ArrowRight className="w-4 h-4 mr-2 text-muted-foreground" />
|
|
||||||
{message.heading}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,16 @@ export async function Header() {
|
||||||
const chats = session?.user?.email ? await getChats(session.user.email) : []
|
const chats = session?.user?.email ? await getChats(session.user.email) : []
|
||||||
|
|
||||||
return (
|
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">
|
<header className="h-16 shrink-0 z-50 border-b bg-gradient-to-b from-background/10 via-background/50 backdrop-blur-xl to-background/80 px-4 flex sticky top-0 w-full items-center justify-between">
|
||||||
<div className="flex items-center w-2/5">
|
<div className="flex items-center">
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<Sidebar chats={chats} session={session} />
|
<Sidebar chats={chats} session={session} />
|
||||||
<Separator className="h-6 w-6 text-muted-foreground/50" />
|
<div className="hidden md:flex items-center">
|
||||||
<UserMenu session={session} />
|
<Separator className="h-6 w-6 text-muted-foreground/50" />
|
||||||
|
<UserMenu session={session} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2 items-center justify-end w-2/5">
|
<div className="flex space-x-2 items-center justify-end">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/vercel/nextjs-ai-chatbot/"
|
href="https://github.com/vercel/nextjs-ai-chatbot/"
|
||||||
|
|
@ -34,7 +36,8 @@ export async function Header() {
|
||||||
className={cn(buttonVariants())}
|
className={cn(buttonVariants())}
|
||||||
>
|
>
|
||||||
<Vercel className="w-4 h-4 mr-2" />
|
<Vercel className="w-4 h-4 mr-2" />
|
||||||
Deploy to Vercel
|
<span className="hidden sm:block">Deploy to Vercel</span>
|
||||||
|
<span className="sm:hidden">Vercel</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { CornerDownLeft, Plus, RefreshCcw, StopCircle } from 'lucide-react'
|
import { CornerDownLeft, Plus } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
|
||||||
import Textarea from 'react-textarea-autosize'
|
import Textarea from 'react-textarea-autosize'
|
||||||
|
|
||||||
import { Button, buttonVariants } from '@/components/ui/button'
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { fontMessage } from '@/lib/fonts'
|
|
||||||
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
|
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useChatStore } from '@/lib/hooks/use-chat-store'
|
import { useChatStore } from '@/lib/hooks/use-chat-store'
|
||||||
|
|
@ -20,19 +18,12 @@ import Link from 'next/link'
|
||||||
|
|
||||||
export interface PromptProps {
|
export interface PromptProps {
|
||||||
onSubmit: (value: string) => void
|
onSubmit: (value: string) => void
|
||||||
onRefresh?: () => void
|
|
||||||
onAbort?: () => void
|
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PromptForm({
|
export function PromptForm({ onSubmit, isLoading }: PromptProps) {
|
||||||
onSubmit,
|
|
||||||
onRefresh,
|
|
||||||
onAbort,
|
|
||||||
isLoading
|
|
||||||
}: PromptProps) {
|
|
||||||
const { defaultMessage } = useChatStore()
|
const { defaultMessage } = useChatStore()
|
||||||
const [input, setInput] = useState(defaultMessage)
|
const [input, setInput] = React.useState(defaultMessage)
|
||||||
const { formRef, onKeyDown } = useEnterSubmit()
|
const { formRef, onKeyDown } = useEnterSubmit()
|
||||||
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
|
|
@ -50,88 +41,59 @@ export function PromptForm({
|
||||||
<form
|
<form
|
||||||
onSubmit={async e => {
|
onSubmit={async e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (input === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
setInput('')
|
setInput('')
|
||||||
await onSubmit(input)
|
await onSubmit(input)
|
||||||
}}
|
}}
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
>
|
>
|
||||||
<div className="relative flex h-full flex-1 flex-row-reverse items-stretch md:flex-col">
|
<TooltipProvider>
|
||||||
<div>
|
<div className="relative flex w-full grow flex-col sm:rounded-md sm:border bg-background overflow-hidden px-12">
|
||||||
<div className="ml-1 flex h-full justify-center gap-0 md:m-auto md:mb-2 md:w-full md:gap-2">
|
<Tooltip>
|
||||||
{onRefresh ? (
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Link
|
||||||
variant="ghost"
|
href="/"
|
||||||
className="relative border-0 h-full md:h-auto px-3 md:border bg-white dark:bg-zinc-900 dark:border-zinc-800 dark:text-zinc-400"
|
className={cn(
|
||||||
onClick={onRefresh}
|
buttonVariants({ size: 'sm', variant: 'outline' }),
|
||||||
|
'w-8 h-8 p-0 rounded-full absolute top-4 left-4 bg-background'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-gull w-full items-center justify-center gap-2">
|
<Plus className="h-4 w-4" />
|
||||||
<RefreshCcw className="h-4 w-4 text-zinc-500 md:h-3 md:w-3" />
|
<span className="sr-only">New Chat</span>
|
||||||
<span className="hidden md:block">Regenerate response</span>
|
</Link>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</Button>
|
<TooltipContent>New Chat</TooltipContent>
|
||||||
) : null}
|
</Tooltip>
|
||||||
</div>
|
<Textarea
|
||||||
</div>
|
ref={inputRef}
|
||||||
<TooltipProvider>
|
tabIndex={0}
|
||||||
<div className="relative flex w-full grow flex-col rounded-md border bg-background overflow-hidden px-12">
|
onKeyDown={onKeyDown}
|
||||||
|
rows={1}
|
||||||
|
value={input}
|
||||||
|
onChange={e => setInput(e.target.value)}
|
||||||
|
placeholder="Send a message."
|
||||||
|
spellCheck={false}
|
||||||
|
className="min-h-[60px] sm:text-sm bg-transparent px-4 py-[1.4rem] focus-within:outline-none w-full resize-none"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link
|
<Button
|
||||||
href="/"
|
type="submit"
|
||||||
className={cn(
|
size="icon"
|
||||||
buttonVariants({ size: 'sm', variant: 'outline' }),
|
disabled={isLoading || input === ''}
|
||||||
'w-8 h-8 p-0 rounded-full absolute top-4 left-4 bg-background'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<CornerDownLeft className="h-4 w-4" />
|
||||||
<span className="sr-only">New Chat</span>
|
<span className="sr-only">Send message</span>
|
||||||
</Link>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>New Chat</TooltipContent>
|
<TooltipContent>Send message</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Textarea
|
|
||||||
ref={inputRef}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
rows={1}
|
|
||||||
value={input}
|
|
||||||
onChange={e => setInput(e.target.value)}
|
|
||||||
placeholder="Send a message."
|
|
||||||
spellCheck={false}
|
|
||||||
className={cn(
|
|
||||||
'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">
|
|
||||||
{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 type="submit" className="h-8 w-8 p-0">
|
|
||||||
<CornerDownLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Send message</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</div>
|
||||||
</div>
|
</TooltipProvider>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import { useTransition } from 'react'
|
||||||
import { Loader2Icon, MessageSquare, Trash2Icon } from 'lucide-react'
|
import { Loader2Icon, MessageSquare, Trash2Icon } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname, useRouter } from 'next/navigation'
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
import * as React from 'react'
|
|
||||||
import { useTransition } from 'react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { removeChat } from '@/app/actions'
|
import { removeChat } from '@/app/actions'
|
||||||
|
|
|
||||||
|
|
@ -26,21 +26,21 @@ export async function Sidebar({ session, chats }: SidebarProps) {
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="ghost" className="w-9 h-9 p-0">
|
<Button variant="ghost" className="w-9 h-9 p-0 -ml-2">
|
||||||
<SidebarIcon className="w-6 h-6" />
|
<SidebarIcon className="w-6 h-6" />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
position="left"
|
position="left"
|
||||||
className="w-[300px] gap-0 top-2 px-4 rounded-r-lg bottom-2 h-auto flex flex-col"
|
className="w-[300px] gap-0 top-0 bottom-0 lg:top-2 p-0 rounded-r-lg lg:bottom-2 h-auto flex flex-col"
|
||||||
>
|
>
|
||||||
<SheetHeader>
|
<SheetHeader className="px-4 py-4">
|
||||||
<SheetTitle>Chat History</SheetTitle>
|
<SheetTitle className="text-sm">Chat History</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="flex-1">
|
<div className="flex-1 overflow-auto">
|
||||||
{chats?.length ? (
|
{chats?.length ? (
|
||||||
<div className="space-y-2 -mx-2 py-4">
|
<div className="space-y-2 px-2">
|
||||||
{chats.map(chat => (
|
{chats.map(chat => (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
|
|
@ -59,7 +59,7 @@ export async function Sidebar({ session, chats }: SidebarProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center p-4">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
{session?.user && (
|
{session?.user && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'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',
|
'inline-flex items-center justify-center shadow 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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
default:
|
||||||
|
'bg-primary shadow-md text-primary-foreground hover:bg-primary/90',
|
||||||
destructive:
|
destructive:
|
||||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
outline:
|
outline:
|
||||||
|
|
@ -20,10 +21,10 @@ const buttonVariants = cva(
|
||||||
link: 'underline-offset-4 shadow-none hover:underline text-primary'
|
link: 'underline-offset-4 shadow-none hover:underline text-primary'
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-9 py-2 px-4',
|
default: 'h-8 py-2 px-4',
|
||||||
sm: 'h-9 px-3 rounded-md',
|
sm: 'h-8 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'
|
icon: 'h-8 w-8 p-0'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
|
||||||
|
|
@ -124,4 +124,4 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||||
})
|
})
|
||||||
CodeBlock.displayName = 'CodeBlock'
|
CodeBlock.displayName = 'CodeBlock'
|
||||||
|
|
||||||
export default CodeBlock
|
export { CodeBlock }
|
||||||
|
|
|
||||||
31
components/ui/separator.tsx
Normal file
31
components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
|
|
@ -32,5 +32,7 @@ export function UserMenu({ session }: UserMenuProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p className="text-sm font-medium">Logged in as {session.user.name}</p>
|
return (
|
||||||
|
<p className="text-sm font-medium px-2">Logged in as {session.user.name}</p>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
JetBrains_Mono as FontMono,
|
JetBrains_Mono as FontMono,
|
||||||
IBM_Plex_Sans as FontMessage,
|
Lora as FontMessage,
|
||||||
Inter as FontSans
|
Inter as FontSans
|
||||||
} from 'next/font/google'
|
} from 'next/font/google'
|
||||||
|
|
||||||
|
|
@ -11,7 +11,6 @@ export const fontSans = FontSans({
|
||||||
|
|
||||||
export const fontMessage = FontMessage({
|
export const fontMessage = FontMessage({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
weight: ['400', '500', '600'],
|
|
||||||
variable: '--font-message'
|
variable: '--font-message'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@vercel/analytics": "^1.0.0",
|
"@vercel/analytics": "^1.0.0",
|
||||||
|
|
|
||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
|
|
@ -23,6 +23,9 @@ dependencies:
|
||||||
'@radix-ui/react-dropdown-menu':
|
'@radix-ui/react-dropdown-menu':
|
||||||
specifier: ^2.0.4
|
specifier: ^2.0.4
|
||||||
version: 2.0.4(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
version: 2.0.4(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-separator':
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 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':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 1.0.2(@types/react@18.2.6)(react@18.2.0)
|
version: 1.0.2(@types/react@18.2.6)(react@18.2.0)
|
||||||
|
|
@ -996,6 +999,27 @@ packages:
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
|
||||||
|
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/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)
|
||||||
|
'@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-slot@1.0.1(react@18.2.0):
|
/@radix-ui/react-slot@1.0.1(react@18.2.0):
|
||||||
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==}
|
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue