diff --git a/app/actions.ts b/app/actions.ts index dc5f5a5..aa4c98f 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,27 +1,27 @@ -"use server"; +'use server' -import { kv } from "@vercel/kv"; -import { revalidatePath } from "next/cache"; +import { kv } from '@vercel/kv' +import { revalidatePath } from 'next/cache' export async function removeChat({ id, path, - userId, + userId }: { - id: string; - userId: string; - path: string; + id: string + userId: string + path: string }) { // @todo next-auth@v5 doesn't work in server actions yet // const session = await auth(); - const uid = await kv.hget(`chat:${id}`, "userId"); + const uid = await kv.hget(`chat:${id}`, 'userId') if (uid !== userId) { - throw new Error("Unauthorized"); + throw new Error('Unauthorized') } - await kv.del(`chat:${id}`); - await kv.zrem(`user:chat:${userId}`, `chat:${id}`); + await kv.del(`chat:${id}`) + await kv.zrem(`user:chat:${userId}`, `chat:${id}`) - revalidatePath("/"); - revalidatePath("/chat/[id]"); + revalidatePath('/') + revalidatePath('/chat/[id]') } diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index e47b0fd..883210b 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,2 +1,2 @@ -export { GET, POST } from "@/auth"; -export const runtime = "edge"; +export { GET, POST } from '@/auth' +export const runtime = 'edge' diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 438241f..710194d 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,50 +1,50 @@ -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"; +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"; +export const runtime = 'edge' 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) { - 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) { - const json = await req.json(); + const json = await req.json() // @ts-ignore - console.log(req.auth); // todo fix types + console.log(req.auth) // todo fix types const messages = json.messages.map((m: any) => ({ content: m.content, - role: m.role, - })); + role: m.role + })) const res = await openai.createChatCompletion({ - model: "gpt-3.5-turbo", + model: 'gpt-3.5-turbo', messages, temperature: 0.7, top_p: 1, frequency_penalty: 1, max_tokens: 500, n: 1, - stream: true, - }); + stream: true + }) const stream = await OpenAIStream(res, { async onCompletion(completion) { // @ts-ignore if (req.auth?.user?.email == null) { - return; + return } - const title = json.messages[0].content.substring(0, 20); - const userId = (req as any).auth?.user?.email; - const id = json.id ?? nanoid(); - const createdAt = Date.now(); + const title = json.messages[0].content.substring(0, 20) + const userId = (req as any).auth?.user?.email + const id = json.id ?? nanoid() + const createdAt = Date.now() const payload = { id, title, @@ -54,17 +54,17 @@ export const POST = auth(async function POST(req: Request) { ...messages, { content: completion, - role: "assistant", - }, - ], - }; - await kv.hmset(`chat:${id}`, payload); + role: 'assistant' + } + ] + } + await kv.hmset(`chat:${id}`, payload) await kv.zadd(`user:chat:${userId}`, { score: createdAt, - member: `chat:${id}`, - }); - }, - }); + member: `chat:${id}` + }) + } + }) - return new StreamingTextResponse(stream); -}); + return new StreamingTextResponse(stream) +}) diff --git a/app/chat-list.tsx b/app/chat-list.tsx index 9e63425..7675690 100644 --- a/app/chat-list.tsx +++ b/app/chat-list.tsx @@ -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 { NextChatLogo } from "@/components/ui/nextchat-logo"; -import { Plus } from "lucide-react"; -import { EmptyScreen } from "./empty"; +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[]; + messages: Message[] } export function ChatList({ messages }: ChatList) { @@ -30,7 +30,7 @@ export function ChatList({ messages }: ChatList) {
{messages.length > 0 ? (
- {messages.map((message) => ( + {messages.map(message => (
- ); + ) } diff --git a/app/chat-message.tsx b/app/chat-message.tsx index 8b84e4a..dab5d07 100644 --- a/app/chat-message.tsx +++ b/app/chat-message.tsx @@ -1,12 +1,12 @@ -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"; +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; + message: Message } export function ChatMessage(props: ChatMessageProps) { @@ -14,15 +14,15 @@ export function ChatMessage(props: ChatMessageProps) {
- {props.message.role === "user" ? ( + {props.message.role === 'user' ? (
▍ - ); + ) } - 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 ? ( ) : ( {children} - ); + ) }, table({ children }) { return ( {children}
- ); + ) }, th({ children }) { return ( {children} - ); + ) }, td({ children }) { return ( {children} - ); - }, + ) + } }} > {props.message.content} @@ -96,12 +96,12 @@ export function ChatMessage(props: ChatMessageProps) {
- ); + ) } -ChatMessage.displayName = "ChatMessage"; +ChatMessage.displayName = 'ChatMessage' -export function IconOpenAI(props: JSX.IntrinsicElements["svg"]) { +export function IconOpenAI(props: JSX.IntrinsicElements['svg']) { return ( OpenAI icon - ); + ) } diff --git a/app/chat.tsx b/app/chat.tsx index 0daaef0..d328db2 100644 --- a/app/chat.tsx +++ b/app/chat.tsx @@ -1,30 +1,30 @@ -"use client"; +'use client' -import { useRouter } from "next/navigation"; -import { Prompt } from "./prompt"; -import { useChat, type Message } from "ai-connector"; -import { ChatList } from "./chat-list"; +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; + initialMessages?: Message[] + id?: string } export function Chat({ id, // create, - initialMessages, + initialMessages }: ChatProps) { - const router = useRouter(); + const router = useRouter() const { isLoading, messages, reload, append } = useChat({ initialMessages: initialMessages as any[], - id, + id // onCreate: (id: string) => { // router.push(`/chat/${id}`); // }, - }); + }) return (
@@ -33,18 +33,18 @@ export function Chat({
{ + onSubmit={value => { append({ content: value, - role: "user", - }); + role: 'user' + }) }} onRefresh={messages.length ? reload : undefined} isLoading={isLoading} />
- ); + ) } -Chat.displayName = "Chat"; +Chat.displayName = 'Chat' diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index fbe5b35..61094d6 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -1,34 +1,34 @@ -import { Sidebar } from "@/app/sidebar"; +import { Sidebar } from '@/app/sidebar' -import { auth } from "@/auth"; -import { type Metadata } from "next"; +import { auth } from '@/auth' +import { type Metadata } from 'next' -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 '@/app/chat' +import { type Chat as ChatType } from '@/lib/types' +import { kv } from '@vercel/kv' +import { Message } from 'ai-connector' -export const runtime = "edge"; -export const preferredRegion = "home"; +export const runtime = 'edge' +export const preferredRegion = 'home' export interface ChatPageProps { params: { - id: string; - }; + id: string + } } export async function generateMetadata({ - params, + params }: ChatPageProps): Promise { - const session = await auth(); - const chat = await getChat(params.id, session?.user?.email ?? ""); + const session = await auth() + const chat = await getChat(params.id, session?.user?.email ?? '') return { - title: chat?.title.slice(0, 50) ?? "Chat", - }; + title: chat?.title.slice(0, 50) ?? 'Chat' + } } export default async function ChatPage({ params }: ChatPageProps) { - const session = await auth(); - const chat = await getChat(params.id, session?.user?.email ?? ""); + const session = await auth() + const chat = await getChat(params.id, session?.user?.email ?? '') return (
@@ -37,20 +37,20 @@ export default async function ChatPage({ params }: ChatPageProps) {
- ); + ) } -ChatPage.displayName = "ChatPage"; +ChatPage.displayName = 'ChatPage' async function getChat(id: string, userId: string) { - const chat = await kv.hgetall(`chat:${id}`); + const chat = await kv.hgetall(`chat:${id}`) if (!chat) { - throw new Error("Not found"); + throw new Error('Not found') } if (userId && chat.userId !== userId) { - throw new Error("Unauthorized"); + throw new Error('Unauthorized') } - return chat; + return chat } diff --git a/app/empty.tsx b/app/empty.tsx index 1c58122..0946db4 100644 --- a/app/empty.tsx +++ b/app/empty.tsx @@ -1,12 +1,12 @@ -import { cn } from "@/lib/utils"; -import { ExternalLink } from "./external-link"; -import { fontMessage } from "@/lib/fonts"; +import { cn } from '@/lib/utils' +import { ExternalLink } from './external-link' +import { fontMessage } from '@/lib/fonts' function ExampleBubble({ children }: { children?: React.ReactNode }) { return (

{children}

- ); + ) } export function EmptyScreen() { return (
@@ -35,8 +35,8 @@ export function EmptyScreen() {

Welcome to Next.js Chatbot!

- This is an open source AI chatbot app built with{" "} - Next.js and{" "} + This is an open source AI chatbot app built with{' '} + Next.js and{' '} Vercel Postgres @@ -52,5 +52,5 @@ export function EmptyScreen() {

- ); + ) } diff --git a/app/external-link.tsx b/app/external-link.tsx index 7025ee5..175cd6b 100644 --- a/app/external-link.tsx +++ b/app/external-link.tsx @@ -1,9 +1,9 @@ export function ExternalLink({ href, - children, + children }: { - href: string; - children: React.ReactNode; + href: string + children: React.ReactNode }) { return ( - ); + ) } diff --git a/app/layout.tsx b/app/layout.tsx index eb3ca71..b9e8d26 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,29 +1,29 @@ -import "./globals.css"; -import { Metadata } from "next"; +import './globals.css' +import { Metadata } from 'next' -import { ThemeProvider } from "@/components/theme-provider"; -import { fontMono, fontSans } from "@/lib/fonts"; -import { cn } from "@/lib/utils"; +import { ThemeProvider } from '@/components/theme-provider' +import { fontMono, fontSans } from '@/lib/fonts' +import { cn } from '@/lib/utils' export const metadata: Metadata = { title: { - default: "Next.js Chatbot", - template: `%s - Next.js Chatbot`, + default: '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: [ - { media: "(prefers-color-scheme: light)", color: "white" }, - { media: "(prefers-color-scheme: dark)", color: "black" }, + { media: '(prefers-color-scheme: light)', color: 'white' }, + { media: '(prefers-color-scheme: dark)', color: 'black' } ], icons: { - icon: "/favicon.ico", - shortcut: "/favicon-16x16.png", - apple: "/apple-touch-icon.png", - }, -}; + icon: '/favicon.ico', + shortcut: '/favicon-16x16.png', + apple: '/apple-touch-icon.png' + } +} interface RootLayoutProps { - children: React.ReactNode; + children: React.ReactNode } export default function RootLayout({ children }: RootLayoutProps) { @@ -33,7 +33,7 @@ export default function RootLayout({ children }: RootLayoutProps) { - ); + ) } diff --git a/app/page.tsx b/app/page.tsx index 6212e99..0804ebc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,12 @@ -import { Chat } from "./chat"; -import { Sidebar } from "./sidebar"; -import { auth } from "@/auth"; +import { Chat } from './chat' +import { Sidebar } from './sidebar' +import { auth } from '@/auth' -export const runtime = "edge"; -export const preferredRegion = "home"; +export const runtime = 'edge' +export const preferredRegion = 'home' export default async function IndexPage() { - const session = await auth(); + const session = await auth() return (
@@ -14,5 +14,5 @@ export default async function IndexPage() {
- ); + ) } diff --git a/app/prompt.tsx b/app/prompt.tsx index 26ad418..0c03997 100644 --- a/app/prompt.tsx +++ b/app/prompt.tsx @@ -1,35 +1,35 @@ -"use client"; +'use client' -import { CornerDownLeft, RefreshCcw, StopCircle } from "lucide-react"; -import { useState } from "react"; -import Textarea from "react-textarea-autosize"; +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"; +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; + onSubmit: (value: string) => void + onRefresh?: () => void + onAbort?: () => void + isLoading: boolean } export function Prompt({ onSubmit, onRefresh, onAbort, - isLoading, + isLoading }: PromptProps) { - const [input, setInput] = useState(""); - const { formRef, onKeyDown } = useCmdEnterSubmit(); + const [input, setInput] = useState('') + const { formRef, onKeyDown } = useCmdEnterSubmit() return (
{ - e.preventDefault(); - setInput(""); - await onSubmit(input); + 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" @@ -77,16 +77,16 @@ export function Prompt({ onKeyDown={onKeyDown} rows={1} value={input} - onChange={(e) => setInput(e.target.value)} + 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", + '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", + overflowY: 'hidden' }} /> {isLoading ? ( @@ -108,7 +108,7 @@ export function Prompt({
- ); + ) } -Prompt.displayName = "Prompt"; +Prompt.displayName = 'Prompt' diff --git a/app/sidebar-item.tsx b/app/sidebar-item.tsx index 09cae84..f0598e6 100644 --- a/app/sidebar-item.tsx +++ b/app/sidebar-item.tsx @@ -1,41 +1,41 @@ -"use client"; +'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"; +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, + userId }: { - title: string; - href: string; - id: string; - userId: string; + title: string + href: string + id: string + userId: string }) { - const pathname = usePathname(); - const router = useRouter(); - const active = pathname === href; - const [isPending, startTransition] = useTransition(); + const pathname = usePathname() + const router = useRouter() + const active = pathname === href + const [isPending, startTransition] = useTransition() - if (!id) return null; + if (!id) return null return ( @@ -48,13 +48,13 @@ export function SidebarItem({ - ); + ) } diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 991f64e..4ba42f9 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -1,18 +1,18 @@ -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"; +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; + session?: Session + newChat?: boolean } export function Sidebar({ session, newChat }: SidebarProps) { @@ -33,10 +33,10 @@ export function Sidebar({ session, newChat }: SidebarProps) { @@ -80,38 +80,38 @@ export function Sidebar({ session, newChat }: SidebarProps) { - ); + ) } -Sidebar.displayName = "Sidebar"; +Sidebar.displayName = 'Sidebar' 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 => ( - )); + )) } async function getChats(userId: string) { try { - const pipeline = kv.pipeline(); - const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1); + const pipeline = kv.pipeline() + const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1) 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) { - return []; + return [] } } diff --git a/components/markdown.tsx b/components/markdown.tsx index 6e6aba2..d449146 100644 --- a/components/markdown.tsx +++ b/components/markdown.tsx @@ -1,9 +1,9 @@ -import { FC, memo } from "react"; -import ReactMarkdown, { Options } from "react-markdown"; +import { FC, memo } from 'react' +import ReactMarkdown, { Options } from 'react-markdown' export const MemoizedReactMarkdown: FC = memo( ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.className === nextProps.className -); +) diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index b4e8e4f..8df1f59 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -1,9 +1,9 @@ -"use client"; +'use client' -import * as React from "react"; -import { ThemeProvider as NextThemesProvider } from "next-themes"; -import { ThemeProviderProps } from "next-themes/dist/types"; +import * as React from 'react' +import { ThemeProvider as NextThemesProvider } from 'next-themes' +import { ThemeProviderProps } from 'next-themes/dist/types' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children}; + return {children} } diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx index 93847ec..5493bd6 100644 --- a/components/theme-toggle.tsx +++ b/components/theme-toggle.tsx @@ -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 { Moon, Sun } from "lucide-react"; -import { useTransition } from "react"; +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 { setTheme, theme } = useTheme() + const [_, startTransition] = useTransition() return ( - ); + ) } diff --git a/components/ui/button.tsx b/components/ui/button.tsx index c604faa..788f113 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,36 +1,36 @@ -import * as React from "react"; -import { VariantProps, cva } from "class-variance-authority"; +import * as React from 'react' +import { VariantProps, cva } from 'class-variance-authority' -import { cn } from "@/lib/utils"; +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 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: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: - "border border-input hover:bg-accent hover:text-accent-foreground", + '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", + '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' }, 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-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' + } }, defaultVariants: { - variant: "default", - size: "default", - }, + variant: 'default', + size: 'default' + } } -); +) export interface ButtonProps extends React.ButtonHTMLAttributes, @@ -44,9 +44,9 @@ const Button = React.forwardRef( ref={ref} {...props} /> - ); + ) } -); -Button.displayName = "Button"; +) +Button.displayName = 'Button' -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/components/ui/codeblock.tsx b/components/ui/codeblock.tsx index 264cd6f..ddc0834 100644 --- a/components/ui/codeblock.tsx +++ b/components/ui/codeblock.tsx @@ -1,83 +1,83 @@ -"use client"; -import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-cliipboard"; -import { Check, Clipboard, Download } from "lucide-react"; -import { FC, memo } from "react"; -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; +'use client' +import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-cliipboard' +import { Check, Clipboard, Download } from 'lucide-react' +import { FC, memo } from 'react' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' interface Props { - language: string; - value: string; + language: string + value: string } interface languageMap { - [key: string]: string | undefined; + [key: string]: string | undefined } export const programmingLanguages: languageMap = { - javascript: ".js", - python: ".py", - java: ".java", - c: ".c", - cpp: ".cpp", - "c++": ".cpp", - "c#": ".cs", - ruby: ".rb", - php: ".php", - swift: ".swift", - "objective-c": ".m", - kotlin: ".kt", - typescript: ".ts", - go: ".go", - perl: ".pl", - rust: ".rs", - scala: ".scala", - haskell: ".hs", - lua: ".lua", - shell: ".sh", - sql: ".sql", - html: ".html", - css: ".css", + javascript: '.js', + python: '.py', + java: '.java', + c: '.c', + cpp: '.cpp', + 'c++': '.cpp', + 'c#': '.cs', + ruby: '.rb', + php: '.php', + swift: '.swift', + 'objective-c': '.m', + kotlin: '.kt', + typescript: '.ts', + go: '.go', + perl: '.pl', + rust: '.rs', + scala: '.scala', + haskell: '.hs', + lua: '.lua', + shell: '.sh', + sql: '.sql', + html: '.html', + css: '.css' // 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) => { - const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0 - let result = ""; + const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0 + let result = '' 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 = memo(({ language, value }) => { - const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); + const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) const downloadAsFile = () => { - if (typeof window === "undefined") { - return; + if (typeof window === 'undefined') { + return } - const fileExtension = programmingLanguages[language] || ".file"; + const fileExtension = programmingLanguages[language] || '.file' const suggestedFileName = `file-${generateRandomString( 3, true - )}${fileExtension}`; - const fileName = window.prompt("Enter file name" || "", suggestedFileName); + )}${fileExtension}` + const fileName = window.prompt('Enter file name' || '', suggestedFileName) if (!fileName) { // user pressed cancel on prompt - return; + return } - const blob = new Blob([value], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.download = fileName; - link.href = url; - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; + const blob = new Blob([value], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.download = fileName + link.href = url + link.style.display = 'none' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } return (
@@ -90,7 +90,7 @@ const CodeBlock: FC = memo(({ language, value }) => { onClick={() => copyToClipboard(value)} > {isCopied ? : } - {isCopied ? "Copied!" : "Copy code"} + {isCopied ? 'Copied!' : 'Copy code'}
- ); -}); -CodeBlock.displayName = "CodeBlock"; + ) +}) +CodeBlock.displayName = 'CodeBlock' -export default CodeBlock; +export default CodeBlock diff --git a/components/ui/login.tsx b/components/ui/login.tsx index b1b32a9..c8ef08d 100644 --- a/components/ui/login.tsx +++ b/components/ui/login.tsx @@ -1,15 +1,15 @@ -"use client"; +'use client' -import { useRouter } from "next/navigation"; +import { useRouter } from 'next/navigation' export function Login() { - const router = useRouter(); + const router = useRouter() return ( - ); + ) } diff --git a/components/ui/nextchat-logo.tsx b/components/ui/nextchat-logo.tsx index 2fcd355..c092a6e 100644 --- a/components/ui/nextchat-logo.tsx +++ b/components/ui/nextchat-logo.tsx @@ -1,9 +1,9 @@ -import { useId } from "react"; +import { useId } from 'react' export function NextChatLogo( - props: JSX.IntrinsicElements["svg"] & { inverted?: boolean } + props: JSX.IntrinsicElements['svg'] & { inverted?: boolean } ) { - const id = useId(); + const id = useId() return ( - + @@ -37,35 +37,35 @@ export function NextChatLogo( y2="9.50002" gradientUnits="userSpaceOnUse" > - + - + - + - ); + ) } -NextChatLogo.displayName = "DevGPT-logo"; +NextChatLogo.displayName = 'DevGPT-logo' diff --git a/components/ui/user-menu.tsx b/components/ui/user-menu.tsx index 4cfc263..bec0bcc 100644 --- a/components/ui/user-menu.tsx +++ b/components/ui/user-menu.tsx @@ -1,17 +1,17 @@ -"use client"; -import { ThemeToggle } from "@/components/theme-toggle"; -import { type Session } from "@auth/nextjs/types"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; - -import Image from "next/image"; -import { useRouter } from "next/navigation"; +'use client' +import { ThemeToggle } from '@/components/theme-toggle' +import { type Session } from '@auth/nextjs/types' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { signIn } from '@auth/nextjs/client' +import Image from 'next/image' +import { useRouter } from 'next/navigation' export interface UserMenuProps { - session: Session; + session: Session } export function UserMenu({ session }: UserMenuProps) { - const router = useRouter(); + const router = useRouter() return (
@@ -22,8 +22,8 @@ export function UserMenu({ session }: UserMenuProps) { 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"} + src={session.user?.image ? `${session.user.image}&s=60` : ''} + alt={session.user.name ?? 'Avatar'} /> ) : (
@@ -84,7 +84,7 @@ export function UserMenu({ session }: UserMenuProps) { router.push("/api/auth/signout")} + onClick={() => router.push('/api/auth/signout')} > Log Out @@ -92,7 +92,7 @@ export function UserMenu({ session }: UserMenuProps) {
- ); + ) } -UserMenu.displayName = "UserMenu"; +UserMenu.displayName = 'UserMenu' diff --git a/components/ui/vercel-logo.tsx b/components/ui/vercel-logo.tsx index f85fe69..d25b96d 100644 --- a/components/ui/vercel-logo.tsx +++ b/components/ui/vercel-logo.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils"; +import { cn } from '@/lib/utils' export const VercelLogo = ({ className }: { className?: string }) => ( ( fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Vercel Logo" - className={cn(className, "dark:fill-white fill-black")} + className={cn(className, 'dark:fill-white fill-black')} > -); +) diff --git a/lib/analytics.ts b/lib/analytics.ts index 7d213c9..5eead76 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -1,62 +1,62 @@ -import type { NextRequest, NextFetchEvent } from "next/server"; -import type { NextApiRequest } from "next"; +import type { NextRequest, NextFetchEvent } from 'next/server' +import type { NextApiRequest } from 'next' export const initAnalytics = ({ request, - event, + event }: { - request: NextRequest | NextApiRequest | Request; - event?: NextFetchEvent; + request: NextRequest | NextApiRequest | Request + event?: NextFetchEvent }) => { - const endpoint = process.env.VERCEL_URL; + const endpoint = process.env.VERCEL_URL return { track: async (eventName: string, data?: any) => { try { - if (!endpoint && process.env.NODE_ENV === "development") { + if (!endpoint && process.env.NODE_ENV === 'development') { console.log( `[Vercel Web Analytics] Track "${eventName}"` + - (data ? ` with data ${JSON.stringify(data || {})}` : "") - ); - return; + (data ? ` with data ${JSON.stringify(data || {})}` : '') + ) + return } - const headers: { [key: string]: string } = {}; + const headers: { [key: string]: string } = {} Object.entries(request.headers).map(([key, value]) => { - headers[key] = value; - }); + headers[key] = value + }) const body = { o: headers.referer, ts: new Date().getTime(), - r: "", + r: '', en: eventName, - ed: data, - }; + ed: data + } const promise = fetch( `https://${process.env.VERCEL_URL}/_vercel/insights/event`, { headers: { - "content-type": "application/json", - "user-agent": headers["user-agent"] as string, - "x-forwarded-for": headers["x-forwarded-for"] as string, - "x-va-server": "1", + 'content-type': 'application/json', + 'user-agent': headers['user-agent'] as string, + 'x-forwarded-for': headers['x-forwarded-for'] as string, + 'x-va-server': '1' }, body: JSON.stringify(body), - method: "POST", + method: 'POST' } - ); + ) if (event) { - event.waitUntil(promise); + event.waitUntil(promise) } { - await promise; + await promise } } catch (err) { - console.error(err); + console.error(err) } - }, - }; -}; + } + } +} diff --git a/lib/fonts.ts b/lib/fonts.ts index 9ab8af2..0ba1393 100644 --- a/lib/fonts.ts +++ b/lib/fonts.ts @@ -1,21 +1,21 @@ import { JetBrains_Mono as FontMono, IBM_Plex_Sans as FontMessage, - Inter as FontSans, -} from "next/font/google"; + Inter as FontSans +} from 'next/font/google' export const fontSans = FontSans({ - subsets: ["latin"], - variable: "--font-sans", -}); + subsets: ['latin'], + variable: '--font-sans' +}) export const fontMessage = FontMessage({ - subsets: ["latin"], - weight: ["400", "500", "600"], - variable: "--font-message", -}); + subsets: ['latin'], + weight: ['400', '500', '600'], + variable: '--font-message' +}) export const fontMono = FontMono({ - subsets: ["latin"], - variable: "--font-mono", -}); + subsets: ['latin'], + variable: '--font-mono' +}) diff --git a/lib/hooks/use-command-enter-submit.tsx b/lib/hooks/use-command-enter-submit.tsx index bf178c7..61e796c 100644 --- a/lib/hooks/use-command-enter-submit.tsx +++ b/lib/hooks/use-command-enter-submit.tsx @@ -1,19 +1,19 @@ -import type { RefObject } from "react"; -import { useRef } from "react"; +import type { RefObject } from 'react' +import { useRef } from 'react' export function useCmdEnterSubmit(): { - formRef: RefObject; - onKeyDown: (event: React.KeyboardEvent) => void; + formRef: RefObject + onKeyDown: (event: React.KeyboardEvent) => void } { - const formRef = useRef(null); + const formRef = useRef(null) const handleKeyDown = ( event: React.KeyboardEvent ): void => { - if (event.key === "Enter") { - formRef.current?.requestSubmit(); + if (event.key === 'Enter') { + formRef.current?.requestSubmit() } - }; + } - return { formRef, onKeyDown: handleKeyDown }; + return { formRef, onKeyDown: handleKeyDown } } diff --git a/lib/hooks/use-copy-to-cliipboard.tsx b/lib/hooks/use-copy-to-cliipboard.tsx index 3a9ffa1..df4d1f8 100644 --- a/lib/hooks/use-copy-to-cliipboard.tsx +++ b/lib/hooks/use-copy-to-cliipboard.tsx @@ -1,14 +1,14 @@ -'use client'; -import { useState } from 'react'; +'use client' +import { useState } from 'react' export interface useCopyToClipboardProps { - timeout?: number; + timeout?: number } export function useCopyToClipboard({ - timeout = 2000, + timeout = 2000 }: useCopyToClipboardProps) { - const [isCopied, setIsCopied] = useState(false); + const [isCopied, setIsCopied] = useState(false) const copyToClipboard = (value: string) => { if ( @@ -16,17 +16,17 @@ export function useCopyToClipboard({ !navigator.clipboard || !navigator.clipboard.writeText ) { - return; + return } navigator.clipboard.writeText(value).then(() => { - setIsCopied(true); + setIsCopied(true) setTimeout(() => { - setIsCopied(false); - }, timeout); - }); - }; + setIsCopied(false) + }, timeout) + }) + } - return { isCopied, copyToClipboard }; + return { isCopied, copyToClipboard } } diff --git a/lib/hooks/use-follow-scroll.ts b/lib/hooks/use-follow-scroll.ts index e580bb1..47a5c29 100644 --- a/lib/hooks/use-follow-scroll.ts +++ b/lib/hooks/use-follow-scroll.ts @@ -1,43 +1,43 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef } from 'react' const scrollToEnd = () => { window.scrollTo({ top: document.body.scrollHeight, - left: 0, - }); -}; + left: 0 + }) +} export function useFollowScroll(shouldFollow: boolean) { - const shouldFollowRef = useRef(shouldFollow); - const scrollRef = useRef(false); - const triggeredBySelfRef = useRef(false); + const shouldFollowRef = useRef(shouldFollow) + const scrollRef = useRef(false) + const triggeredBySelfRef = useRef(false) useEffect(() => { const handleScroll = () => { if (triggeredBySelfRef.current) { - triggeredBySelfRef.current = false; - return; + triggeredBySelfRef.current = false + return } - scrollRef.current = true; - }; - window.addEventListener("scroll", handleScroll); - return () => window.removeEventListener("scroll", handleScroll); - }, []); + scrollRef.current = true + } + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, []) useEffect(() => { if (!scrollRef.current && shouldFollowRef.current) { setTimeout(() => { - triggeredBySelfRef.current = true; - scrollToEnd(); - }); + triggeredBySelfRef.current = true + scrollToEnd() + }) } - }); + }) useEffect(() => { - shouldFollowRef.current = shouldFollow; + shouldFollowRef.current = shouldFollow if (!shouldFollow) { // Reset scrollRef - scrollRef.current = false; + scrollRef.current = false } - }, [shouldFollow]); + }, [shouldFollow]) } diff --git a/lib/hooks/use-local-storage.ts b/lib/hooks/use-local-storage.ts index ba6e8c5..15c4291 100644 --- a/lib/hooks/use-local-storage.ts +++ b/lib/hooks/use-local-storage.ts @@ -1,26 +1,26 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState } from 'react' const useLocalStorage = ( key: string, initialValue: T ): [T, (value: T) => void] => { - const [storedValue, setStoredValue] = useState(initialValue); + const [storedValue, setStoredValue] = useState(initialValue) useEffect(() => { // Retrieve from localStorage - const item = window.localStorage.getItem(key); + const item = window.localStorage.getItem(key) if (item) { - setStoredValue(JSON.parse(item)); + setStoredValue(JSON.parse(item)) } - }, [key]); + }, [key]) const setValue = (value: T) => { // Save state - setStoredValue(value); + setStoredValue(value) // Save to localStorage - window.localStorage.setItem(key, JSON.stringify(value)); - }; - return [storedValue, setValue]; -}; + window.localStorage.setItem(key, JSON.stringify(value)) + } + return [storedValue, setValue] +} -export default useLocalStorage; +export default useLocalStorage diff --git a/lib/hooks/use-scroll.ts b/lib/hooks/use-scroll.ts index 3c8014e..30b841a 100644 --- a/lib/hooks/use-scroll.ts +++ b/lib/hooks/use-scroll.ts @@ -1,16 +1,16 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from 'react' export default function useScroll(threshold: number) { - const [scrolled, setScrolled] = useState(false); + const [scrolled, setScrolled] = useState(false) const onScroll = useCallback(() => { - setScrolled(window.pageYOffset > threshold); - }, [threshold]); + setScrolled(window.pageYOffset > threshold) + }, [threshold]) useEffect(() => { - window.addEventListener("scroll", onScroll); - return () => window.removeEventListener("scroll", onScroll); - }, [onScroll]); + window.addEventListener('scroll', onScroll) + return () => window.removeEventListener('scroll', onScroll) + }, [onScroll]) - return scrolled; + return scrolled } diff --git a/lib/hooks/use-window-size.ts b/lib/hooks/use-window-size.ts index f7c03d3..5bb92ce 100644 --- a/lib/hooks/use-window-size.ts +++ b/lib/hooks/use-window-size.ts @@ -1,14 +1,14 @@ -"use client"; -import { useEffect, useState } from "react"; +'use client' +import { useEffect, useState } from 'react' export default function useWindowSize() { const [windowSize, setWindowSize] = useState<{ - width: number | undefined; - height: number | undefined; + width: number | undefined + height: number | undefined }>({ width: undefined, - height: undefined, - }); + height: undefined + }) useEffect(() => { // Handler to call on window resize @@ -16,24 +16,23 @@ export default function useWindowSize() { // Set window width/height to state setWindowSize({ width: window.innerWidth, - height: window.innerHeight, - }); + height: window.innerHeight + }) } // Add event listener - window.addEventListener("resize", handleResize); + window.addEventListener('resize', handleResize) // Call handler right away so state gets updated with initial window size - handleResize(); + handleResize() // Remove event listener on cleanup - return () => window.removeEventListener("resize", handleResize); - }, []); // Empty array ensures that effect is only run on mount + return () => window.removeEventListener('resize', handleResize) + }, []) // Empty array ensures that effect is only run on mount return { windowSize, - isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768, - isDesktop: - typeof windowSize?.width === "number" && windowSize?.width >= 768, - }; + isMobile: typeof windowSize?.width === 'number' && windowSize?.width < 768, + isDesktop: typeof windowSize?.width === 'number' && windowSize?.width >= 768 + } } diff --git a/lib/types.ts b/lib/types.ts index 93f2f1e..f7eb363 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,9 +1,9 @@ -import { type Message } from "ai-connector"; +import { type Message } from 'ai-connector' export interface Chat extends Record { - id: string; - title: string; - createdAt: Date; - userId: string; - messages: Message[]; + id: string + title: string + createdAt: Date + userId: string + messages: Message[] } diff --git a/lib/utils.ts b/lib/utils.ts index 3f10406..533bc28 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,34 +1,34 @@ -import { clsx, type ClassValue } from "clsx"; -import { customAlphabet } from "nanoid"; -import { twMerge } from "tailwind-merge"; +import { clsx, type ClassValue } from 'clsx' +import { customAlphabet } from 'nanoid' +import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)) } export const nanoid = customAlphabet( - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 7 -); // 7-character random string +) // 7-character random string export async function fetcher( input: RequestInfo, init?: RequestInit ): Promise { - const res = await fetch(input, init); + const res = await fetch(input, init) if (!res.ok) { - const json = await res.json(); + const json = await res.json() if (json.error) { const error = new Error(json.error) as Error & { - status: number; - }; - error.status = res.status; - throw error; + status: number + } + error.status = res.status + throw error } else { - throw new Error("An unexpected error occurred"); + throw new Error('An unexpected error occurred') } } - return res.json(); + return res.json() } diff --git a/package.json b/package.json index 3351d81..20544cd 100644 --- a/package.json +++ b/package.json @@ -61,5 +61,13 @@ "overrides": { "@auth/core": "0.0.0-manual.527fff6c" } + }, + "prettier": { + "tabWidth": 2, + "semi": false, + "useTabs": false, + "singleQuote": true, + "arrowParens": "avoid", + "trailingComma": "none" } }