feat: implement new ui (#9)

This commit is contained in:
Jared Palmer 2023-06-11 15:52:10 -04:00 committed by GitHub
commit 44f93d338e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 2655 additions and 1044 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
OPENAI_API_KEY=XXXXXXXX
NEXTAUTH_SECRET=XXXXXXXX

View file

@ -1,5 +1,25 @@
{
"$schema": "https://json.schemastore.org/eslintrc",
"root": true,
"extends": ["next/core-web-vitals", "prettier"]
"extends": [
"next/core-web-vitals",
"prettier",
"plugin:tailwindcss/recommended"
],
"plugins": ["tailwindcss"],
"rules": {
"tailwindcss/no-custom-classname": "off"
},
"settings": {
"tailwindcss": {
"callees": ["cn", "cva"],
"config": "tailwind.config.js"
}
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
}
]
}

1
.gitignore vendored
View file

@ -35,3 +35,4 @@ yarn-error.log*
.contentlayer
.env
.vercel
.vscode

View file

@ -1,4 +0,0 @@
{
"typescript.tsdk": "../../node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

View file

@ -1,3 +1,45 @@
# Next.js AI Chatbot
A an AI-powered Chatbot built with Next.js built by Vercel Labs.
A Next.js 13 and App Router-ready AI chatbot template featuring:
- [Next.js](https://nextjs.org) App Router
- React Server Components (RSCs), Suspense, and Server Actions
- [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
- Support for OpenAI (default), Anthropic, HuggingFace, or custom AI chat models and/or LangChain
- Edge runtime-ready
- [Shadcn UI](https://ui.shadcn.com)
- Styling with [Tailwind CSS](https://tailwindcss.com)
- [Radix UI](https://radix-ui.com) for headless component primitives
- Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv)
- [Next Auth](https://github.com/nextauthjs/next-auth) for authentication
## Model Providers
This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks to the [Vercel AI SDK](https://sdk.vercel.ai/docs), switching LLM providers to [Anthropic](https://anthropic.com), [HuggingFace](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code.
## Running locally
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts.
1. Install Vercel CLI: `npm i -g vercel`
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
3. Download your environment variables: `vercel env pull`
```bash
pnpm install
pnpm dev
```
Your app should now be running on [localhost:3000](http://localhost:3000/).
## Authors
This library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from:
- Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)
- Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)
- Shadcn ([@shadcn](https://twitter.com/shadcn)) - [Contractor](https://shadcn.com)
[Contributors](https://github.com/vercel-labs/ai-chatbot/graphs/contributors)

View file

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

View file

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

View file

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

View file

@ -1,46 +0,0 @@
'use client'
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'
export interface ChatList {
messages: Message[]
}
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="px-4 py-2 flex items-center justify-between">
<div className="flex flex-row gap-2 whitespace-nowrap items-center">
<NextChatLogo className="h-6 w-6" />
<span className="select-none">Next.js Chatbot</span>
</div>
<div>
<button className="text-white p-2 rounded hover:bg-zinc-800 transition duration-100">
<Plus className="h-4 w-4" />
</button>
</div>
</div>
</div>
<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>
) : (
<EmptyScreen />
)}
</div>
</div>
)
}

View file

@ -1,119 +0,0 @@
import CodeBlock from '@/components/ui/codeblock'
import { MemoizedReactMarkdown } from '@/components/markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import { cn } from '@/lib/utils'
import { fontMessage } from '@/lib/fonts'
import { Message } from 'ai-connector'
export interface ChatMessageProps {
message: Message
}
export function ChatMessage(props: ChatMessageProps) {
return (
<div
className={cn(
{
'bg-zinc-50': props.message.role === 'assistant'
},
'pr-0 lg:pr-[260px]',
fontMessage.className
)}
>
<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('`▍`', '▍')
}
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>
)
}
}}
>
{props.message.content}
</MemoizedReactMarkdown>
</div>
</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

@ -1,50 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
import { Prompt } from './prompt'
import { useChat, type Message } from 'ai-connector'
import { ChatList } from './chat-list'
export interface ChatProps {
// create?: (input: string) => Chat | undefined;
initialMessages?: Message[]
id?: string
}
export function Chat({
id,
// create,
initialMessages
}: ChatProps) {
const router = useRouter()
const { isLoading, messages, reload, append } = useChat({
initialMessages: initialMessages as any[],
id
// onCreate: (id: string) => {
// router.push(`/chat/${id}`);
// },
})
return (
<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>
<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>
)
}
Chat.displayName = 'Chat'

View file

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

View file

@ -1,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,17 +3,76 @@
@tailwind utilities;
@layer base {
#__next,
#root {
height: 100%;
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%;
--accent-foreground: ;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 240 5% 64.9%;
--radius: 0.5rem;
}
body,
html {
height: 100%;
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%;
--accent-foreground: ;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 240 3.7% 15.9%;
}
}
pre:has(div.codeblock) {
padding: 0;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -1,9 +1,11 @@
import './globals.css'
import { Metadata } from 'next'
import { ThemeProvider } from '@/components/theme-provider'
import '@/app/globals.css'
import { fontMono, fontSans } from '@/lib/fonts'
import { cn } from '@/lib/utils'
import { Header } from '@/components/header'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { ThemeProvider } from '@/components/theme-provider'
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="flex min-h-screen flex-col">
{/* @ts-ignore */}
<Header />
<main className="flex-1 bg-muted/50">{children}</main>
</div>
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
)
}

