Run prettier
This commit is contained in:
parent
aa83a871dd
commit
417f69e0f1
34 changed files with 530 additions and 523 deletions
|
|
@ -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]')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { GET, POST } from "@/auth";
|
||||
export const runtime = "edge";
|
||||
export { GET, POST } from '@/auth'
|
||||
export const runtime = 'edge'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
32
app/chat.tsx
32
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 (
|
||||
<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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
14
app/page.tsx
14
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 (
|
||||
<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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
lib/fonts.ts
24
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'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
setIsCopied(false)
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
return { isCopied, copyToClipboard };
|
||||
return { isCopied, copyToClipboard }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
lib/types.ts
12
lib/types.ts
|
|
@ -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[]
|
||||
}
|
||||
|
|
|
|||
28
lib/utils.ts
28
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<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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue