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 { 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<string>(`chat:${id}`, "userId");
const uid = await kv.hget<string>(`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]')
}

View file

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

View file

@ -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)
})

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 { 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) {
<div className="h-full w-full overflow-auto">
{messages.length > 0 ? (
<div className="group w-full text-zinc-900 dark:text-white divide-y dark:divide-zinc-800">
{messages.map((message) => (
{messages.map(message => (
<ChatMessage
key={message.id || message.content}
message={message}
@ -42,5 +42,5 @@ export function ChatList({ messages }: ChatList) {
)}
</div>
</div>
);
)
}

View file

@ -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) {
<div
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
)}
>
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-xl xl:max-w-3xl">
<div className="flex w-full gap-4 items-start">
{props.message.role === "user" ? (
{props.message.role === 'user' ? (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 p-2 select-none">
<div
className="font-medium uppercase text-zinc-100"
@ -42,53 +42,53 @@ export function ChatMessage(props: ChatMessageProps) {
components={{
code({ node, inline, className, children, ...props }) {
if (children.length) {
if (children[0] == "▍") {
if (children[0] == '▍') {
return (
<span className="mt-1 animate-pulse cursor-default">
</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 ? (
<CodeBlock
key={Math.random()}
language={(match && match[1]) || ""}
value={String(children).replace(/\n$/, "")}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
);
)
},
table({ children }) {
return (
<table className="border-collapse border border-black px-3 py-1 ">
{children}
</table>
);
)
},
th({ children }) {
return (
<th className="break-words border border-black bg-gray-500 px-3 py-1 text-white ">
{children}
</th>
);
)
},
td({ children }) {
return (
<td className="break-words border border-black px-3 py-1">
{children}
</td>
);
},
)
}
}}
>
{props.message.content}
@ -96,12 +96,12 @@ export function ChatMessage(props: ChatMessageProps) {
</div>
</div>
</div>
);
)
}
ChatMessage.displayName = "ChatMessage";
ChatMessage.displayName = 'ChatMessage'
export function IconOpenAI(props: JSX.IntrinsicElements["svg"]) {
export function IconOpenAI(props: JSX.IntrinsicElements['svg']) {
return (
<svg
fill="#000000"
@ -115,5 +115,5 @@ export function IconOpenAI(props: JSX.IntrinsicElements["svg"]) {
<title>OpenAI icon</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
);
)
}

View file

@ -1,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 (
<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 className="sticky light-gradient dark:bg-gradient-to-b dark:from-zinc-900 dark:to-zinc-950 bottom-0 left-0 w-full border-t bg-white dark:bg-black md:border-t-0 py-4 md:border-transparent md:!bg-transparent md:dark:border-transparent pr-0 lg:pr-[260px] flex dark:border-transparent items-center justify-center">
<Prompt
onSubmit={(value) => {
onSubmit={value => {
append({
content: value,
role: "user",
});
role: 'user'
})
}}
onRefresh={messages.length ? reload : undefined}
isLoading={isLoading}
/>
</div>
</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 { 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<Metadata> {
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 (
<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[]} />
</div>
</div>
);
)
}
ChatPage.displayName = "ChatPage";
ChatPage.displayName = 'ChatPage'
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) {
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
}

View file

@ -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 (
<div
className={cn(
"flex-1 flex items-start justify-between p-4 rounded-lg relative bg-zinc-100 text-sm font-medium transition-all cursor-pointer select-none hover:drop-shadow-[0_1px_1px_rgba(0,0,0,0.08)] hover:shadow-[0rem_0rem_0rem_0.0625rem_rgba(0,0,0,.02),0rem_0.125rem_0.25rem_rgba(0,0,0,.08),0rem_0.45rem_1rem_rgba(0,0,0,.06)] hover:bg-white group duration-300",
'flex-1 flex items-start justify-between p-4 rounded-lg relative bg-zinc-100 text-sm font-medium transition-all cursor-pointer select-none hover:drop-shadow-[0_1px_1px_rgba(0,0,0,0.08)] hover:shadow-[0rem_0rem_0rem_0.0625rem_rgba(0,0,0,.02),0rem_0.125rem_0.25rem_rgba(0,0,0,.08),0rem_0.45rem_1rem_rgba(0,0,0,.06)] hover:bg-white group duration-300',
fontMessage.className
)}
style={{}}
@ -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"
style={{
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>
<p>{children}</p>
</div>
);
)
}
export function EmptyScreen() {
return (
<div
className={cn(
"w-full h-full flex items-center justify-center pt-4 pb-8 pr-0 lg:pr-[260px]"
'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">
@ -35,8 +35,8 @@ export function EmptyScreen() {
<div className="text-zinc-500 font-medium">
<p>Welcome to Next.js Chatbot!</p>
<p className="mt-2">
This is an open source AI chatbot app built with{" "}
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{" "}
This is an open source AI chatbot app built with{' '}
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
<ExternalLink href="https://vercel.com/storage/postgres">
Vercel Postgres
</ExternalLink>
@ -52,5 +52,5 @@ export function EmptyScreen() {
</div>
</div>
</div>
);
)
}

View file

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

View file

@ -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) {
<head />
<body
className={cn(
"font-sans antialiased",
'font-sans antialiased',
fontSans.variable,
fontMono.variable
)}
@ -45,5 +45,5 @@ export default function RootLayout({ children }: RootLayoutProps) {
</body>
</html>
</>
);
)
}

View file

@ -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 (
<div className="relative flex h-full w-full overflow-hidden">
<Sidebar session={session} newChat />
@ -14,5 +14,5 @@ export default async function IndexPage() {
<Chat />
</div>
</div>
);
)
}

View file

@ -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 (
<form
onSubmit={async (e) => {
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({
</div>
</div>
</form>
);
)
}
Prompt.displayName = "Prompt";
Prompt.displayName = 'Prompt'

View file

@ -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 (
<Link
href={href}
className={cn(
"group flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium transition-colors duration-100 hover:bg-zinc-500/10 hover:dark:bg-zinc-300/20",
'group flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium transition-colors duration-100 hover:bg-zinc-500/10 hover:dark:bg-zinc-300/20',
isPending
? "text-zinc-400 dark:text-zinc-500"
: "text-zinc-800 dark:text-zinc-400",
? 'text-zinc-400 dark:text-zinc-500'
: 'text-zinc-800 dark:text-zinc-400',
active
? "bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white"
: "bg-transparent"
? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
: 'bg-transparent'
)}
>
<MessageCircleIcon className="h-4 w-4" />
@ -48,13 +48,13 @@ export function SidebarItem({
<button
className="opacity-0 -mr-6 transition-opacity duration-0 hover:bg-zinc-400/50 group-hover:mr-0 group-hover:opacity-80 p-1 -my-1 rounded group-hover:duration-600"
disabled={isPending}
onClick={(e) => {
e.preventDefault();
onClick={e => {
e.preventDefault()
startTransition(() => {
removeChat({ id, userId, path: href }).then(() => {
router.push("/");
});
});
router.push('/')
})
})
}}
>
{isPending ? (
@ -64,5 +64,5 @@ export function SidebarItem({
)}
</button>
</Link>
);
)
}

View file

@ -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) {
<Link
href="/"
className={cn(
"flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm text-zinc-800 dark:text-zinc-200 font-medium transition-colors duration-300 dark:hover:bg-zinc-300/20 select-none",
'flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm text-zinc-800 dark:text-zinc-200 font-medium transition-colors duration-300 dark:hover:bg-zinc-300/20 select-none',
newChat
? "bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white"
: "hover:bg-zinc-500/10"
? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
: 'hover:bg-zinc-500/10'
)}
>
<Plus className="h-4 w-4" />
@ -80,38 +80,38 @@ export function Sidebar({ session, newChat }: SidebarProps) {
</div>
</div>
</div>
);
)
}
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 => (
<SidebarItem
key={c.id}
title={c.title}
userId={session?.user?.email ?? ""}
userId={session?.user?.email ?? ''}
href={`/chat/${c.id}`}
id={c.id}
/>
));
))
}
async function getChats(userId: string) {
try {
const pipeline = kv.pipeline();
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1);
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 []
}
}

View file

@ -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<Options> = memo(
ReactMarkdown,
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.className === nextProps.className
);
)

View file

@ -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 <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 { 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 (
<Button
variant="ghost"
@ -16,18 +16,18 @@ export function ThemeToggle() {
className="p-0 leading-4 h-4 font-normal w-full"
onClick={() => {
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="">Toggle theme</span>
{!theme ? null : theme === "dark" ? (
{!theme ? null : theme === 'dark' ? (
<Moon className="h-4 transition-all" />
) : (
<Sun className="h-4 transition-all" />
)}
</span>
</Button>
);
)
}

View file

@ -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<HTMLButtonElement>,
@ -44,9 +44,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
ref={ref}
{...props}
/>
);
)
}
);
Button.displayName = "Button";
)
Button.displayName = 'Button'
export { Button, buttonVariants };
export { Button, buttonVariants }

View file

@ -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<Props> = 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 (
<div className="codeblock relative w-full font-sans text-[16px]">
<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)}
>
{isCopied ? <Check size={18} /> : <Clipboard size={18} />}
{isCopied ? "Copied!" : "Copy code"}
{isCopied ? 'Copied!' : 'Copy code'}
</button>
<button
type="button"
@ -107,21 +107,21 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
style={coldarkDark}
customStyle={{
margin: 0,
width: "100%",
background: "transparent",
width: '100%',
background: 'transparent'
}}
codeTagProps={{
style: {
fontSize: "0.9rem",
fontFamily: "var(--font-mono)",
},
fontSize: '0.9rem',
fontFamily: 'var(--font-mono)'
}
}}
>
{value}
</SyntaxHighlighter>
</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() {
const router = useRouter();
const router = useRouter()
return (
<button
className="inline-flex w-full items-center justify-center rounded border border-zinc-800 bg-white h-8 px-4 -my-1.5 text-sm leading-6 tracking-tight text-zinc-900 transition-colors ease-in-out hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => router.push("/api/auth/signin")}
onClick={() => router.push('/api/auth/signin')}
>
<span className="font-medium">Login</span>
</button>
);
)
}

View file

@ -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 (
<svg
width={17}
@ -22,10 +22,10 @@ export function NextChatLogo(
y2="14.2667"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={props.inverted ? "white" : "black"} />
<stop stopColor={props.inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={props.inverted ? "white" : "black"}
stopColor={props.inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
@ -37,35 +37,35 @@ export function NextChatLogo(
y2="9.50002"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={props.inverted ? "white" : "black"} />
<stop stopColor={props.inverted ? 'white' : 'black'} />
<stop
offset={1}
stopColor={props.inverted ? "white" : "black"}
stopColor={props.inverted ? 'white' : 'black'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<path
d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
fill={props.inverted ? "black" : "white"}
stroke={props.inverted ? "black" : "white"}
fill={props.inverted ? 'black' : 'white'}
stroke={props.inverted ? 'black' : 'white'}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<mask
id="mask0_91_2047"
style={{ maskType: "alpha" }}
style={{ maskType: 'alpha' }}
maskUnits="userSpaceOnUse"
x={1}
y={0}
width={16}
height={16}
>
<circle cx={9} cy={8} r={8} fill={props.inverted ? "black" : "white"} />
<circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
</mask>
<g mask="url(#mask0_91_2047)">
<circle cx={9} cy={8} r={8} fill={props.inverted ? "black" : "white"} />
<circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
<path
d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
fill={`url(#gradient-${id}-1)`}
@ -79,7 +79,7 @@ export function NextChatLogo(
/>
</g>
</svg>
);
)
}
NextChatLogo.displayName = "DevGPT-logo";
NextChatLogo.displayName = 'DevGPT-logo'

View file

@ -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 (
<div className="flex items-center justify-between">
<DropdownMenu.Root>
@ -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'}
/>
) : (
<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
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
</DropdownMenu.Item>
@ -92,7 +92,7 @@ export function UserMenu({ session }: UserMenuProps) {
</DropdownMenu.Portal>
</DropdownMenu.Root>
</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 }) => (
<svg
@ -7,8 +7,8 @@ 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')}
>
<path d="M117.082 0L234.164 202.794H0L117.082 0Z" fill="currentColor" />
</svg>
);
)

View file

@ -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)
}
}
}
}
},
};
};

View file

@ -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'
})

View file

@ -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<HTMLFormElement>;
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
formRef: RefObject<HTMLFormElement>
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
} {
const formRef = useRef<HTMLFormElement>(null);
const formRef = useRef<HTMLFormElement>(null)
const handleKeyDown = (
event: React.KeyboardEvent<HTMLTextAreaElement>
): void => {
if (event.key === "Enter") {
formRef.current?.requestSubmit();
if (event.key === 'Enter') {
formRef.current?.requestSubmit()
}
}
};
return { formRef, onKeyDown: handleKeyDown };
return { formRef, onKeyDown: handleKeyDown }
}

View file

@ -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<Boolean>(false);
const [isCopied, setIsCopied] = useState<Boolean>(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);
});
};
return { isCopied, copyToClipboard };
setIsCopied(false)
}, timeout)
})
}
return { isCopied, copyToClipboard }
}

View file

@ -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])
}

View file

@ -1,26 +1,26 @@
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react'
const useLocalStorage = <T>(
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

View file

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

View file

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

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> {
id: string;
title: string;
createdAt: Date;
userId: string;
messages: Message[];
id: string
title: string
createdAt: Date
userId: string
messages: Message[]
}

View file

@ -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<JSON = any>(
input: RequestInfo,
init?: RequestInit
): Promise<JSON> {
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()
}

View file

@ -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"
}
}