Run prettier

This commit is contained in:
Jared Palmer 2023-06-02 15:33:48 -04:00
parent aa83a871dd
commit 417f69e0f1
34 changed files with 530 additions and 523 deletions

View file

@ -1,27 +1,27 @@
"use server"; 'use server'
import { kv } from "@vercel/kv"; import { kv } from '@vercel/kv'
import { revalidatePath } from "next/cache"; import { revalidatePath } from 'next/cache'
export async function removeChat({ export async function removeChat({
id, id,
path, path,
userId, userId
}: { }: {
id: string; id: string
userId: string; userId: string
path: string; path: string
}) { }) {
// @todo next-auth@v5 doesn't work in server actions yet // @todo next-auth@v5 doesn't work in server actions yet
// const session = await auth(); // const session = await auth();
const uid = await kv.hget<string>(`chat:${id}`, "userId"); const uid = await kv.hget<string>(`chat:${id}`, 'userId')
if (uid !== userId) { if (uid !== userId) {
throw new Error("Unauthorized"); throw new Error('Unauthorized')
} }
await kv.del(`chat:${id}`); await kv.del(`chat:${id}`)
await kv.zrem(`user:chat:${userId}`, `chat:${id}`); await kv.zrem(`user:chat:${userId}`, `chat:${id}`)
revalidatePath("/"); revalidatePath('/')
revalidatePath("/chat/[id]"); revalidatePath('/chat/[id]')
} }

View file

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

View file

@ -1,50 +1,50 @@
import { auth } from "@/auth"; import { auth } from '@/auth'
import { kv } from "@vercel/kv"; import { kv } from '@vercel/kv'
import { nanoid } from "@/lib/utils"; import { nanoid } from '@/lib/utils'
import { OpenAIStream, StreamingTextResponse } from "ai-connector"; import { OpenAIStream, StreamingTextResponse } from 'ai-connector'
import { Configuration, OpenAIApi } from "openai-edge"; import { Configuration, OpenAIApi } from 'openai-edge'
export const runtime = "edge"; export const runtime = 'edge'
const configuration = new Configuration({ const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY, apiKey: process.env.OPENAI_API_KEY
}); })
const openai = new OpenAIApi(configuration); const openai = new OpenAIApi(configuration)
if (!process.env.OPENAI_API_KEY) { if (!process.env.OPENAI_API_KEY) {
throw new Error("Missing env var from OpenAI"); throw new Error('Missing env var from OpenAI')
} }
export const POST = auth(async function POST(req: Request) { export const POST = auth(async function POST(req: Request) {
const json = await req.json(); const json = await req.json()
// @ts-ignore // @ts-ignore
console.log(req.auth); // todo fix types console.log(req.auth) // todo fix types
const messages = json.messages.map((m: any) => ({ const messages = json.messages.map((m: any) => ({
content: m.content, content: m.content,
role: m.role, role: m.role
})); }))
const res = await openai.createChatCompletion({ const res = await openai.createChatCompletion({
model: "gpt-3.5-turbo", model: 'gpt-3.5-turbo',
messages, messages,
temperature: 0.7, temperature: 0.7,
top_p: 1, top_p: 1,
frequency_penalty: 1, frequency_penalty: 1,
max_tokens: 500, max_tokens: 500,
n: 1, n: 1,
stream: true, stream: true
}); })
const stream = await OpenAIStream(res, { const stream = await OpenAIStream(res, {
async onCompletion(completion) { async onCompletion(completion) {
// @ts-ignore // @ts-ignore
if (req.auth?.user?.email == null) { if (req.auth?.user?.email == null) {
return; return
} }
const title = json.messages[0].content.substring(0, 20); const title = json.messages[0].content.substring(0, 20)
const userId = (req as any).auth?.user?.email; const userId = (req as any).auth?.user?.email
const id = json.id ?? nanoid(); const id = json.id ?? nanoid()
const createdAt = Date.now(); const createdAt = Date.now()
const payload = { const payload = {
id, id,
title, title,
@ -54,17 +54,17 @@ export const POST = auth(async function POST(req: Request) {
...messages, ...messages,
{ {
content: completion, content: completion,
role: "assistant", role: 'assistant'
}, }
], ]
}; }
await kv.hmset(`chat:${id}`, payload); await kv.hmset(`chat:${id}`, payload)
await kv.zadd(`user:chat:${userId}`, { await kv.zadd(`user:chat:${userId}`, {
score: createdAt, score: createdAt,
member: `chat:${id}`, member: `chat:${id}`
}); })
}, }
}); })
return new StreamingTextResponse(stream); return new StreamingTextResponse(stream)
}); })

View file