View file

@ -1,18 +1,8 @@
import { Chat } from './chat'
import { Sidebar } from './sidebar'
import { auth } from '@/auth'
import { Chat } from '@/components/chat'
export const runtime = 'edge'
// 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>
)
return <Chat />
}

View file

@ -1,114 +0,0 @@
'use client'
import { CornerDownLeft, RefreshCcw, StopCircle } from 'lucide-react'
import { useState } from 'react'
import Textarea from 'react-textarea-autosize'
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'
export interface PromptProps {
onSubmit: (value: string) => void
onRefresh?: () => void
onAbort?: () => void
isLoading: boolean
}
export function Prompt({
onSubmit,
onRefresh,
onAbort,
isLoading
}: PromptProps) {
const [input, setInput] = useState('')
const { formRef, onKeyDown } = useCmdEnterSubmit()
return (
<form
onSubmit={async e => {
e.preventDefault()
setInput('')
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>
<div className="ml-1 flex h-full justify-center gap-0 md:m-auto md:mb-2 md:w-full md:gap-2">
{onRefresh ? (
<Button
variant="ghost"
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"
onClick={onRefresh}
>
<div className="flex h-gull w-full items-center justify-center gap-2">
<RefreshCcw className="h-4 w-4 text-zinc-500 md:h-3 md:w-3" />
<span className="hidden md:block">Regenerate response</span>
</div>
</Button>
) : null}
{/* <button
id="share-button"
className="btn btn-neutral flex justify-center gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-3 w-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
/>
</svg>
Share
</button> */}
</div>
</div>
<div className="relative flex w-full grow flex-col rounded-md 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">
<Textarea
tabIndex={0}
onKeyDown={onKeyDown}
rows={1}
value={input}
onChange={e => setInput(e.target.value)}
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',
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>
</div>
</form>
)
}
Prompt.displayName = 'Prompt'

View file

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

View file

@ -1,117 +0,0 @@
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 { SidebarItem } from './sidebar-item'
export interface SidebarProps {
session?: Session
newChat?: boolean
}
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>
</div>
</div>
</div>
)
}
Sidebar.displayName = 'Sidebar'
async function SidebarList({ session }: { session?: Session }) {
const results: Chat[] = await getChats(session?.user?.email ?? '')
return results.map(c => (
<SidebarItem
key={c.id}
title={c.title}
userId={session?.user?.email ?? ''}
href={`/chat/${c.id}`}
id={c.id}
/>
))
}
async function getChats(userId: string) {
try {
const pipeline = kv.pipeline()
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1)
for (const chat of chats) {
pipeline.hgetall(chat)
}
const results = await pipeline.exec()
return results as Chat[]
} catch (error) {
return []
}
}

28
auth.ts
View file

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

View file

@ -0,0 +1,48 @@
'use client'
import * as React from 'react'
import { ArrowDown } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button, type ButtonProps } from '@/components/ui/button'
export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
const [isAtBottom, setIsAtBottom] = React.useState(false)
React.useEffect(() => {
const handleScroll = () => {
setIsAtBottom(
window.innerHeight + window.scrollY > document.body.offsetHeight - 100
)
}
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return (
<Button
variant="outline"
size="icon"
className={cn(
'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
isAtBottom ? 'opacity-0' : 'opacity-100',
className
)}
onClick={() =>
window.scrollTo({
top: document.body.offsetHeight,
behavior: 'smooth'
})
}
{...props}
>
<ArrowDown className="h-4 w-4" />
<span className="sr-only">Scroll to bottom</span>
</Button>
)
}

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

@ -0,0 +1,28 @@
'use client'
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) {
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 />
)}
</div>
)
}

View file

@ -0,0 +1,73 @@
import { Message } from 'ai-connector'
import { User } from 'lucide-react'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import { cn } from '@/lib/utils'
import { CodeBlock } from '@/components/ui/codeblock'
import { OpenAI } from '@/components/icons'
import { MemoizedReactMarkdown } from '@/components/markdown'
export interface ChatMessageProps {
message: Message
}
export function ChatMessage({ message, ...props }: ChatMessageProps) {
return (
<div className={cn('relative mb-4 flex items-start md:-ml-12')} {...props}>
<div
className={cn(
'flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
message.role === 'user'
? 'bg-background'
: 'bg-primary text-primary-foreground'
)}
>
{message.role === 'user' ? (
<User className="h-4 w-4" />
) : (
<OpenAI className="h-4 w-4" />
)}
</div>
<div className="ml-4 px-1">
<MemoizedReactMarkdown
className="prose leading-6 dark:prose-invert prose-p:leading-[1.8rem] prose-pre:rounded-md prose-pre:bg-[#282c34]"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
p({ children }) {
return <p className="mb-2 last:mb-0">{children}</p>
},
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('`▍`', '▍')
}
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>
)
}
}}
>
{message.content}
</MemoizedReactMarkdown>
</div>
</div>
)
}

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

@ -0,0 +1,75 @@
'use client'
import { UseChatHelpers } from 'ai-connector'
import { RefreshCcw, StopCircle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { ExternalLink } from '@/components/external-link'
import { PromptForm } from '@/components/prompt-form'
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
export interface ChatPanelProps
extends Pick<
UseChatHelpers,
'append' | 'isLoading' | 'reload' | 'messages' | 'stop'
> {
id?: string
}
export function ChatPanel({
isLoading,
stop,
append,
reload,
messages
}: ChatPanelProps) {
return (
<div className="fixed inset-x-0 bottom-0 bg-gradient-to-b from-muted/10 from-10% to-muted/30 to-50%">
<ButtonScrollToBottom />
<div className="mx-auto sm:max-w-2xl sm:px-4">
<div className="flex h-10 items-center justify-center">
{isLoading ? (
<Button
variant="outline"
onClick={() => stop()}
className="bg-background"
>
<StopCircle className="mr-2 h-4 w-4" />
Stop generating
</Button>
) : (
messages?.length > 0 && (
<Button
variant="outline"
onClick={() => reload()}
className="bg-background"
>
<RefreshCcw className="mr-2 h-4 w-4" />
Regenerate response
</Button>
)
)}
</div>
<div className="space-y-4 border-t bg-background px-4 py-2 shadow-lg sm:rounded-t-xl sm:border md:py-4">
<PromptForm
onSubmit={value => {
append({
content: value,
role: 'user'
})
}}
isLoading={isLoading}
/>
<p className="hidden px-2 text-center text-xs leading-normal text-muted-foreground sm:block">
Open source AI chatbot app built with{' '}
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
<ExternalLink href="https://vercel.com/storage/kv">
Vercel KV
</ExternalLink>
.
</p>
</div>
</div>
</div>
)
}

33
components/chat.tsx Normal file
View file

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

View file

@ -0,0 +1,57 @@
import { ArrowRight } from 'lucide-react'
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'
const exampleMessages = [
{
heading: 'Explain technical concepts',
message: `What is a "serverless function"?`
},
{
heading: 'Summarize an 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={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)}
>
<ArrowRight className="mr-2 h-4 w-4 text-muted-foreground" />
{message.heading}
</Button>
))}
</div>
</div>
)
}