@ -1,14 +1,14 @@
"use client"; 'use client'
import { type Message } from "ai-connector"; import { type Message } from 'ai-connector'
import { ChatMessage } from "./chat-message"; import { ChatMessage } from './chat-message'
import { NextChatLogo } from "@/components/ui/nextchat-logo"; import { NextChatLogo } from '@/components/ui/nextchat-logo'
import { Plus } from "lucide-react"; import { Plus } from 'lucide-react'
import { EmptyScreen } from "./empty"; import { EmptyScreen } from './empty'
export interface ChatList { export interface ChatList {
messages: Message[]; messages: Message[]
} }
export function ChatList({ messages }: ChatList) { export function ChatList({ messages }: ChatList) {
@ -30,7 +30,7 @@ export function ChatList({ messages }: ChatList) {
<div className="h-full w-full overflow-auto"> <div className="h-full w-full overflow-auto">
{messages.length > 0 ? ( {messages.length > 0 ? (
<div className="group w-full text-zinc-900 dark:text-white divide-y dark:divide-zinc-800"> <div className="group w-full text-zinc-900 dark:text-white divide-y dark:divide-zinc-800">
{messages.map((message) => ( {messages.map(message => (
<ChatMessage <ChatMessage
key={message.id || message.content} key={message.id || message.content}
message={message} message={message}
@ -42,5 +42,5 @@ export function ChatList({ messages }: ChatList) {
)} )}
</div> </div>
</div> </div>
); )
} }

View file

@ -1,12 +1,12 @@
import CodeBlock from "@/components/ui/codeblock"; import CodeBlock from '@/components/ui/codeblock'
import { MemoizedReactMarkdown } from "@/components/markdown"; import { MemoizedReactMarkdown } from '@/components/markdown'
import remarkGfm from "remark-gfm"; import remarkGfm from 'remark-gfm'
import remarkMath from "remark-math"; 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 { Message } from "ai-connector"; import { Message } from 'ai-connector'
export interface ChatMessageProps { export interface ChatMessageProps {
message: Message; message: Message
} }
export function ChatMessage(props: ChatMessageProps) { export function ChatMessage(props: ChatMessageProps) {
@ -14,15 +14,15 @@ export function ChatMessage(props: ChatMessageProps) {
<div <div
className={cn( className={cn(
{ {
"bg-zinc-50": props.message.role === "assistant", 'bg-zinc-50': props.message.role === 'assistant'
}, },
"pr-0 lg:pr-[260px]", 'pr-0 lg:pr-[260px]',
fontMessage.className 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="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"> <div className="flex w-full gap-4 items-start">
{props.message.role === "user" ? ( {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="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 <div
className="font-medium uppercase text-zinc-100" className="font-medium uppercase text-zinc-100"
@ -42,53 +42,53 @@ export function ChatMessage(props: ChatMessageProps) {
components={{ components={{
code({ node, inline, className, children, ...props }) { code({ node, inline, className, children, ...props }) {
if (children.length) { if (children.length) {
if (children[0] == "▍") { if (children[0] == '▍') {
return ( return (
<span className="mt-1 animate-pulse cursor-default"> <span className="mt-1 animate-pulse cursor-default">
</span> </span>
); )
} }
children[0] = (children[0] as string).replace("`▍`", "▍"); children[0] = (children[0] as string).replace('`▍`', '▍')
} }
const match = /language-(\w+)/.exec(className || ""); const match = /language-(\w+)/.exec(className || '')
return !inline ? ( return !inline ? (
<CodeBlock <CodeBlock
key={Math.random()} key={Math.random()}
language={(match && match[1]) || ""} language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, "")} value={String(children).replace(/\n$/, '')}
{...props} {...props}
/> />
) : ( ) : (
<code className={className} {...props}> <code className={className} {...props}>
{children} {children}
</code> </code>
); )
}, },
table({ children }) { table({ children }) {
return ( return (
<table className="border-collapse border border-black px-3 py-1 "> <table className="border-collapse border border-black px-3 py-1 ">
{children} {children}
</table> </table>
); )
}, },
th({ children }) { th({ children }) {
return ( return (
<th className="break-words border border-black bg-gray-500 px-3 py-1 text-white "> <th className="break-words border border-black bg-gray-500 px-3 py-1 text-white ">
{children} {children}
</th> </th>
); )
}, },
td({ children }) { td({ children }) {
return ( return (
<td className="break-words border border-black px-3 py-1"> <td className="break-words border border-black px-3 py-1">
{children} {children}
</td> </td>
); )
}, }
}} }}
> >
{props.message.content} {props.message.content}
@ -96,12 +96,12 @@ export function ChatMessage(props: ChatMessageProps) {
</div> </div>
</div> </div>
</div> </div>
); )
} }
ChatMessage.displayName = "ChatMessage"; ChatMessage.displayName = 'ChatMessage'
export function IconOpenAI(props: JSX.IntrinsicElements["svg"]) { export function IconOpenAI(props: JSX.IntrinsicElements['svg']) {
return ( return (
<svg <svg
fill="#000000" fill="#000000"
@ -115,5 +115,5 @@ export function IconOpenAI(props: JSX.IntrinsicElements["svg"]) {
<title>OpenAI icon</title> <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" /> <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> </svg>
); )
} }

View file

@ -1,30 +1,30 @@
"use client"; 'use client'
import { useRouter } from "next/navigation"; import { useRouter } from 'next/navigation'
import { Prompt } from "./prompt"; import { Prompt } from './prompt'
import { useChat, type Message } from "ai-connector"; import { useChat, type Message } from 'ai-connector'
import { ChatList } from "./chat-list"; import { ChatList } from './chat-list'
export interface ChatProps { export interface ChatProps {
// create?: (input: string) => Chat | undefined; // create?: (input: string) => Chat | undefined;
initialMessages?: Message[]; initialMessages?: Message[]
id?: string; id?: string
} }
export function Chat({ export function Chat({
id, id,
// create, // create,
initialMessages, initialMessages
}: ChatProps) { }: ChatProps) {
const router = useRouter(); const router = useRouter()
const { isLoading, messages, reload, append } = useChat({ const { isLoading, messages, reload, append } = useChat({
initialMessages: initialMessages as any[], initialMessages: initialMessages as any[],
id, id
// onCreate: (id: string) => { // onCreate: (id: string) => {
// router.push(`/chat/${id}`); // router.push(`/chat/${id}`);
// }, // },
}); })
return ( return (
<main className="transition-width relative min-h-full w-full flex-1 overflow-y-auto flex flex-col"> <main className="transition-width relative min-h-full w-full flex-1 overflow-y-auto flex flex-col">
@ -33,18 +33,18 @@ export function Chat({
</div> </div>
<div className="sticky light-gradient dark:bg-gradient-to-b dark:from-zinc-900 dark:to-zinc-950 bottom-0 left-0 w-full border-t bg-white dark:bg-black md:border-t-0 py-4 md:border-transparent md:!bg-transparent md:dark:border-transparent pr-0 lg:pr-[260px] flex dark:border-transparent items-center justify-center"> <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 <Prompt
onSubmit={(value) => { onSubmit={value => {
append({ append({
content: value, content: value,
role: "user", role: 'user'
}); })
}} }}
onRefresh={messages.length ? reload : undefined} onRefresh={messages.length ? reload : undefined}
isLoading={isLoading} isLoading={isLoading}
/> />
</div> </div>
</main> </main>
); )
} }
Chat.displayName = "Chat"; Chat.displayName = 'Chat'

View file

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

View file

@ -1,12 +1,12 @@
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
import { ExternalLink } from "./external-link"; import { ExternalLink } from './external-link'
import { fontMessage } from "@/lib/fonts"; import { fontMessage } from '@/lib/fonts'
function ExampleBubble({ children }: { children?: React.ReactNode }) { function ExampleBubble({ children }: { children?: React.ReactNode }) {
return ( return (
<div <div
className={cn( 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", '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 fontMessage.className
)} )}
style={{}} style={{}}
@ -15,19 +15,19 @@ function ExampleBubble({ children }: { children?: React.ReactNode }) {
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" 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={{ style={{
clipPath: clipPath:
'path("M31.1838 0C24.9593 10.7604 13.3251 18 0 18C9.94113 18 18 9.94113 18 0H31.1838Z")', 'path("M31.1838 0C24.9593 10.7604 13.3251 18 0 18C9.94113 18 18 9.94113 18 0H31.1838Z")'
}} }}
></div> ></div>
<p>{children}</p> <p>{children}</p>
</div> </div>
); )
} }
export function EmptyScreen() { export function EmptyScreen() {
return ( return (
<div <div
className={cn( className={cn(
"w-full h-full flex items-center justify-center pt-4 pb-8 pr-0 lg:pr-[260px]" '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="p-8 rounded-lg flex flex-col items-center justify-center gap-8 max-w-2xl">
@ -35,8 +35,8 @@ export function EmptyScreen() {
<div className="text-zinc-500 font-medium"> <div className="text-zinc-500 font-medium">
<p>Welcome to Next.js Chatbot!</p> <p>Welcome to Next.js Chatbot!</p>
<p className="mt-2"> <p className="mt-2">
This is an open source AI chatbot app built with{" "} This is an 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/postgres"> <ExternalLink href="https://vercel.com/storage/postgres">
Vercel Postgres Vercel Postgres
</ExternalLink> </ExternalLink>
@ -52,5 +52,5 @@ export function EmptyScreen() {
</div> </div>
</div> </div>
</div> </div>
); )
} }

View file

@ -1,9 +1,9 @@
export function ExternalLink({ export function ExternalLink({
href, href,
children, children
}: { }: {
href: string; href: string
children: React.ReactNode; children: React.ReactNode
}) { }) {
return ( return (
<a <a
@ -25,5 +25,5 @@ export function ExternalLink({
></path> ></path>
</svg> </svg>
</a> </a>
); )
} }

View file

@ -1,29 +1,29 @@
import "./globals.css"; import './globals.css'
import { Metadata } from "next"; import { Metadata } from 'next'
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from '@/components/theme-provider'
import { fontMono, fontSans } from "@/lib/fonts"; import { fontMono, fontSans } from '@/lib/fonts'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
default: "Next.js Chatbot", default: 'Next.js Chatbot',
template: `%s - Next.js Chatbot`, template: `%s - Next.js Chatbot`
}, },
description: "An AI-powered chatbot built with Next.js and Vercel.", description: 'An AI-powered chatbot built with Next.js and Vercel.',
themeColor: [ themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" }, { media: '(prefers-color-scheme: light)', color: 'white' },
{ media: "(prefers-color-scheme: dark)", color: "black" }, { media: '(prefers-color-scheme: dark)', color: 'black' }
], ],
icons: { icons: {
icon: "/favicon.ico", icon: '/favicon.ico',
shortcut: "/favicon-16x16.png", shortcut: '/favicon-16x16.png',
apple: "/apple-touch-icon.png", apple: '/apple-touch-icon.png'
}, }
}; }
interface RootLayoutProps { interface RootLayoutProps {
children: React.ReactNode; children: React.ReactNode
} }
export default function RootLayout({ children }: RootLayoutProps) { export default function RootLayout({ children }: RootLayoutProps) {
@ -33,7 +33,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
<head /> <head />
<body <body
className={cn( className={cn(
"font-sans antialiased", 'font-sans antialiased',
fontSans.variable, fontSans.variable,
fontMono.variable fontMono.variable
)} )}
@ -45,5 +45,5 @@ export default function RootLayout({ children }: RootLayoutProps) {
</body> </body>
</html> </html>
</> </>
); )
} }

View file

@ -1,12 +1,12 @@
import { Chat } from "./chat"; import { Chat } from './chat'
import { Sidebar } from "./sidebar"; import { Sidebar } from './sidebar'
import { auth } from "@/auth"; import { auth } from '@/auth'
export const runtime = "edge"; export const runtime = 'edge'
export const preferredRegion = "home"; export const preferredRegion = 'home'
export default async function IndexPage() { export default async function IndexPage() {
const session = await auth(); const session = await auth()
return ( return (
<div className="relative flex h-full w-full overflow-hidden"> <div className="relative flex h-full w-full overflow-hidden">
<Sidebar session={session} newChat /> <Sidebar session={session} newChat />
@ -14,5 +14,5 @@ export default async function IndexPage() {
<Chat /> <Chat />
</div> </div>
</div> </div>
); )
} }

View file

@ -1,35 +1,35 @@
"use client"; 'use client'
import { CornerDownLeft, RefreshCcw, StopCircle } from "lucide-react"; import { CornerDownLeft, RefreshCcw, StopCircle } from 'lucide-react'
import { useState } from "react"; import { useState } from 'react'
import Textarea from "react-textarea-autosize"; import Textarea from 'react-textarea-autosize'
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button'
import { fontMessage } from "@/lib/fonts"; import { fontMessage } from '@/lib/fonts'
import { useCmdEnterSubmit } from "@/lib/hooks/use-command-enter-submit"; import { useCmdEnterSubmit } from '@/lib/hooks/use-command-enter-submit'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
export interface PromptProps { export interface PromptProps {
onSubmit: (value: string) => void; onSubmit: (value: string) => void
onRefresh?: () => void; onRefresh?: () => void
onAbort?: () => void; onAbort?: () => void
isLoading: boolean; isLoading: boolean
} }
export function Prompt({ export function Prompt({
onSubmit, onSubmit,
onRefresh, onRefresh,
onAbort, onAbort,
isLoading, isLoading
}: PromptProps) { }: PromptProps) {
const [input, setInput] = useState(""); const [input, setInput] = useState('')
const { formRef, onKeyDown } = useCmdEnterSubmit(); const { formRef, onKeyDown } = useCmdEnterSubmit()
return ( return (
<form <form
onSubmit={async (e) => { onSubmit={async e => {
e.preventDefault(); e.preventDefault()
setInput(""); setInput('')
await onSubmit(input); await onSubmit(input)
}} }}
ref={formRef} 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" 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"
@ -77,16 +77,16 @@ export function Prompt({
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
rows={1} rows={1}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={e => setInput(e.target.value)}
placeholder="Send a message." placeholder="Send a message."
spellCheck={false} spellCheck={false}
className={cn( 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", '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 fontMessage.className
)} )}
style={{ style={{
height: 46, height: 46,
overflowY: "hidden", overflowY: 'hidden'
}} }}
/> />
{isLoading ? ( {isLoading ? (
@ -108,7 +108,7 @@ export function Prompt({
</div> </div>
</div> </div>
</form> </form>
); )
} }
Prompt.displayName = "Prompt"; Prompt.displayName = 'Prompt'

View file

@ -1,41 +1,41 @@
"use client"; 'use client'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
import Link from "next/link"; import Link from 'next/link'
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from 'next/navigation'
import { MessageCircleIcon, Trash2Icon, Loader2Icon } from "lucide-react"; import { MessageCircleIcon, Trash2Icon, Loader2Icon } from 'lucide-react'
import { removeChat } from "./actions"; import { removeChat } from './actions'
import { useTransition } from "react"; import { useTransition } from 'react'
export function SidebarItem({ export function SidebarItem({
title, title,
href, href,
id, id,
userId, userId
}: { }: {
title: string; title: string
href: string; href: string
id: string; id: string
userId: string; userId: string
}) { }) {
const pathname = usePathname(); const pathname = usePathname()
const router = useRouter(); const router = useRouter()
const active = pathname === href; const active = pathname === href
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition()
if (!id) return null; if (!id) return null
return ( return (
<Link <Link
href={href} href={href}
className={cn( 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", '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 isPending
? "text-zinc-400 dark:text-zinc-500" ? 'text-zinc-400 dark:text-zinc-500'
: "text-zinc-800 dark:text-zinc-400", : 'text-zinc-800 dark:text-zinc-400',
active active
? "bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white" ? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
: "bg-transparent" : 'bg-transparent'
)} )}
> >
<MessageCircleIcon className="h-4 w-4" /> <MessageCircleIcon className="h-4 w-4" />
@ -48,13 +48,13 @@ export function SidebarItem({
<button <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" 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} disabled={isPending}
onClick={(e) => { onClick={e => {
e.preventDefault(); e.preventDefault()
startTransition(() => { startTransition(() => {
removeChat({ id, userId, path: href }).then(() => { removeChat({ id, userId, path: href }).then(() => {
router.push("/"); router.push('/')
}); })
}); })
}} }}
> >
{isPending ? ( {isPending ? (
@ -64,5 +64,5 @@ export function SidebarItem({
)} )}
</button> </button>
</Link> </Link>
); )
} }

View file

@ -1,18 +1,18 @@
import { Login } from "@/components/ui/login"; import { Login } from '@/components/ui/login'
import { NextChatLogo } from "@/components/ui/nextchat-logo"; import { NextChatLogo } from '@/components/ui/nextchat-logo'
import { UserMenu } from "@/components/ui/user-menu"; import { UserMenu } from '@/components/ui/user-menu'
import { kv } from "@vercel/kv"; import { kv } from '@vercel/kv'
import { Chat } from "@/lib/types"; import { Chat } from '@/lib/types'
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
import { type Session } from "@auth/nextjs/types"; import { type Session } from '@auth/nextjs/types'
import { Plus } from "lucide-react"; import { Plus } from 'lucide-react'
import Link from "next/link"; import Link from 'next/link'
import { ExternalLink } from "./external-link"; import { ExternalLink } from './external-link'
import { SidebarItem } from "./sidebar-item"; import { SidebarItem } from './sidebar-item'
export interface SidebarProps { export interface SidebarProps {
session?: Session; session?: Session
newChat?: boolean; newChat?: boolean
} }
export function Sidebar({ session, newChat }: SidebarProps) { export function Sidebar({ session, newChat }: SidebarProps) {
@ -33,10 +33,10 @@ export function Sidebar({ session, newChat }: SidebarProps) {
<Link <Link
href="/" href="/"
className={cn( 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", '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 newChat
? "bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white" ? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
: "hover:bg-zinc-500/10" : 'hover:bg-zinc-500/10'
)} )}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
@ -80,38 +80,38 @@ export function Sidebar({ session, newChat }: SidebarProps) {
</div> </div>
</div> </div>
</div> </div>
); )
} }
Sidebar.displayName = "Sidebar"; Sidebar.displayName = 'Sidebar'
async function SidebarList({ session }: { session?: Session }) { async function SidebarList({ session }: { session?: Session }) {
const results: Chat[] = await getChats(session?.user?.email ?? ""); const results: Chat[] = await getChats(session?.user?.email ?? '')
return results.map((c) => ( return results.map(c => (
<SidebarItem <SidebarItem
key={c.id} key={c.id}
title={c.title} title={c.title}
userId={session?.user?.email ?? ""} userId={session?.user?.email ?? ''}
href={`/chat/${c.id}`} href={`/chat/${c.id}`}
id={c.id} id={c.id}
/> />
)); ))
} }
async function getChats(userId: string) { async function getChats(userId: string) {
try { try {
const pipeline = kv.pipeline(); const pipeline = kv.pipeline()
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1); const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1)
for (const chat of chats) { for (const chat of chats) {
pipeline.hgetall(chat); pipeline.hgetall(chat)
} }
const results = await pipeline.exec(); const results = await pipeline.exec()
return results as Chat[]; return results as Chat[]
} catch (error) { } catch (error) {
return []; return []
} }
} }

View file

@ -1,9 +1,9 @@
import { FC, memo } from "react"; import { FC, memo } from 'react'
import ReactMarkdown, { Options } from "react-markdown"; import ReactMarkdown, { Options } from 'react-markdown'
export const MemoizedReactMarkdown: FC<Options> = memo( export const MemoizedReactMarkdown: FC<Options> = memo(
ReactMarkdown, ReactMarkdown,
(prevProps, nextProps) => (prevProps, nextProps) =>
prevProps.children === nextProps.children && prevProps.children === nextProps.children &&
prevProps.className === nextProps.className prevProps.className === nextProps.className
); )

View file

@ -1,9 +1,9 @@
"use client"; 'use client'
import * as React from "react"; import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from "next-themes/dist/types"; import { ThemeProviderProps } from 'next-themes/dist/types'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>; return <NextThemesProvider {...props}>{children}</NextThemesProvider>
} }

View file

@ -1,14 +1,14 @@
"use client"; 'use client'
import { useTheme } from "next-themes"; import { useTheme } from 'next-themes'
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button'
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from 'lucide-react'
import { useTransition } from "react"; import { useTransition } from 'react'
export function ThemeToggle() { export function ThemeToggle() {
const { setTheme, theme } = useTheme(); const { setTheme, theme } = useTheme()
const [_, startTransition] = useTransition(); const [_, startTransition] = useTransition()
return ( return (
<Button <Button
variant="ghost" variant="ghost"
@ -16,18 +16,18 @@ export function ThemeToggle() {
className="p-0 leading-4 h-4 font-normal w-full" className="p-0 leading-4 h-4 font-normal w-full"
onClick={() => { onClick={() => {
startTransition(() => { startTransition(() => {
setTheme(theme === "light" ? "dark" : "light"); 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="flex flex-row justify-between text-xs text-zinc-900 dark:text-zinc-400 w-full">
<span className="">Toggle theme</span> <span className="">Toggle theme</span>
{!theme ? null : theme === "dark" ? ( {!theme ? null : theme === 'dark' ? (
<Moon className="h-4 transition-all" /> <Moon className="h-4 transition-all" />
) : ( ) : (
<Sun className="h-4 transition-all" /> <Sun className="h-4 transition-all" />
)} )}
</span> </span>
</Button> </Button>
); )
} }

View file

@ -1,36 +1,36 @@
import * as React from "react"; import * as React from 'react'
import { VariantProps, cva } from "class-variance-authority"; import { VariantProps, cva } 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 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 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 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:
"border border-input hover:bg-accent hover:text-accent-foreground", 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: 'hover:bg-accent hover:text-accent-foreground',
link: "underline-offset-4 hover:underline text-primary", link: 'underline-offset-4 hover:underline text-primary'
}, },
size: { size: {
default: "h-10 py-2 px-4", default: 'h-10 py-2 px-4',
sm: "h-9 px-3 rounded-md", sm: 'h-9 px-3 rounded-md',
lg: "h-11 px-8 rounded-md", lg: 'h-11 px-8 rounded-md',
xs: "h-6 px-3 rounded-md", xs: 'h-6 px-3 rounded-md'
}, }
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default'
},
} }
); }
)
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
@ -44,9 +44,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
ref={ref} ref={ref}
{...props} {...props}
/> />
); )
} }
); )
Button.displayName = "Button"; Button.displayName = 'Button'
export { Button, buttonVariants }; export { Button, buttonVariants }

View file

@ -1,83 +1,83 @@
"use client"; 'use client'
import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-cliipboard"; import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-cliipboard'
import { Check, Clipboard, Download } from "lucide-react"; import { Check, Clipboard, Download } from 'lucide-react'
import { FC, memo } from "react"; import { FC, memo } from 'react'
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
interface Props { interface Props {
language: string; language: string
value: string; value: string
} }
interface languageMap { interface languageMap {
[key: string]: string | undefined; [key: string]: string | undefined
} }
export const programmingLanguages: languageMap = { export const programmingLanguages: languageMap = {
javascript: ".js", javascript: '.js',
python: ".py", python: '.py',
java: ".java", java: '.java',
c: ".c", c: '.c',
cpp: ".cpp", cpp: '.cpp',
"c++": ".cpp", 'c++': '.cpp',
"c#": ".cs", 'c#': '.cs',
ruby: ".rb", ruby: '.rb',
php: ".php", php: '.php',
swift: ".swift", swift: '.swift',
"objective-c": ".m", 'objective-c': '.m',
kotlin: ".kt", kotlin: '.kt',
typescript: ".ts", typescript: '.ts',
go: ".go", go: '.go',
perl: ".pl", perl: '.pl',
rust: ".rs", rust: '.rs',
scala: ".scala", scala: '.scala',
haskell: ".hs", haskell: '.hs',
lua: ".lua", lua: '.lua',
shell: ".sh", shell: '.sh',
sql: ".sql", sql: '.sql',
html: ".html", html: '.html',
css: ".css", css: '.css'
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
}; }
export const generateRandomString = (length: number, lowercase = false) => { export const generateRandomString = (length: number, lowercase = false) => {
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0 const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
let result = ""; let result = ''
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length)); result += chars.charAt(Math.floor(Math.random() * chars.length))
} }
return lowercase ? result.toLowerCase() : result; return lowercase ? result.toLowerCase() : result
}; }
const CodeBlock: FC<Props> = memo(({ language, value }) => { const CodeBlock: FC<Props> = memo(({ language, value }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
const downloadAsFile = () => { const downloadAsFile = () => {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return
} }
const fileExtension = programmingLanguages[language] || ".file"; const fileExtension = programmingLanguages[language] || '.file'
const suggestedFileName = `file-${generateRandomString( const suggestedFileName = `file-${generateRandomString(
3, 3,
true true
)}${fileExtension}`; )}${fileExtension}`
const fileName = window.prompt("Enter file name" || "", suggestedFileName); const fileName = window.prompt('Enter file name' || '', suggestedFileName)
if (!fileName) { if (!fileName) {
// user pressed cancel on prompt // user pressed cancel on prompt
return; return
} }
const blob = new Blob([value], { type: "text/plain" }); const blob = new Blob([value], { type: 'text/plain' })
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob)
const link = document.createElement("a"); const link = document.createElement('a')
link.download = fileName; link.download = fileName
link.href = url; link.href = url
link.style.display = "none"; link.style.display = 'none'
document.body.appendChild(link); document.body.appendChild(link)
link.click(); link.click()
document.body.removeChild(link); document.body.removeChild(link)
URL.revokeObjectURL(url); URL.revokeObjectURL(url)
}; }
return ( return (
<div className="codeblock relative w-full font-sans text-[16px]"> <div className="codeblock relative w-full font-sans text-[16px]">
<div className="flex w-full items-center justify-between px-4 py-1.5"> <div className="flex w-full items-center justify-between px-4 py-1.5">
@ -90,7 +90,7 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
onClick={() => copyToClipboard(value)} onClick={() => copyToClipboard(value)}
> >
{isCopied ? <Check size={18} /> : <Clipboard size={18} />} {isCopied ? <Check size={18} /> : <Clipboard size={18} />}
{isCopied ? "Copied!" : "Copy code"} {isCopied ? 'Copied!' : 'Copy code'}
</button> </button>
<button <button
type="button" type="button"
@ -107,21 +107,21 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
style={coldarkDark} style={coldarkDark}
customStyle={{ customStyle={{
margin: 0, margin: 0,
width: "100%", width: '100%',
background: "transparent", background: 'transparent'
}} }}
codeTagProps={{ codeTagProps={{
style: { style: {
fontSize: "0.9rem", fontSize: '0.9rem',
fontFamily: "var(--font-mono)", fontFamily: 'var(--font-mono)'
}, }
}} }}
> >
{value} {value}
</SyntaxHighlighter> </SyntaxHighlighter>
</div> </div>
); )
}); })
CodeBlock.displayName = "CodeBlock"; CodeBlock.displayName = 'CodeBlock'
export default CodeBlock; export default CodeBlock

View file

@ -1,15 +1,15 @@
"use client"; 'use client'
import { useRouter } from "next/navigation"; import { useRouter } from 'next/navigation'
export function Login() { export function Login() {
const router = useRouter(); const router = useRouter()
return ( return (
<button <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" 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={() => router.push("/api/auth/signin")} onClick={() => router.push('/api/auth/signin')}
> >
<span className="font-medium">Login</span> <span className="font-medium">Login</span>
</button> </button>
); )
} }

View file

@ -1,9 +1,9 @@
import { useId } from "react"; import { useId } from 'react'
export function NextChatLogo( export function NextChatLogo(
props: JSX.IntrinsicElements["svg"] & { inverted?: boolean } props: JSX.IntrinsicElements['svg'] & { inverted?: boolean }
) { ) {
const id = useId(); const id = useId()
return ( return (
<svg <svg
width={17} width={17}
@ -22,10 +22,10 @@ export function NextChatLogo(
y2="14.2667" y2="14.2667"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stopColor={props.inverted ? "white" : "black"} /> <stop stopColor={props.inverted ? 'white' : 'black'} />
<stop <stop
offset={1} offset={1}
stopColor={props.inverted ? "white" : "black"} stopColor={props.inverted ? 'white' : 'black'}
stopOpacity={0} stopOpacity={0}
/> />
</linearGradient> </linearGradient>
@ -37,35 +37,35 @@ export function NextChatLogo(
y2="9.50002" y2="9.50002"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop stopColor={props.inverted ? "white" : "black"} /> <stop stopColor={props.inverted ? 'white' : 'black'} />
<stop <stop
offset={1} offset={1}
stopColor={props.inverted ? "white" : "black"} stopColor={props.inverted ? 'white' : 'black'}
stopOpacity={0} stopOpacity={0}
/> />
</linearGradient> </linearGradient>
</defs> </defs>
<path <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" 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"} fill={props.inverted ? 'black' : 'white'}
stroke={props.inverted ? "black" : "white"} stroke={props.inverted ? 'black' : 'white'}
strokeWidth={2} strokeWidth={2}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
<mask <mask
id="mask0_91_2047" id="mask0_91_2047"
style={{ maskType: "alpha" }} style={{ maskType: 'alpha' }}
maskUnits="userSpaceOnUse" maskUnits="userSpaceOnUse"
x={1} x={1}
y={0} y={0}
width={16} width={16}
height={16} height={16}
> >
<circle cx={9} cy={8} r={8} fill={props.inverted ? "black" : "white"} /> <circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
</mask> </mask>
<g mask="url(#mask0_91_2047)"> <g mask="url(#mask0_91_2047)">
<circle cx={9} cy={8} r={8} fill={props.inverted ? "black" : "white"} /> <circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
<path <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" 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)`} fill={`url(#gradient-${id}-1)`}
@ -79,7 +79,7 @@ export function NextChatLogo(
/> />
</g> </g>
</svg> </svg>
); )
} }
NextChatLogo.displayName = "DevGPT-logo"; NextChatLogo.displayName = 'DevGPT-logo'

View file

@ -1,17 +1,17 @@
"use client"; 'use client'
import { ThemeToggle } from "@/components/theme-toggle"; import { ThemeToggle } from '@/components/theme-toggle'
import { type Session } from "@auth/nextjs/types"; import { type Session } from '@auth/nextjs/types'
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { signIn } from '@auth/nextjs/client'
import Image from "next/image"; import Image from 'next/image'
import { useRouter } from "next/navigation"; import { useRouter } from 'next/navigation'
export interface UserMenuProps { export interface UserMenuProps {
session: Session; session: Session
} }
export function UserMenu({ session }: UserMenuProps) { export function UserMenu({ session }: UserMenuProps) {
const router = useRouter(); const router = useRouter()
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<DropdownMenu.Root> <DropdownMenu.Root>
@ -22,8 +22,8 @@ export function UserMenu({ session }: UserMenuProps) {
width={24} width={24}
height={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" 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` : ""} src={session.user?.image ? `${session.user.image}&s=60` : ''}
alt={session.user.name ?? "Avatar"} 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="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">
@ -84,7 +84,7 @@ export function UserMenu({ session }: UserMenuProps) {
</DropdownMenu.Item> </DropdownMenu.Item>
<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" 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={() => router.push("/api/auth/signout")} onClick={() => router.push('/api/auth/signout')}
> >
Log Out Log Out
</DropdownMenu.Item> </DropdownMenu.Item>
@ -92,7 +92,7 @@ export function UserMenu({ session }: UserMenuProps) {
</DropdownMenu.Portal> </DropdownMenu.Portal>
</DropdownMenu.Root> </DropdownMenu.Root>
</div> </div>
); )
} }
UserMenu.displayName = "UserMenu"; UserMenu.displayName = 'UserMenu'

View file

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

View file

@ -1,62 +1,62 @@
import type { NextRequest, NextFetchEvent } from "next/server"; import type { NextRequest, NextFetchEvent } from 'next/server'
import type { NextApiRequest } from "next"; import type { NextApiRequest } from 'next'
export const initAnalytics = ({ export const initAnalytics = ({
request, request,
event, event
}: { }: {
request: NextRequest | NextApiRequest | Request; request: NextRequest | NextApiRequest | Request
event?: NextFetchEvent; event?: NextFetchEvent
}) => { }) => {
const endpoint = process.env.VERCEL_URL; const endpoint = process.env.VERCEL_URL
return { return {
track: async (eventName: string, data?: any) => { track: async (eventName: string, data?: any) => {
try { try {
if (!endpoint && process.env.NODE_ENV === "development") { if (!endpoint && process.env.NODE_ENV === 'development') {
console.log( console.log(
`[Vercel Web Analytics] Track "${eventName}"` + `[Vercel Web Analytics] Track "${eventName}"` +
(data ? ` with data ${JSON.stringify(data || {})}` : "") (data ? ` with data ${JSON.stringify(data || {})}` : '')
); )
return; return
} }
const headers: { [key: string]: string } = {}; const headers: { [key: string]: string } = {}
Object.entries(request.headers).map(([key, value]) => { Object.entries(request.headers).map(([key, value]) => {
headers[key] = value; headers[key] = value
}); })
const body = { const body = {
o: headers.referer, o: headers.referer,
ts: new Date().getTime(), ts: new Date().getTime(),
r: "", r: '',
en: eventName, en: eventName,
ed: data, ed: data
}; }
const promise = fetch( const promise = fetch(
`https://${process.env.VERCEL_URL}/_vercel/insights/event`, `https://${process.env.VERCEL_URL}/_vercel/insights/event`,
{ {
headers: { headers: {
"content-type": "application/json", 'content-type': 'application/json',
"user-agent": headers["user-agent"] as string, 'user-agent': headers['user-agent'] as string,
"x-forwarded-for": headers["x-forwarded-for"] as string, 'x-forwarded-for': headers['x-forwarded-for'] as string,
"x-va-server": "1", 'x-va-server': '1'
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
method: "POST", method: 'POST'
} }
); )
if (event) { if (event) {
event.waitUntil(promise); event.waitUntil(promise)
} }
{ {
await promise; await promise
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err)
} }
}, }
}; }
}; }

View file

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

View file

@ -1,19 +1,19 @@
import type { RefObject } from "react"; import type { RefObject } from 'react'
import { useRef } from "react"; import { useRef } from 'react'
export function useCmdEnterSubmit(): { export function useCmdEnterSubmit(): {
formRef: RefObject<HTMLFormElement>; formRef: RefObject<HTMLFormElement>
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void; onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
} { } {
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null)
const handleKeyDown = ( const handleKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement> event: React.KeyboardEvent<HTMLTextAreaElement>
): void => { ): void => {
if (event.key === "Enter") { if (event.key === 'Enter') {
formRef.current?.requestSubmit(); formRef.current?.requestSubmit()
}
} }
};
return { formRef, onKeyDown: handleKeyDown }; return { formRef, onKeyDown: handleKeyDown }
} }

View file

@ -1,14 +1,14 @@
'use client'; 'use client'
import { useState } from 'react'; import { useState } from 'react'
export interface useCopyToClipboardProps { export interface useCopyToClipboardProps {
timeout?: number; timeout?: number
} }
export function useCopyToClipboard({ export function useCopyToClipboard({
timeout = 2000, timeout = 2000
}: useCopyToClipboardProps) { }: useCopyToClipboardProps) {
const [isCopied, setIsCopied] = useState<Boolean>(false); const [isCopied, setIsCopied] = useState<Boolean>(false)
const copyToClipboard = (value: string) => { const copyToClipboard = (value: string) => {
if ( if (
@ -16,17 +16,17 @@ export function useCopyToClipboard({
!navigator.clipboard || !navigator.clipboard ||
!navigator.clipboard.writeText !navigator.clipboard.writeText
) { ) {
return; return
} }
navigator.clipboard.writeText(value).then(() => { navigator.clipboard.writeText(value).then(() => {
setIsCopied(true); setIsCopied(true)
setTimeout(() => { setTimeout(() => {
setIsCopied(false); setIsCopied(false)
}, timeout); }, timeout)
}); })
}; }
return { isCopied, copyToClipboard }; return { isCopied, copyToClipboard }
} }

View file

@ -1,43 +1,43 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from 'react'
const scrollToEnd = () => { const scrollToEnd = () => {
window.scrollTo({ window.scrollTo({
top: document.body.scrollHeight, top: document.body.scrollHeight,
left: 0, left: 0
}); })
}; }
export function useFollowScroll(shouldFollow: boolean) { export function useFollowScroll(shouldFollow: boolean) {
const shouldFollowRef = useRef(shouldFollow); const shouldFollowRef = useRef(shouldFollow)
const scrollRef = useRef(false); const scrollRef = useRef(false)
const triggeredBySelfRef = useRef(false); const triggeredBySelfRef = useRef(false)
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
if (triggeredBySelfRef.current) { if (triggeredBySelfRef.current) {
triggeredBySelfRef.current = false; triggeredBySelfRef.current = false
return; return
} }
scrollRef.current = true; scrollRef.current = true
}; }
window.addEventListener("scroll", handleScroll); window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener('scroll', handleScroll)
}, []); }, [])
useEffect(() => { useEffect(() => {
if (!scrollRef.current && shouldFollowRef.current) { if (!scrollRef.current && shouldFollowRef.current) {
setTimeout(() => { setTimeout(() => {
triggeredBySelfRef.current = true; triggeredBySelfRef.current = true
scrollToEnd(); scrollToEnd()
}); })
} }
}); })
useEffect(() => { useEffect(() => {
shouldFollowRef.current = shouldFollow; shouldFollowRef.current = shouldFollow
if (!shouldFollow) { if (!shouldFollow) {
// Reset scrollRef // Reset scrollRef
scrollRef.current = false; scrollRef.current = false
} }
}, [shouldFollow]); }, [shouldFollow])
} }

View file

@ -1,26 +1,26 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
const useLocalStorage = <T>( const useLocalStorage = <T>(
key: string, key: string,
initialValue: T initialValue: T
): [T, (value: T) => void] => { ): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState(initialValue); const [storedValue, setStoredValue] = useState(initialValue)
useEffect(() => { useEffect(() => {
// Retrieve from localStorage // Retrieve from localStorage
const item = window.localStorage.getItem(key); const item = window.localStorage.getItem(key)
if (item) { if (item) {
setStoredValue(JSON.parse(item)); setStoredValue(JSON.parse(item))
} }
}, [key]); }, [key])
const setValue = (value: T) => { const setValue = (value: T) => {
// Save state // Save state
setStoredValue(value); setStoredValue(value)
// Save to localStorage // Save to localStorage
window.localStorage.setItem(key, JSON.stringify(value)); window.localStorage.setItem(key, JSON.stringify(value))
}; }
return [storedValue, setValue]; return [storedValue, setValue]
}; }
export default useLocalStorage; export default useLocalStorage

View file

@ -1,16 +1,16 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from 'react'
export default function useScroll(threshold: number) { export default function useScroll(threshold: number) {
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false)
const onScroll = useCallback(() => { const onScroll = useCallback(() => {
setScrolled(window.pageYOffset > threshold); setScrolled(window.pageYOffset > threshold)
}, [threshold]); }, [threshold])
useEffect(() => { useEffect(() => {
window.addEventListener("scroll", onScroll); window.addEventListener('scroll', onScroll)
return () => window.removeEventListener("scroll", onScroll); return () => window.removeEventListener('scroll', onScroll)
}, [onScroll]); }, [onScroll])
return scrolled; return scrolled
} }

View file

@ -1,14 +1,14 @@
"use client"; 'use client'
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
export default function useWindowSize() { export default function useWindowSize() {
const [windowSize, setWindowSize] = useState<{ const [windowSize, setWindowSize] = useState<{
width: number | undefined; width: number | undefined
height: number | undefined; height: number | undefined
}>({ }>({
width: undefined, width: undefined,
height: undefined, height: undefined
}); })
useEffect(() => { useEffect(() => {
// Handler to call on window resize // Handler to call on window resize
@ -16,24 +16,23 @@ export default function useWindowSize() {
// Set window width/height to state // Set window width/height to state
setWindowSize({ setWindowSize({
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight
}); })
} }
// Add event listener // Add event listener
window.addEventListener("resize", handleResize); window.addEventListener('resize', handleResize)
// Call handler right away so state gets updated with initial window size // Call handler right away so state gets updated with initial window size
handleResize(); handleResize()
// Remove event listener on cleanup // Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener('resize', handleResize)
}, []); // Empty array ensures that effect is only run on mount }, []) // Empty array ensures that effect is only run on mount
return { return {
windowSize, windowSize,
isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768, isMobile: typeof windowSize?.width === 'number' && windowSize?.width < 768,
isDesktop: isDesktop: typeof windowSize?.width === 'number' && windowSize?.width >= 768
typeof windowSize?.width === "number" && windowSize?.width >= 768, }
};
} }

View file

@ -1,9 +1,9 @@
import { type Message } from "ai-connector"; import { type Message } from 'ai-connector'
export interface Chat extends Record<string, any> { export interface Chat extends Record<string, any> {
id: string; id: string
title: string; title: string
createdAt: Date; createdAt: Date
userId: string; userId: string
messages: Message[]; messages: Message[]
} }

View file

@ -1,34 +1,34 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from 'clsx'
import { customAlphabet } from "nanoid"; import { customAlphabet } from 'nanoid'
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs))
} }
export const nanoid = customAlphabet( export const nanoid = customAlphabet(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
7 7
); // 7-character random string ) // 7-character random string
export async function fetcher<JSON = any>( export async function fetcher<JSON = any>(
input: RequestInfo, input: RequestInfo,
init?: RequestInit init?: RequestInit
): Promise<JSON> { ): Promise<JSON> {
const res = await fetch(input, init); const res = await fetch(input, init)
if (!res.ok) { if (!res.ok) {
const json = await res.json(); const json = await res.json()
if (json.error) { if (json.error) {
const error = new Error(json.error) as Error & { const error = new Error(json.error) as Error & {
status: number; status: number
}; }
error.status = res.status; error.status = res.status
throw error; throw error
} else { } else {
throw new Error("An unexpected error occurred"); throw new Error('An unexpected error occurred')
} }
} }
return res.json(); return res.json()
} }

View file

@ -61,5 +61,13 @@
"overrides": { "overrides": {
"@auth/core": "0.0.0-manual.527fff6c" "@auth/core": "0.0.0-manual.527fff6c"
} }
},
"prettier": {
"tabWidth": 2,
"semi": false,
"useTabs": false,
"singleQuote": true,
"arrowParens": "avoid",
"trailingComma": "none"
} }
} }