View file

@ -9,7 +9,7 @@ export function ExternalLink({
<a
href={href}
target="_blank"
className="inline-flex gap-1 hover:underline flex-1 justify-center leading-4"
className="inline-flex flex-1 justify-center gap-1 leading-4 hover:underline"
>
<span>{children}</span>
<svg

50
components/header.tsx Normal file
View file

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

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

160
components/icons.tsx Normal file
View file

@ -0,0 +1,160 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
export function NextChat({
className,
inverted,
...props
}: React.ComponentProps<'svg'> & { inverted?: boolean }) {
const id = React.useId()
return (
<svg
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn('h-4 w-4', className)}
{...props}
>
<defs>
<linearGradient
id={`gradient-${id}-1`}
x1="10.6889"
y1="10.3556"
x2="13.8445"
y2="14.2667"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
<linearGradient
id={`gradient-${id}-2`}
x1="11.7555"
y1="4.8"
x2="11.7376"
y2="9.50002"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<path
d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
fill={inverted ? 'black' : 'white'}
stroke={inverted ? 'black' : 'white'}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<mask
id="mask0_91_2047"
style={{ maskType: 'alpha' }}
maskUnits="userSpaceOnUse"
x={1}
y={0}
width={16}
height={16}
>
<circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
</mask>
<g mask="url(#mask0_91_2047)">
<circle cx={9} cy={8} r={8} fill={inverted ? 'black' : 'white'} />
<path
d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
fill={`url(#gradient-${id}-1)`}
/>
<rect
x="11.2222"
y="4.8"
width="1.06667"
height="6.4"
fill={`url(#gradient-${id}-2)`}
/>
</g>
</svg>
)
}
export function OpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
fill="currentColor"
viewBox="0 0 24 24"
role="img"
xmlns="http://www.w3.org/2000/svg"
className={cn('h-4 w-4', className)}
{...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>
)
}
export function Vercel({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
aria-label="Vercel logomark"
role="img"
viewBox="0 0 74 64"
className={cn('h-4 w-4', className)}
{...props}
>
<path
d="M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z"
fill="currentColor"
></path>
</svg>
)
}
export function GitHub({ className, ...props }: React.ComponentProps<'svg'>) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn('h-4 w-4', className)}
{...props}
>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
)
}
export function Separator({
className,
...props
}: React.ComponentProps<'svg'>) {
return (
<svg
fill="none"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1"
viewBox="0 0 24 24"
aria-hidden="true"
className={cn('h-4 w-4', className)}
{...props}
>
<path d="M16.88 3.549L7.12 20.451"></path>
</svg>
)
}

View file

@ -0,0 +1,99 @@
'use client'
import * as React from 'react'
import Link from 'next/link'
import { CornerDownLeft, Plus } from 'lucide-react'
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'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip'
export interface PromptProps {
onSubmit: (value: string) => void
isLoading: boolean
}
export function PromptForm({ onSubmit, isLoading }: PromptProps) {
const { defaultMessage } = useChatStore()
const [input, setInput] = React.useState(defaultMessage)
const { formRef, onKeyDown } = useEnterSubmit()
const inputRef = React.useRef<HTMLTextAreaElement>(null)
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
React.useEffect(() => {
setInput(defaultMessage)
}, [defaultMessage])
return (
<form
onSubmit={async e => {
e.preventDefault()
if (input === '') {
return
}
setInput('')
await onSubmit(input)
}}
ref={formRef}
>
<TooltipProvider>
<div className="relative flex w-full grow flex-col overflow-hidden bg-background px-8 sm:rounded-md sm:border sm:px-12">
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/"
className={cn(
buttonVariants({ size: 'sm', variant: 'outline' }),
'absolute left-0 top-4 h-8 w-8 rounded-full bg-background p-0 sm:left-4'
)}
>
<Plus className="h-4 w-4" />
<span className="sr-only">New Chat</span>
</Link>
</TooltipTrigger>
<TooltipContent>New Chat</TooltipContent>
</Tooltip>
<Textarea
ref={inputRef}
tabIndex={0}
onKeyDown={onKeyDown}
rows={1}
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Send a message."
spellCheck={false}
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
/>
<div className="absolute right-0 top-4 sm:right-4">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
size="icon"
disabled={isLoading || input === ''}
>
<CornerDownLeft className="h-4 w-4" />
<span className="sr-only">Send message</span>
</Button>
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
</div>
</div>
</TooltipProvider>
</form>
)
}

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

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

View file

@ -0,0 +1,40 @@
import { getChats } from '@/app/actions'
import { Session } from '@auth/core/types'
import { SidebarItem } from './sidebar-item'
export interface SidebarListProps {
session?: Session
}
export async function SidebarList(props: SidebarListProps) {
const chats = await getChats(props.session?.user?.email)
return (
<div className="flex-1 overflow-auto">
{chats?.length ? (
<div className="px-2 space-y-2">
{chats.map(chat => (
<SidebarItem
key={chat.id}
title={chat.title}
userId={props?.session?.user?.email ?? ''}
href={`/chat/${chat.id}`}
id={chat.id}
/>
))}
</div>
) : (
<div className="p-8 text-center">
<p className="text-sm text-muted-foreground">
{props?.session?.user ? (
<>No chat history</>
) : (
<>Login for history</>
)}
</p>
</div>
)}
</div>
)
}
SidebarList.displayName = 'SidebarList'

55
components/sidebar.tsx Normal file
View file

@ -0,0 +1,55 @@
'use client'
import * as React from 'react'
import { signOut } from '@auth/nextjs/client'
import { type Session } from '@auth/nextjs/types'
import { Sidebar as SidebarIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger
} from '@/components/ui/sheet'
import { ThemeToggle } from '@/components/theme-toggle'
export interface SidebarProps {
session?: Session
children?: React.ReactNode
}
export function Sidebar({ session, children }: SidebarProps) {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" className="-ml-2 h-9 w-9 p-0">
<SidebarIcon className="h-6 w-6" />
<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"
>
<SheetHeader className="p-4">
<SheetTitle className="text-sm">Chat History</SheetTitle>
</SheetHeader>
{children}
<div className="flex items-center p-4">
<ThemeToggle />
{session?.user && (
<Button
variant="ghost"
onClick={() => signOut()}
className="ml-auto"
>
Logout
</Button>
)}
</div>
</SheetContent>
</Sheet>
)
}

View file

@ -0,0 +1,14 @@
export function TailwindIndicator() {
if (process.env.NODE_ENV === 'production') return null
return (
<div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden">sm</div>
<div className="hidden md:block lg:hidden">md</div>
<div className="hidden lg:block xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
)
}

View file

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

View file

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

View file

@ -1,28 +1,30 @@
import * as React from 'react'
import { VariantProps, cva } from 'class-variance-authority'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
default:
'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary'
ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 shadow-none hover:underline'
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md',
xs: 'h-6 px-3 rounded-md'
default: 'h-8 px-4 py-2',
sm: 'h-8 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-8 w-8 p-0'
}
},
defaultVariants: {
@ -34,12 +36,15 @@ const buttonVariants = cva(
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<button
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}

View file

@ -1,10 +1,12 @@
'use client'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-cliipboard'
import { Check, Clipboard, Download } from 'lucide-react'
import { FC, memo } from 'react'
import { Check, Clipboard, Download } from 'lucide-react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-cliipboard'
interface Props {
language: string
value: string
@ -124,4 +126,4 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
})
CodeBlock.displayName = 'CodeBlock'
export default CodeBlock
export { CodeBlock }

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

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

View file

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

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

@ -0,0 +1,25 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }

View file

@ -1,14 +0,0 @@
'use client'
import { signIn } from '@auth/nextjs/client'
export function Login() {
return (
<button
className="inline-flex w-full items-center justify-center rounded border border-zinc-800 bg-white h-8 px-4 -my-1.5 text-sm leading-6 tracking-tight text-zinc-900 transition-colors ease-in-out hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => signIn('github')}
>
<span className="font-medium">Login</span>
</button>
)
}

View file

@ -1,85 +0,0 @@
import { useId } from 'react'
export function NextChatLogo(
props: JSX.IntrinsicElements['svg'] & { inverted?: boolean }
) {
const id = useId()
return (
<svg
width={17}
height={17}
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<defs>
<linearGradient
id={`gradient-${id}-1`}
x1="10.6889"
y1="10.3556"
x2="13.8445"
y2="14.2667"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={props.inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={props.inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
<linearGradient
id={`gradient-${id}-2`}
x1="11.7555"
y1="4.8"
x2="11.7376"
y2="9.50002"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={props.inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={props.inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<path
d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
fill={props.inverted ? 'black' : 'white'}
stroke={props.inverted ? 'black' : 'white'}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<mask
id="mask0_91_2047"
style={{ maskType: 'alpha' }}
maskUnits="userSpaceOnUse"
x={1}
y={0}
width={16}
height={16}
>
<circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
</mask>
<g mask="url(#mask0_91_2047)">
<circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
<path
d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
fill={`url(#gradient-${id}-1)`}
/>
<rect
x="11.2222"
y="4.8"
width="1.06667"
height="6.4"
fill={`url(#gradient-${id}-2)`}
/>
</g>
</svg>
)
}
NextChatLogo.displayName = 'DevGPT-logo'

View 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 }

145
components/ui/sheet.tsx Normal file
View file

@ -0,0 +1,145 @@
'use client'
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = ({
className,
children,
...props
}: SheetPrimitive.DialogPortalProps) => (
<SheetPrimitive.Portal
className={cn('fixed inset-0 z-50 flex', className)}
{...props}
>
{children}
</SheetPrimitive.Portal>
)
SheetPortal.displayName = SheetPrimitive.Portal.displayName
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
className
)}
{...props}
ref={ref}
/>
))
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) => (
<SheetPortal>
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ position }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
)
SheetHeader.displayName = 'SheetHeader'
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
)
SheetFooter.displayName = 'SheetFooter'
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription
}

View file

@ -0,0 +1,24 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'
export { Textarea }

30
components/ui/tooltip.tsx Normal file
View file

@ -0,0 +1,30 @@
'use client'
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View file

@ -1,96 +0,0 @@
'use client'
import { ThemeToggle } from '@/components/theme-toggle'
import { signOut } from '@auth/nextjs/client'
import { type Session } from '@auth/nextjs/types'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import Image from 'next/image'
export interface UserMenuProps {
session: Session
}
export function UserMenu({ session }: UserMenuProps) {
return (
<div className="flex items-center justify-between">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="focus:outline-none">
{session.user?.image ? (
<Image
width={24}
height={24}
className="h-6 w-6 rounded-full select-none ring-zinc-100/10 ring-1 hover:opacity-80 transition-opacity duration-300"
src={session.user?.image ? `${session.user.image}&s=60` : ''}
alt={session.user.name ?? 'Avatar'}
/>
) : (
<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: 12 }}
>
{session.user?.name ? session.user?.name.slice(0, 2) : null}
</div>
</div>
)}
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="min-w-[200px] bg-white dark:bg-zinc-950 rounded-lg shadow-lg text-zinc-900 dark:text-zinc-400 overflow-hidden border border-transparent dark:border-zinc-700 focus:outline-none relative z-20 py-2"
sideOffset={8}
align="end"
>
<DropdownMenu.Item className="py-2 px-3 focus:outline-none">
<div className="text-xs font-medium">{session.user?.name}</div>
<div className="text-xs text-zinc-500">{session.user?.email}</div>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-[1px] bg-zinc-100 dark:bg-zinc-800" />
<DropdownMenu.Item className="py-2 px-3 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors duration-200 cursor-pointer text-xs focus:outline-none">
<ThemeToggle />
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-[1px] bg-zinc-100 dark:bg-zinc-800" />
<DropdownMenu.Item className="py-2 px-3 hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors duration-200 cursor-pointer text-xs focus:outline-none ">
<a
href="https://vercel.com"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-between w-full"
>
<span>Vercel Homepage</span>
<span>
<svg
fill="none"
height={16}
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width={16}
aria-hidden="true"
className="mr-1"
>
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
<path d="M15 3h6v6" />
<path d="M10 14L21 3" />
</svg>
</span>
</a>
</DropdownMenu.Item>
<DropdownMenu.Item
className="py-2 px-3 hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors duration-200 cursor-pointer text-xs focus:outline-none"
onClick={() => signOut()}
>
Log Out
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
)
}
UserMenu.displayName = 'UserMenu'

View file

@ -1,14 +0,0 @@
import { cn } from '@/lib/utils'
export const VercelLogo = ({ className }: { className?: string }) => (
<svg
height={22}
viewBox="0 0 235 203"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Vercel Logo"
className={cn(className, 'dark:fill-white fill-black')}
>
<path d="M117.082 0L234.164 202.794H0L117.082 0Z" fill="currentColor" />
</svg>
)

37
components/user-menu.tsx Normal file
View file

@ -0,0 +1,37 @@
'use client'
import * as React from 'react'
import { signIn } from '@auth/nextjs/client'
import { type Session } from '@auth/nextjs/types'
import { Loader2Icon } from 'lucide-react'
import { Button } from '@/components/ui/button'
export interface UserMenuProps {
session?: Session
}
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 && <Loader2Icon className="mr-2 h-4 w-4 animate-spin" />}
Login
</Button>
)
}
return (
<p className="px-2 text-sm font-medium">Logged in as {session.user.name}</p>
)
}

View file

@ -1,5 +1,5 @@
import type { NextRequest, NextFetchEvent } from 'next/server'
import type { NextApiRequest } from 'next'
import type { NextFetchEvent, NextRequest } from 'next/server'
export const initAnalytics = ({
request,

View file

@ -1,20 +1,10 @@
import {
JetBrains_Mono as FontMono,
IBM_Plex_Sans as FontMessage,
Inter as FontSans
} from 'next/font/google'
import { JetBrains_Mono as FontMono, Inter as FontSans } from 'next/font/google'
export const fontSans = FontSans({
subsets: ['latin'],
variable: '--font-sans'
})
export const fontMessage = FontMessage({
subsets: ['latin'],
weight: ['400', '500', '600'],
variable: '--font-message'
})
export const fontMono = FontMono({
subsets: ['latin'],
variable: '--font-mono'

View file

@ -0,0 +1,11 @@
import { create } from 'zustand'
interface UseChatStore {
defaultMessage: string
setDefaultMessage: (message: string) => void
}
export const useChatStore = create<UseChatStore>()(set => ({
defaultMessage: '',
setDefaultMessage: message => set({ defaultMessage: message })
}))

View file

@ -1,4 +1,5 @@
'use client'
import { useState } from 'react'
export interface useCopyToClipboardProps {

View file

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

View file

@ -1,4 +1,5 @@
'use client'
import { useEffect, useState } from 'react'
export default function useWindowSize() {

View file

@ -1 +1 @@
export { auth as middleware } from "./auth";
export { auth as middleware } from './auth'

View file

@ -16,7 +16,12 @@
"dependencies": {
"@auth/core": "0.0.0-manual.527fff6c",
"@auth/nextjs": "0.0.0-manual.030e8328",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.6",
"@vercel/analytics": "^1.0.0",
"@vercel/kv": "^0.2.1",
"ai-connector": "^0.0.5",
@ -26,7 +31,7 @@
"focus-trap-react": "^10.1.1",
"lucide-react": "0.216.0",
"nanoid": "^4.0.2",
"next": "13.4.5-canary.3",
"next": "13.4.5-canary.12",
"next-themes": "^0.2.1",
"openai-edge": "^0.5.1",
"react": "^18.2.0",
@ -36,7 +41,8 @@
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.4.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1"
"remark-math": "^5.1.1",
"zustand": "^4.3.8"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
@ -44,30 +50,24 @@
"@types/react": "^18.0.22",
"@types/react-dom": "^18.0.7",
"@types/react-syntax-highlighter": "^15.5.6",
"@typescript-eslint/parser": "^5.59.7",
"autoprefixer": "^10.4.13",
"dotenv": "^16.0.3",
"drizzle-kit": "^0.18.0",
"eslint": "^8.31.0",
"eslint-config-next": "13.4.5-canary.3",
"eslint-config-next": "13.4.5-canary.12",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-tailwindcss": "^3.12.0",
"postcss": "^8.4.21",
"prettier": "^2.7.1",
"tailwindcss": "^3.3.1",
"tailwind-merge": "^1.12.0",
"tailwindcss": "^3.3.1",
"tailwindcss-animate": "^1.0.5",
"typescript": "^4.9.3"
"typescript": "^5.1.3"
},
"pnpm": {
"overrides": {
"@auth/core": "0.0.0-manual.527fff6c"
}
},
"prettier": {
"tabWidth": 2,
"semi": false,
"useTabs": false,
"singleQuote": true,
"arrowParens": "avoid",
"trailingComma": "none"
}
}

738
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

34
prettier.config.cjs Normal file
View file

@ -0,0 +1,34 @@
/** @type {import('prettier').Config} */
module.exports = {
endOfLine: 'lf',
semi: false,
useTabs: false,
singleQuote: true,
arrowParens: 'avoid',
tabWidth: 2,
trailingComma: 'none',
importOrder: [
'^(react/(.*)$)|^(react$)',
'^(next/(.*)$)|^(next$)',
'<THIRD_PARTY_MODULES>',
'',
'^types$',
'^@/types/(.*)$',
'^@/config/(.*)$',
'^@/lib/(.*)$',
'^@/hooks/(.*)$',
'^@/components/ui/(.*)$',
'^@/components/(.*)$',
'^@/registry/(.*)$',
'^@/styles/(.*)$',
'^@/app/(.*)$',
'',
'^[./]'
],
importOrderSeparation: false,
importOrderSortSpecifiers: true,
importOrderBuiltinModulesToTop: true,
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
importOrderMergeDuplicateImports: true,
importOrderCombineTypeAndValueImports: true
}

View file

@ -1,36 +1,99 @@
const { fontFamily } = require("tailwindcss/defaultTheme");
const { fontFamily } = require('tailwindcss/defaultTheme')
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
darkMode: ['class'],
content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
theme: {
container: {
center: true,
padding: "2rem",
padding: '2rem',
screens: {
"2xl": "1400px",
},
'2xl': '1400px'
}
},
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
sans: ['var(--font-sans)', ...fontFamily.sans]
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
sans: ['var(--font-sans)', ...fontFamily.sans]
},
keyframes: {
"accordion-down": {
'accordion-down': {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
to: { height: 'var(--radix-accordion-content-height)' }
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 }
},
'slide-from-left': {
'0%': {
transform: 'translateX(-100%)'
},
'100%': {
transform: 'translateX(0)'
}
},
'slide-to-left': {
'0%': {
transform: 'translateX(0)'
},
'100%': {
transform: 'translateX(-100%)'
}
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
'slide-from-left':
'slide-from-left 0.4s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
'slide-to-left':
'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
};
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
}