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 { kv } from '@vercel/kv'
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
export async function removeChat({
|
export async function removeChat({
|
||||||
id,
|
id,
|
||||||
path,
|
path,
|
||||||
userId,
|
userId
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string
|
||||||
userId: string;
|
userId: string
|
||||||
path: string;
|
path: string
|
||||||
}) {
|
}) {
|
||||||
// @todo next-auth@v5 doesn't work in server actions yet
|
// @todo next-auth@v5 doesn't work in server actions yet
|
||||||
// const session = await auth();
|
// const session = await auth();
|
||||||
|
|
||||||
const uid = await kv.hget<string>(`chat:${id}`, "userId");
|
const uid = await kv.hget<string>(`chat:${id}`, 'userId')
|
||||||
if (uid !== userId) {
|
if (uid !== userId) {
|
||||||
throw new Error("Unauthorized");
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
await kv.del(`chat:${id}`);
|
await kv.del(`chat:${id}`)
|
||||||
await kv.zrem(`user:chat:${userId}`, `chat:${id}`);
|
await kv.zrem(`user:chat:${userId}`, `chat:${id}`)
|
||||||
|
|
||||||
revalidatePath("/");
|
revalidatePath('/')
|
||||||
revalidatePath("/chat/[id]");
|
revalidatePath('/chat/[id]')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export { GET, POST } from "@/auth";
|
export { GET, POST } from '@/auth'
|
||||||
export const runtime = "edge";
|
export const runtime = 'edge'
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
import { auth } from "@/auth";
|
import { auth } from '@/auth'
|
||||||
import { kv } from "@vercel/kv";
|
import { kv } from '@vercel/kv'
|
||||||
import { nanoid } from "@/lib/utils";
|
import { nanoid } from '@/lib/utils'
|
||||||
import { OpenAIStream, StreamingTextResponse } from "ai-connector";
|
import { OpenAIStream, StreamingTextResponse } from 'ai-connector'
|
||||||
import { Configuration, OpenAIApi } from "openai-edge";
|
import { Configuration, OpenAIApi } from 'openai-edge'
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = 'edge'
|
||||||
|
|
||||||
const configuration = new Configuration({
|
const configuration = new Configuration({
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
apiKey: process.env.OPENAI_API_KEY
|
||||||
});
|
})
|
||||||
|
|
||||||
const openai = new OpenAIApi(configuration);
|
const openai = new OpenAIApi(configuration)
|
||||||
|
|
||||||
if (!process.env.OPENAI_API_KEY) {
|
if (!process.env.OPENAI_API_KEY) {
|
||||||
throw new Error("Missing env var from OpenAI");
|
throw new Error('Missing env var from OpenAI')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST = auth(async function POST(req: Request) {
|
export const POST = auth(async function POST(req: Request) {
|
||||||
const json = await req.json();
|
const json = await req.json()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
console.log(req.auth); // todo fix types
|
console.log(req.auth) // todo fix types
|
||||||
const messages = json.messages.map((m: any) => ({
|
const messages = json.messages.map((m: any) => ({
|
||||||
content: m.content,
|
content: m.content,
|
||||||
role: m.role,
|
role: m.role
|
||||||
}));
|
}))
|
||||||
const res = await openai.createChatCompletion({
|
const res = await openai.createChatCompletion({
|
||||||
model: "gpt-3.5-turbo",
|
model: 'gpt-3.5-turbo',
|
||||||
messages,
|
messages,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
top_p: 1,
|
top_p: 1,
|
||||||
frequency_penalty: 1,
|
frequency_penalty: 1,
|
||||||
max_tokens: 500,
|
max_tokens: 500,
|
||||||
n: 1,
|
n: 1,
|
||||||
stream: true,
|
stream: true
|
||||||
});
|
})
|
||||||
|
|
||||||
const stream = await OpenAIStream(res, {
|
const stream = await OpenAIStream(res, {
|
||||||
async onCompletion(completion) {
|
async onCompletion(completion) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (req.auth?.user?.email == null) {
|
if (req.auth?.user?.email == null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const title = json.messages[0].content.substring(0, 20);
|
const title = json.messages[0].content.substring(0, 20)
|
||||||
const userId = (req as any).auth?.user?.email;
|
const userId = (req as any).auth?.user?.email
|
||||||
const id = json.id ?? nanoid();
|
const id = json.id ?? nanoid()
|
||||||
const createdAt = Date.now();
|
const createdAt = Date.now()
|
||||||
const payload = {
|
const payload = {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
|
@ -54,17 +54,17 @@ export const POST = auth(async function POST(req: Request) {
|
||||||
...messages,
|
...messages,
|
||||||
{
|
{
|
||||||
content: completion,
|
content: completion,
|
||||||
role: "assistant",
|
role: 'assistant'
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
await kv.hmset(`chat:${id}`, payload);
|
await kv.hmset(`chat:${id}`, payload)
|
||||||
await kv.zadd(`user:chat:${userId}`, {
|
await kv.zadd(`user:chat:${userId}`, {
|
||||||
score: createdAt,
|
score: createdAt,
|
||||||
member: `chat:${id}`,
|
member: `chat:${id}`
|
||||||
});
|
})
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return new StreamingTextResponse(stream);
|
return new StreamingTextResponse(stream)
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import { type Message } from "ai-connector";
|
import { type Message } from 'ai-connector'
|
||||||
|
|
||||||
import { ChatMessage } from "./chat-message";
|
import { ChatMessage } from './chat-message'
|
||||||
import { NextChatLogo } from "@/components/ui/nextchat-logo";
|
import { NextChatLogo } from '@/components/ui/nextchat-logo'
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from 'lucide-react'
|
||||||
import { EmptyScreen } from "./empty";
|
import { EmptyScreen } from './empty'
|
||||||
|
|
||||||
export interface ChatList {
|
export interface ChatList {
|
||||||
messages: Message[];
|
messages: Message[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatList({ messages }: ChatList) {
|
export function ChatList({ messages }: ChatList) {
|
||||||
|
|
@ -30,7 +30,7 @@ export function ChatList({ messages }: ChatList) {
|
||||||
<div className="h-full w-full overflow-auto">
|
<div className="h-full w-full overflow-auto">
|
||||||
{messages.length > 0 ? (
|
{messages.length > 0 ? (
|
||||||
<div className="group w-full text-zinc-900 dark:text-white divide-y dark:divide-zinc-800">
|
<div className="group w-full text-zinc-900 dark:text-white divide-y dark:divide-zinc-800">
|
||||||
{messages.map((message) => (
|
{messages.map(message => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={message.id || message.content}
|
key={message.id || message.content}
|
||||||
message={message}
|
message={message}
|
||||||
|
|
@ -42,5 +42,5 @@ export function ChatList({ messages }: ChatList) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import CodeBlock from "@/components/ui/codeblock";
|
import CodeBlock from '@/components/ui/codeblock'
|
||||||
import { MemoizedReactMarkdown } from "@/components/markdown";
|
import { MemoizedReactMarkdown } from '@/components/markdown'
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from "remark-math";
|
import remarkMath from 'remark-math'
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
import { fontMessage } from "@/lib/fonts";
|
import { fontMessage } from '@/lib/fonts'
|
||||||
import { Message } from "ai-connector";
|
import { Message } from 'ai-connector'
|
||||||
export interface ChatMessageProps {
|
export interface ChatMessageProps {
|
||||||
message: Message;
|
message: Message
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessage(props: ChatMessageProps) {
|
export function ChatMessage(props: ChatMessageProps) {
|
||||||
|
|
@ -14,15 +14,15 @@ export function ChatMessage(props: ChatMessageProps) {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
{
|
{
|
||||||
"bg-zinc-50": props.message.role === "assistant",
|
'bg-zinc-50': props.message.role === 'assistant'
|
||||||
},
|
},
|
||||||
"pr-0 lg:pr-[260px]",
|
'pr-0 lg:pr-[260px]',
|
||||||
fontMessage.className
|
fontMessage.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-xl xl:max-w-3xl">
|
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-xl xl:max-w-3xl">
|
||||||
<div className="flex w-full gap-4 items-start">
|
<div className="flex w-full gap-4 items-start">
|
||||||
{props.message.role === "user" ? (
|
{props.message.role === 'user' ? (
|
||||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 p-2 select-none">
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 p-2 select-none">
|
||||||
<div
|
<div
|
||||||
className="font-medium uppercase text-zinc-100"
|
className="font-medium uppercase text-zinc-100"
|
||||||
|
|
@ -42,53 +42,53 @@ export function ChatMessage(props: ChatMessageProps) {
|
||||||
components={{
|
components={{
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
if (children[0] == "▍") {
|
if (children[0] == '▍') {
|
||||||
return (
|
return (
|
||||||
<span className="mt-1 animate-pulse cursor-default">
|
<span className="mt-1 animate-pulse cursor-default">
|
||||||
▍
|
▍
|
||||||
</span>
|
</span>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
children[0] = (children[0] as string).replace("`▍`", "▍");
|
children[0] = (children[0] as string).replace('`▍`', '▍')
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
const match = /language-(\w+)/.exec(className || '')
|
||||||
|
|
||||||
return !inline ? (
|
return !inline ? (
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
key={Math.random()}
|
key={Math.random()}
|
||||||
language={(match && match[1]) || ""}
|
language={(match && match[1]) || ''}
|
||||||
value={String(children).replace(/\n$/, "")}
|
value={String(children).replace(/\n$/, '')}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<code className={className} {...props}>
|
<code className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
table({ children }) {
|
table({ children }) {
|
||||||
return (
|
return (
|
||||||
<table className="border-collapse border border-black px-3 py-1 ">
|
<table className="border-collapse border border-black px-3 py-1 ">
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
th({ children }) {
|
th({ children }) {
|
||||||
return (
|
return (
|
||||||
<th className="break-words border border-black bg-gray-500 px-3 py-1 text-white ">
|
<th className="break-words border border-black bg-gray-500 px-3 py-1 text-white ">
|
||||||
{children}
|
{children}
|
||||||
</th>
|
</th>
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
td({ children }) {
|
td({ children }) {
|
||||||
return (
|
return (
|
||||||
<td className="break-words border border-black px-3 py-1">
|
<td className="break-words border border-black px-3 py-1">
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
);
|
)
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.message.content}
|
{props.message.content}
|
||||||
|
|
@ -96,12 +96,12 @@ export function ChatMessage(props: ChatMessageProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatMessage.displayName = "ChatMessage";
|
ChatMessage.displayName = 'ChatMessage'
|
||||||
|
|
||||||
export function IconOpenAI(props: JSX.IntrinsicElements["svg"]) {
|
export function IconOpenAI(props: JSX.IntrinsicElements['svg']) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
fill="#000000"
|
fill="#000000"
|
||||||
|
|
@ -115,5 +115,5 @@ export function IconOpenAI(props: JSX.IntrinsicElements["svg"]) {
|
||||||
<title>OpenAI icon</title>
|
<title>OpenAI icon</title>
|
||||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
app/chat.tsx
32
app/chat.tsx
|
|
@ -1,30 +1,30 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from 'next/navigation'
|
||||||
import { Prompt } from "./prompt";
|
import { Prompt } from './prompt'
|
||||||
import { useChat, type Message } from "ai-connector";
|
import { useChat, type Message } from 'ai-connector'
|
||||||
import { ChatList } from "./chat-list";
|
import { ChatList } from './chat-list'
|
||||||
|
|
||||||
export interface ChatProps {
|
export interface ChatProps {
|
||||||
// create?: (input: string) => Chat | undefined;
|
// create?: (input: string) => Chat | undefined;
|
||||||
initialMessages?: Message[];
|
initialMessages?: Message[]
|
||||||
id?: string;
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat({
|
export function Chat({
|
||||||
id,
|
id,
|
||||||
// create,
|
// create,
|
||||||
initialMessages,
|
initialMessages
|
||||||
}: ChatProps) {
|
}: ChatProps) {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const { isLoading, messages, reload, append } = useChat({
|
const { isLoading, messages, reload, append } = useChat({
|
||||||
initialMessages: initialMessages as any[],
|
initialMessages: initialMessages as any[],
|
||||||
id,
|
id
|
||||||
// onCreate: (id: string) => {
|
// onCreate: (id: string) => {
|
||||||
// router.push(`/chat/${id}`);
|
// router.push(`/chat/${id}`);
|
||||||
// },
|
// },
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="transition-width relative min-h-full w-full flex-1 overflow-y-auto flex flex-col">
|
<main className="transition-width relative min-h-full w-full flex-1 overflow-y-auto flex flex-col">
|
||||||
|
|
@ -33,18 +33,18 @@ export function Chat({
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky light-gradient dark:bg-gradient-to-b dark:from-zinc-900 dark:to-zinc-950 bottom-0 left-0 w-full border-t bg-white dark:bg-black md:border-t-0 py-4 md:border-transparent md:!bg-transparent md:dark:border-transparent pr-0 lg:pr-[260px] flex dark:border-transparent items-center justify-center">
|
<div className="sticky light-gradient dark:bg-gradient-to-b dark:from-zinc-900 dark:to-zinc-950 bottom-0 left-0 w-full border-t bg-white dark:bg-black md:border-t-0 py-4 md:border-transparent md:!bg-transparent md:dark:border-transparent pr-0 lg:pr-[260px] flex dark:border-transparent items-center justify-center">
|
||||||
<Prompt
|
<Prompt
|
||||||
onSubmit={(value) => {
|
onSubmit={value => {
|
||||||
append({
|
append({
|
||||||
content: value,
|
content: value,
|
||||||
role: "user",
|
role: 'user'
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
onRefresh={messages.length ? reload : undefined}
|
onRefresh={messages.length ? reload : undefined}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Chat.displayName = "Chat";
|
Chat.displayName = 'Chat'
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
import { Sidebar } from "@/app/sidebar";
|
import { Sidebar } from '@/app/sidebar'
|
||||||
|
|
||||||
import { auth } from "@/auth";
|
import { auth } from '@/auth'
|
||||||
import { type Metadata } from "next";
|
import { type Metadata } from 'next'
|
||||||
|
|
||||||
import { Chat } from "@/app/chat";
|
import { Chat } from '@/app/chat'
|
||||||
import { type Chat as ChatType } from "@/lib/types";
|
import { type Chat as ChatType } from '@/lib/types'
|
||||||
import { kv } from "@vercel/kv";
|
import { kv } from '@vercel/kv'
|
||||||
import { Message } from "ai-connector";
|
import { Message } from 'ai-connector'
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = 'edge'
|
||||||
export const preferredRegion = "home";
|
export const preferredRegion = 'home'
|
||||||
export interface ChatPageProps {
|
export interface ChatPageProps {
|
||||||
params: {
|
params: {
|
||||||
id: string;
|
id: string
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params
|
||||||
}: ChatPageProps): Promise<Metadata> {
|
}: ChatPageProps): Promise<Metadata> {
|
||||||
const session = await auth();
|
const session = await auth()
|
||||||
const chat = await getChat(params.id, session?.user?.email ?? "");
|
const chat = await getChat(params.id, session?.user?.email ?? '')
|
||||||
return {
|
return {
|
||||||
title: chat?.title.slice(0, 50) ?? "Chat",
|
title: chat?.title.slice(0, 50) ?? 'Chat'
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ChatPage({ params }: ChatPageProps) {
|
export default async function ChatPage({ params }: ChatPageProps) {
|
||||||
const session = await auth();
|
const session = await auth()
|
||||||
const chat = await getChat(params.id, session?.user?.email ?? "");
|
const chat = await getChat(params.id, session?.user?.email ?? '')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
|
|
@ -37,20 +37,20 @@ export default async function ChatPage({ params }: ChatPageProps) {
|
||||||
<Chat id={chat.id} initialMessages={chat.messages as Message[]} />
|
<Chat id={chat.id} initialMessages={chat.messages as Message[]} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatPage.displayName = "ChatPage";
|
ChatPage.displayName = 'ChatPage'
|
||||||
|
|
||||||
async function getChat(id: string, userId: string) {
|
async function getChat(id: string, userId: string) {
|
||||||
const chat = await kv.hgetall<ChatType>(`chat:${id}`);
|
const chat = await kv.hgetall<ChatType>(`chat:${id}`)
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
throw new Error("Not found");
|
throw new Error('Not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId && chat.userId !== userId) {
|
if (userId && chat.userId !== userId) {
|
||||||
throw new Error("Unauthorized");
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
return chat;
|
return chat
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
import { ExternalLink } from "./external-link";
|
import { ExternalLink } from './external-link'
|
||||||
import { fontMessage } from "@/lib/fonts";
|
import { fontMessage } from '@/lib/fonts'
|
||||||
|
|
||||||
function ExampleBubble({ children }: { children?: React.ReactNode }) {
|
function ExampleBubble({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 flex items-start justify-between p-4 rounded-lg relative bg-zinc-100 text-sm font-medium transition-all cursor-pointer select-none hover:drop-shadow-[0_1px_1px_rgba(0,0,0,0.08)] hover:shadow-[0rem_0rem_0rem_0.0625rem_rgba(0,0,0,.02),0rem_0.125rem_0.25rem_rgba(0,0,0,.08),0rem_0.45rem_1rem_rgba(0,0,0,.06)] hover:bg-white group duration-300",
|
'flex-1 flex items-start justify-between p-4 rounded-lg relative bg-zinc-100 text-sm font-medium transition-all cursor-pointer select-none hover:drop-shadow-[0_1px_1px_rgba(0,0,0,0.08)] hover:shadow-[0rem_0rem_0rem_0.0625rem_rgba(0,0,0,.02),0rem_0.125rem_0.25rem_rgba(0,0,0,.08),0rem_0.45rem_1rem_rgba(0,0,0,.06)] hover:bg-white group duration-300',
|
||||||
fontMessage.className
|
fontMessage.className
|
||||||
)}
|
)}
|
||||||
style={{}}
|
style={{}}
|
||||||
|
|
@ -15,19 +15,19 @@ function ExampleBubble({ children }: { children?: React.ReactNode }) {
|
||||||
className="bg-zinc-100 w-9 h-6 absolute left-0 top-full -mt-[1px] scale-75 origin-top-left group-hover:bg-white transition-all duration-300"
|
className="bg-zinc-100 w-9 h-6 absolute left-0 top-full -mt-[1px] scale-75 origin-top-left group-hover:bg-white transition-all duration-300"
|
||||||
style={{
|
style={{
|
||||||
clipPath:
|
clipPath:
|
||||||
'path("M31.1838 0C24.9593 10.7604 13.3251 18 0 18C9.94113 18 18 9.94113 18 0H31.1838Z")',
|
'path("M31.1838 0C24.9593 10.7604 13.3251 18 0 18C9.94113 18 18 9.94113 18 0H31.1838Z")'
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<p>{children}</p>
|
<p>{children}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmptyScreen() {
|
export function EmptyScreen() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full h-full flex items-center justify-center pt-4 pb-8 pr-0 lg:pr-[260px]"
|
'w-full h-full flex items-center justify-center pt-4 pb-8 pr-0 lg:pr-[260px]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-8 rounded-lg flex flex-col items-center justify-center gap-8 max-w-2xl">
|
<div className="p-8 rounded-lg flex flex-col items-center justify-center gap-8 max-w-2xl">
|
||||||
|
|
@ -35,8 +35,8 @@ export function EmptyScreen() {
|
||||||
<div className="text-zinc-500 font-medium">
|
<div className="text-zinc-500 font-medium">
|
||||||
<p>Welcome to Next.js Chatbot!</p>
|
<p>Welcome to Next.js Chatbot!</p>
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
This is an open source AI chatbot app built with{" "}
|
This is an open source AI chatbot app built with{' '}
|
||||||
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{" "}
|
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
||||||
<ExternalLink href="https://vercel.com/storage/postgres">
|
<ExternalLink href="https://vercel.com/storage/postgres">
|
||||||
Vercel Postgres
|
Vercel Postgres
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
|
|
@ -52,5 +52,5 @@ export function EmptyScreen() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
export function ExternalLink({
|
export function ExternalLink({
|
||||||
href,
|
href,
|
||||||
children,
|
children
|
||||||
}: {
|
}: {
|
||||||
href: string;
|
href: string
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
|
@ -25,5 +25,5 @@ export function ExternalLink({
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
import "./globals.css";
|
import './globals.css'
|
||||||
import { Metadata } from "next";
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from '@/components/theme-provider'
|
||||||
import { fontMono, fontSans } from "@/lib/fonts";
|
import { fontMono, fontSans } from '@/lib/fonts'
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
default: "Next.js Chatbot",
|
default: 'Next.js Chatbot',
|
||||||
template: `%s - Next.js Chatbot`,
|
template: `%s - Next.js Chatbot`
|
||||||
},
|
},
|
||||||
description: "An AI-powered chatbot built with Next.js and Vercel.",
|
description: 'An AI-powered chatbot built with Next.js and Vercel.',
|
||||||
themeColor: [
|
themeColor: [
|
||||||
{ media: "(prefers-color-scheme: light)", color: "white" },
|
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
||||||
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
{ media: '(prefers-color-scheme: dark)', color: 'black' }
|
||||||
],
|
],
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/favicon.ico",
|
icon: '/favicon.ico',
|
||||||
shortcut: "/favicon-16x16.png",
|
shortcut: '/favicon-16x16.png',
|
||||||
apple: "/apple-touch-icon.png",
|
apple: '/apple-touch-icon.png'
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
interface RootLayoutProps {
|
interface RootLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({ children }: RootLayoutProps) {
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
|
@ -33,7 +33,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
<head />
|
<head />
|
||||||
<body
|
<body
|
||||||
className={cn(
|
className={cn(
|
||||||
"font-sans antialiased",
|
'font-sans antialiased',
|
||||||
fontSans.variable,
|
fontSans.variable,
|
||||||
fontMono.variable
|
fontMono.variable
|
||||||
)}
|
)}
|
||||||
|
|
@ -45,5 +45,5 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
app/page.tsx
14
app/page.tsx
|
|
@ -1,12 +1,12 @@
|
||||||
import { Chat } from "./chat";
|
import { Chat } from './chat'
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from './sidebar'
|
||||||
import { auth } from "@/auth";
|
import { auth } from '@/auth'
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = 'edge'
|
||||||
export const preferredRegion = "home";
|
export const preferredRegion = 'home'
|
||||||
|
|
||||||
export default async function IndexPage() {
|
export default async function IndexPage() {
|
||||||
const session = await auth();
|
const session = await auth()
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
<Sidebar session={session} newChat />
|
<Sidebar session={session} newChat />
|
||||||
|
|
@ -14,5 +14,5 @@ export default async function IndexPage() {
|
||||||
<Chat />
|
<Chat />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import { CornerDownLeft, RefreshCcw, StopCircle } from "lucide-react";
|
import { CornerDownLeft, RefreshCcw, StopCircle } from 'lucide-react'
|
||||||
import { useState } from "react";
|
import { useState } from 'react'
|
||||||
import Textarea from "react-textarea-autosize";
|
import Textarea from 'react-textarea-autosize'
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from '@/components/ui/button'
|
||||||
import { fontMessage } from "@/lib/fonts";
|
import { fontMessage } from '@/lib/fonts'
|
||||||
import { useCmdEnterSubmit } from "@/lib/hooks/use-command-enter-submit";
|
import { useCmdEnterSubmit } from '@/lib/hooks/use-command-enter-submit'
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export interface PromptProps {
|
export interface PromptProps {
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void
|
||||||
onAbort?: () => void;
|
onAbort?: () => void
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Prompt({
|
export function Prompt({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onAbort,
|
onAbort,
|
||||||
isLoading,
|
isLoading
|
||||||
}: PromptProps) {
|
}: PromptProps) {
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState('')
|
||||||
const { formRef, onKeyDown } = useCmdEnterSubmit();
|
const { formRef, onKeyDown } = useCmdEnterSubmit()
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={async (e) => {
|
onSubmit={async e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setInput("");
|
setInput('')
|
||||||
await onSubmit(input);
|
await onSubmit(input)
|
||||||
}}
|
}}
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
className="stretch flex w-full flex-row gap-3 md:max-w-2xl lg:max-w-xl xl:max-w-3xl mx-auto px-4"
|
className="stretch flex w-full flex-row gap-3 md:max-w-2xl lg:max-w-xl xl:max-w-3xl mx-auto px-4"
|
||||||
|
|
@ -77,16 +77,16 @@ export function Prompt({
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
rows={1}
|
rows={1}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={e => setInput(e.target.value)}
|
||||||
placeholder="Send a message."
|
placeholder="Send a message."
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className={cn(
|
className={cn(
|
||||||
"m-0 max-h-[200px] w-full resize-none border-0 bg-transparent p-0 py-[13px] pl-4 pr-7 outline-none ring-0 focus:ring-0 focus-visible:ring-0 dark:bg-transparent",
|
'm-0 max-h-[200px] w-full resize-none border-0 bg-transparent p-0 py-[13px] pl-4 pr-7 outline-none ring-0 focus:ring-0 focus-visible:ring-0 dark:bg-transparent',
|
||||||
fontMessage.className
|
fontMessage.className
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: 46,
|
height: 46,
|
||||||
overflowY: "hidden",
|
overflowY: 'hidden'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
@ -108,7 +108,7 @@ export function Prompt({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Prompt.displayName = "Prompt";
|
Prompt.displayName = 'Prompt'
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
import { MessageCircleIcon, Trash2Icon, Loader2Icon } from "lucide-react";
|
import { MessageCircleIcon, Trash2Icon, Loader2Icon } from 'lucide-react'
|
||||||
import { removeChat } from "./actions";
|
import { removeChat } from './actions'
|
||||||
import { useTransition } from "react";
|
import { useTransition } from 'react'
|
||||||
|
|
||||||
export function SidebarItem({
|
export function SidebarItem({
|
||||||
title,
|
title,
|
||||||
href,
|
href,
|
||||||
id,
|
id,
|
||||||
userId,
|
userId
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string
|
||||||
href: string;
|
href: string
|
||||||
id: string;
|
id: string
|
||||||
userId: string;
|
userId: string
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const active = pathname === href;
|
const active = pathname === href
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
if (!id) return null;
|
if (!id) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium transition-colors duration-100 hover:bg-zinc-500/10 hover:dark:bg-zinc-300/20",
|
'group flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm font-medium transition-colors duration-100 hover:bg-zinc-500/10 hover:dark:bg-zinc-300/20',
|
||||||
isPending
|
isPending
|
||||||
? "text-zinc-400 dark:text-zinc-500"
|
? 'text-zinc-400 dark:text-zinc-500'
|
||||||
: "text-zinc-800 dark:text-zinc-400",
|
: 'text-zinc-800 dark:text-zinc-400',
|
||||||
active
|
active
|
||||||
? "bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white"
|
? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
|
||||||
: "bg-transparent"
|
: 'bg-transparent'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageCircleIcon className="h-4 w-4" />
|
<MessageCircleIcon className="h-4 w-4" />
|
||||||
|
|
@ -48,13 +48,13 @@ export function SidebarItem({
|
||||||
<button
|
<button
|
||||||
className="opacity-0 -mr-6 transition-opacity duration-0 hover:bg-zinc-400/50 group-hover:mr-0 group-hover:opacity-80 p-1 -my-1 rounded group-hover:duration-600"
|
className="opacity-0 -mr-6 transition-opacity duration-0 hover:bg-zinc-400/50 group-hover:mr-0 group-hover:opacity-80 p-1 -my-1 rounded group-hover:duration-600"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
removeChat({ id, userId, path: href }).then(() => {
|
removeChat({ id, userId, path: href }).then(() => {
|
||||||
router.push("/");
|
router.push('/')
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
|
|
@ -64,5 +64,5 @@ export function SidebarItem({
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { Login } from "@/components/ui/login";
|
import { Login } from '@/components/ui/login'
|
||||||
import { NextChatLogo } from "@/components/ui/nextchat-logo";
|
import { NextChatLogo } from '@/components/ui/nextchat-logo'
|
||||||
import { UserMenu } from "@/components/ui/user-menu";
|
import { UserMenu } from '@/components/ui/user-menu'
|
||||||
import { kv } from "@vercel/kv";
|
import { kv } from '@vercel/kv'
|
||||||
import { Chat } from "@/lib/types";
|
import { Chat } from '@/lib/types'
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
import { type Session } from "@auth/nextjs/types";
|
import { type Session } from '@auth/nextjs/types'
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from 'lucide-react'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import { ExternalLink } from "./external-link";
|
import { ExternalLink } from './external-link'
|
||||||
import { SidebarItem } from "./sidebar-item";
|
import { SidebarItem } from './sidebar-item'
|
||||||
|
|
||||||
export interface SidebarProps {
|
export interface SidebarProps {
|
||||||
session?: Session;
|
session?: Session
|
||||||
newChat?: boolean;
|
newChat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ session, newChat }: SidebarProps) {
|
export function Sidebar({ session, newChat }: SidebarProps) {
|
||||||
|
|
@ -33,10 +33,10 @@ export function Sidebar({ session, newChat }: SidebarProps) {
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm text-zinc-800 dark:text-zinc-200 font-medium transition-colors duration-300 dark:hover:bg-zinc-300/20 select-none",
|
'flex shrink-0 cursor-pointer items-center gap-2 rounded p-2 text-sm text-zinc-800 dark:text-zinc-200 font-medium transition-colors duration-300 dark:hover:bg-zinc-300/20 select-none',
|
||||||
newChat
|
newChat
|
||||||
? "bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white"
|
? 'bg-zinc-500/20 text-zinc-900 font-semibold hover:bg-zinc-500/30 dark:text-white'
|
||||||
: "hover:bg-zinc-500/10"
|
: 'hover:bg-zinc-500/10'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
|
|
@ -80,38 +80,38 @@ export function Sidebar({ session, newChat }: SidebarProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Sidebar.displayName = "Sidebar";
|
Sidebar.displayName = 'Sidebar'
|
||||||
|
|
||||||
async function SidebarList({ session }: { session?: Session }) {
|
async function SidebarList({ session }: { session?: Session }) {
|
||||||
const results: Chat[] = await getChats(session?.user?.email ?? "");
|
const results: Chat[] = await getChats(session?.user?.email ?? '')
|
||||||
|
|
||||||
return results.map((c) => (
|
return results.map(c => (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
key={c.id}
|
key={c.id}
|
||||||
title={c.title}
|
title={c.title}
|
||||||
userId={session?.user?.email ?? ""}
|
userId={session?.user?.email ?? ''}
|
||||||
href={`/chat/${c.id}`}
|
href={`/chat/${c.id}`}
|
||||||
id={c.id}
|
id={c.id}
|
||||||
/>
|
/>
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getChats(userId: string) {
|
async function getChats(userId: string) {
|
||||||
try {
|
try {
|
||||||
const pipeline = kv.pipeline();
|
const pipeline = kv.pipeline()
|
||||||
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1);
|
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1)
|
||||||
|
|
||||||
for (const chat of chats) {
|
for (const chat of chats) {
|
||||||
pipeline.hgetall(chat);
|
pipeline.hgetall(chat)
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await pipeline.exec();
|
const results = await pipeline.exec()
|
||||||
|
|
||||||
return results as Chat[];
|
return results as Chat[]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { FC, memo } from "react";
|
import { FC, memo } from 'react'
|
||||||
import ReactMarkdown, { Options } from "react-markdown";
|
import ReactMarkdown, { Options } from 'react-markdown'
|
||||||
|
|
||||||
export const MemoizedReactMarkdown: FC<Options> = memo(
|
export const MemoizedReactMarkdown: FC<Options> = memo(
|
||||||
ReactMarkdown,
|
ReactMarkdown,
|
||||||
(prevProps, nextProps) =>
|
(prevProps, nextProps) =>
|
||||||
prevProps.children === nextProps.children &&
|
prevProps.children === nextProps.children &&
|
||||||
prevProps.className === nextProps.className
|
prevProps.className === nextProps.className
|
||||||
);
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from 'react'
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
import { ThemeProviderProps } from 'next-themes/dist/types'
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from 'next-themes'
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from '@/components/ui/button'
|
||||||
import { Moon, Sun } from "lucide-react";
|
import { Moon, Sun } from 'lucide-react'
|
||||||
import { useTransition } from "react";
|
import { useTransition } from 'react'
|
||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme } = useTheme()
|
||||||
const [_, startTransition] = useTransition();
|
const [_, startTransition] = useTransition()
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -16,18 +16,18 @@ export function ThemeToggle() {
|
||||||
className="p-0 leading-4 h-4 font-normal w-full"
|
className="p-0 leading-4 h-4 font-normal w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
setTheme(theme === "light" ? "dark" : "light");
|
setTheme(theme === 'light' ? 'dark' : 'light')
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex flex-row justify-between text-xs text-zinc-900 dark:text-zinc-400 w-full">
|
<span className="flex flex-row justify-between text-xs text-zinc-900 dark:text-zinc-400 w-full">
|
||||||
<span className="">Toggle theme</span>
|
<span className="">Toggle theme</span>
|
||||||
{!theme ? null : theme === "dark" ? (
|
{!theme ? null : theme === 'dark' ? (
|
||||||
<Moon className="h-4 transition-all" />
|
<Moon className="h-4 transition-all" />
|
||||||
) : (
|
) : (
|
||||||
<Sun className="h-4 transition-all" />
|
<Sun className="h-4 transition-all" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,36 @@
|
||||||
import * as React from "react";
|
import * as React from 'react'
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
import { VariantProps, cva } from 'class-variance-authority'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
|
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
outline:
|
outline:
|
||||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
'border border-input hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
link: "underline-offset-4 hover:underline text-primary",
|
link: 'underline-offset-4 hover:underline text-primary'
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 py-2 px-4",
|
default: 'h-10 py-2 px-4',
|
||||||
sm: "h-9 px-3 rounded-md",
|
sm: 'h-9 px-3 rounded-md',
|
||||||
lg: "h-11 px-8 rounded-md",
|
lg: 'h-11 px-8 rounded-md',
|
||||||
xs: "h-6 px-3 rounded-md",
|
xs: 'h-6 px-3 rounded-md'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default'
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
|
@ -44,9 +44,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
Button.displayName = "Button";
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button, buttonVariants }
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,83 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-cliipboard";
|
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-cliipboard'
|
||||||
import { Check, Clipboard, Download } from "lucide-react";
|
import { Check, Clipboard, Download } from 'lucide-react'
|
||||||
import { FC, memo } from "react";
|
import { FC, memo } from 'react'
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
language: string;
|
language: string
|
||||||
value: string;
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface languageMap {
|
interface languageMap {
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const programmingLanguages: languageMap = {
|
export const programmingLanguages: languageMap = {
|
||||||
javascript: ".js",
|
javascript: '.js',
|
||||||
python: ".py",
|
python: '.py',
|
||||||
java: ".java",
|
java: '.java',
|
||||||
c: ".c",
|
c: '.c',
|
||||||
cpp: ".cpp",
|
cpp: '.cpp',
|
||||||
"c++": ".cpp",
|
'c++': '.cpp',
|
||||||
"c#": ".cs",
|
'c#': '.cs',
|
||||||
ruby: ".rb",
|
ruby: '.rb',
|
||||||
php: ".php",
|
php: '.php',
|
||||||
swift: ".swift",
|
swift: '.swift',
|
||||||
"objective-c": ".m",
|
'objective-c': '.m',
|
||||||
kotlin: ".kt",
|
kotlin: '.kt',
|
||||||
typescript: ".ts",
|
typescript: '.ts',
|
||||||
go: ".go",
|
go: '.go',
|
||||||
perl: ".pl",
|
perl: '.pl',
|
||||||
rust: ".rs",
|
rust: '.rs',
|
||||||
scala: ".scala",
|
scala: '.scala',
|
||||||
haskell: ".hs",
|
haskell: '.hs',
|
||||||
lua: ".lua",
|
lua: '.lua',
|
||||||
shell: ".sh",
|
shell: '.sh',
|
||||||
sql: ".sql",
|
sql: '.sql',
|
||||||
html: ".html",
|
html: '.html',
|
||||||
css: ".css",
|
css: '.css'
|
||||||
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
||||||
};
|
}
|
||||||
|
|
||||||
export const generateRandomString = (length: number, lowercase = false) => {
|
export const generateRandomString = (length: number, lowercase = false) => {
|
||||||
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
|
||||||
let result = "";
|
let result = ''
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
return lowercase ? result.toLowerCase() : result
|
||||||
}
|
}
|
||||||
return lowercase ? result.toLowerCase() : result;
|
|
||||||
};
|
|
||||||
const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
|
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
|
||||||
const downloadAsFile = () => {
|
const downloadAsFile = () => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const fileExtension = programmingLanguages[language] || ".file";
|
const fileExtension = programmingLanguages[language] || '.file'
|
||||||
const suggestedFileName = `file-${generateRandomString(
|
const suggestedFileName = `file-${generateRandomString(
|
||||||
3,
|
3,
|
||||||
true
|
true
|
||||||
)}${fileExtension}`;
|
)}${fileExtension}`
|
||||||
const fileName = window.prompt("Enter file name" || "", suggestedFileName);
|
const fileName = window.prompt('Enter file name' || '', suggestedFileName)
|
||||||
|
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
// user pressed cancel on prompt
|
// user pressed cancel on prompt
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = new Blob([value], { type: "text/plain" });
|
const blob = new Blob([value], { type: 'text/plain' })
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob)
|
||||||
const link = document.createElement("a");
|
const link = document.createElement('a')
|
||||||
link.download = fileName;
|
link.download = fileName
|
||||||
link.href = url;
|
link.href = url
|
||||||
link.style.display = "none";
|
link.style.display = 'none'
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link)
|
||||||
link.click();
|
link.click()
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link)
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url)
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<div className="codeblock relative w-full font-sans text-[16px]">
|
<div className="codeblock relative w-full font-sans text-[16px]">
|
||||||
<div className="flex w-full items-center justify-between px-4 py-1.5">
|
<div className="flex w-full items-center justify-between px-4 py-1.5">
|
||||||
|
|
@ -90,7 +90,7 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||||
onClick={() => copyToClipboard(value)}
|
onClick={() => copyToClipboard(value)}
|
||||||
>
|
>
|
||||||
{isCopied ? <Check size={18} /> : <Clipboard size={18} />}
|
{isCopied ? <Check size={18} /> : <Clipboard size={18} />}
|
||||||
{isCopied ? "Copied!" : "Copy code"}
|
{isCopied ? 'Copied!' : 'Copy code'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -107,21 +107,21 @@ const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||||
style={coldarkDark}
|
style={coldarkDark}
|
||||||
customStyle={{
|
customStyle={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
background: "transparent",
|
background: 'transparent'
|
||||||
}}
|
}}
|
||||||
codeTagProps={{
|
codeTagProps={{
|
||||||
style: {
|
style: {
|
||||||
fontSize: "0.9rem",
|
fontSize: '0.9rem',
|
||||||
fontFamily: "var(--font-mono)",
|
fontFamily: 'var(--font-mono)'
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
CodeBlock.displayName = "CodeBlock";
|
CodeBlock.displayName = 'CodeBlock'
|
||||||
|
|
||||||
export default CodeBlock;
|
export default CodeBlock
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="inline-flex w-full items-center justify-center rounded border border-zinc-800 bg-white h-8 px-4 -my-1.5 text-sm leading-6 tracking-tight text-zinc-900 transition-colors ease-in-out hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50"
|
className="inline-flex w-full items-center justify-center rounded border border-zinc-800 bg-white h-8 px-4 -my-1.5 text-sm leading-6 tracking-tight text-zinc-900 transition-colors ease-in-out hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
onClick={() => router.push("/api/auth/signin")}
|
onClick={() => router.push('/api/auth/signin')}
|
||||||
>
|
>
|
||||||
<span className="font-medium">Login</span>
|
<span className="font-medium">Login</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useId } from "react";
|
import { useId } from 'react'
|
||||||
|
|
||||||
export function NextChatLogo(
|
export function NextChatLogo(
|
||||||
props: JSX.IntrinsicElements["svg"] & { inverted?: boolean }
|
props: JSX.IntrinsicElements['svg'] & { inverted?: boolean }
|
||||||
) {
|
) {
|
||||||
const id = useId();
|
const id = useId()
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width={17}
|
width={17}
|
||||||
|
|
@ -22,10 +22,10 @@ export function NextChatLogo(
|
||||||
y2="14.2667"
|
y2="14.2667"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor={props.inverted ? "white" : "black"} />
|
<stop stopColor={props.inverted ? 'white' : 'black'} />
|
||||||
<stop
|
<stop
|
||||||
offset={1}
|
offset={1}
|
||||||
stopColor={props.inverted ? "white" : "black"}
|
stopColor={props.inverted ? 'white' : 'black'}
|
||||||
stopOpacity={0}
|
stopOpacity={0}
|
||||||
/>
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
|
@ -37,35 +37,35 @@ export function NextChatLogo(
|
||||||
y2="9.50002"
|
y2="9.50002"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor={props.inverted ? "white" : "black"} />
|
<stop stopColor={props.inverted ? 'white' : 'black'} />
|
||||||
<stop
|
<stop
|
||||||
offset={1}
|
offset={1}
|
||||||
stopColor={props.inverted ? "white" : "black"}
|
stopColor={props.inverted ? 'white' : 'black'}
|
||||||
stopOpacity={0}
|
stopOpacity={0}
|
||||||
/>
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<path
|
<path
|
||||||
d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
|
d="M1 16L2.58314 11.2506C1.83084 9.74642 1.63835 8.02363 2.04013 6.39052C2.4419 4.75741 3.41171 3.32057 4.776 2.33712C6.1403 1.35367 7.81003 0.887808 9.4864 1.02289C11.1628 1.15798 12.7364 1.8852 13.9256 3.07442C15.1148 4.26363 15.842 5.83723 15.9771 7.5136C16.1122 9.18997 15.6463 10.8597 14.6629 12.224C13.6794 13.5883 12.2426 14.5581 10.6095 14.9599C8.97637 15.3616 7.25358 15.1692 5.74942 14.4169L1 16Z"
|
||||||
fill={props.inverted ? "black" : "white"}
|
fill={props.inverted ? 'black' : 'white'}
|
||||||
stroke={props.inverted ? "black" : "white"}
|
stroke={props.inverted ? 'black' : 'white'}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
<mask
|
<mask
|
||||||
id="mask0_91_2047"
|
id="mask0_91_2047"
|
||||||
style={{ maskType: "alpha" }}
|
style={{ maskType: 'alpha' }}
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x={1}
|
x={1}
|
||||||
y={0}
|
y={0}
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
>
|
>
|
||||||
<circle cx={9} cy={8} r={8} fill={props.inverted ? "black" : "white"} />
|
<circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0_91_2047)">
|
<g mask="url(#mask0_91_2047)">
|
||||||
<circle cx={9} cy={8} r={8} fill={props.inverted ? "black" : "white"} />
|
<circle cx={9} cy={8} r={8} fill={props.inverted ? 'black' : 'white'} />
|
||||||
<path
|
<path
|
||||||
d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
|
d="M14.2896 14.0018L7.146 4.8H5.80005V11.1973H6.87681V6.16743L13.4444 14.6529C13.7407 14.4545 14.0231 14.2369 14.2896 14.0018Z"
|
||||||
fill={`url(#gradient-${id}-1)`}
|
fill={`url(#gradient-${id}-1)`}
|
||||||
|
|
@ -79,7 +79,7 @@ export function NextChatLogo(
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
NextChatLogo.displayName = "DevGPT-logo";
|
NextChatLogo.displayName = 'DevGPT-logo'
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from '@/components/theme-toggle'
|
||||||
import { type Session } from "@auth/nextjs/types";
|
import { type Session } from '@auth/nextjs/types'
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
|
import { signIn } from '@auth/nextjs/client'
|
||||||
import Image from "next/image";
|
import Image from 'next/image'
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
export interface UserMenuProps {
|
export interface UserMenuProps {
|
||||||
session: Session;
|
session: Session
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMenu({ session }: UserMenuProps) {
|
export function UserMenu({ session }: UserMenuProps) {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
|
|
@ -22,8 +22,8 @@ export function UserMenu({ session }: UserMenuProps) {
|
||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
className="h-6 w-6 rounded-full select-none ring-zinc-100/10 ring-1 hover:opacity-80 transition-opacity duration-300"
|
className="h-6 w-6 rounded-full select-none ring-zinc-100/10 ring-1 hover:opacity-80 transition-opacity duration-300"
|
||||||
src={session.user?.image ? `${session.user.image}&s=60` : ""}
|
src={session.user?.image ? `${session.user.image}&s=60` : ''}
|
||||||
alt={session.user.name ?? "Avatar"}
|
alt={session.user.name ?? 'Avatar'}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 p-2 select-none">
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 p-2 select-none">
|
||||||
|
|
@ -84,7 +84,7 @@ export function UserMenu({ session }: UserMenuProps) {
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
className="py-2 px-3 hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors duration-200 cursor-pointer text-xs focus:outline-none"
|
className="py-2 px-3 hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors duration-200 cursor-pointer text-xs focus:outline-none"
|
||||||
onClick={() => router.push("/api/auth/signout")}
|
onClick={() => router.push('/api/auth/signout')}
|
||||||
>
|
>
|
||||||
Log Out
|
Log Out
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
@ -92,7 +92,7 @@ export function UserMenu({ session }: UserMenuProps) {
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
UserMenu.displayName = "UserMenu";
|
UserMenu.displayName = 'UserMenu'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export const VercelLogo = ({ className }: { className?: string }) => (
|
export const VercelLogo = ({ className }: { className?: string }) => (
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -7,8 +7,8 @@ export const VercelLogo = ({ className }: { className?: string }) => (
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
aria-label="Vercel Logo"
|
aria-label="Vercel Logo"
|
||||||
className={cn(className, "dark:fill-white fill-black")}
|
className={cn(className, 'dark:fill-white fill-black')}
|
||||||
>
|
>
|
||||||
<path d="M117.082 0L234.164 202.794H0L117.082 0Z" fill="currentColor" />
|
<path d="M117.082 0L234.164 202.794H0L117.082 0Z" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,62 @@
|
||||||
import type { NextRequest, NextFetchEvent } from "next/server";
|
import type { NextRequest, NextFetchEvent } from 'next/server'
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from 'next'
|
||||||
|
|
||||||
export const initAnalytics = ({
|
export const initAnalytics = ({
|
||||||
request,
|
request,
|
||||||
event,
|
event
|
||||||
}: {
|
}: {
|
||||||
request: NextRequest | NextApiRequest | Request;
|
request: NextRequest | NextApiRequest | Request
|
||||||
event?: NextFetchEvent;
|
event?: NextFetchEvent
|
||||||
}) => {
|
}) => {
|
||||||
const endpoint = process.env.VERCEL_URL;
|
const endpoint = process.env.VERCEL_URL
|
||||||
|
|
||||||
return {
|
return {
|
||||||
track: async (eventName: string, data?: any) => {
|
track: async (eventName: string, data?: any) => {
|
||||||
try {
|
try {
|
||||||
if (!endpoint && process.env.NODE_ENV === "development") {
|
if (!endpoint && process.env.NODE_ENV === 'development') {
|
||||||
console.log(
|
console.log(
|
||||||
`[Vercel Web Analytics] Track "${eventName}"` +
|
`[Vercel Web Analytics] Track "${eventName}"` +
|
||||||
(data ? ` with data ${JSON.stringify(data || {})}` : "")
|
(data ? ` with data ${JSON.stringify(data || {})}` : '')
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers: { [key: string]: string } = {};
|
const headers: { [key: string]: string } = {}
|
||||||
Object.entries(request.headers).map(([key, value]) => {
|
Object.entries(request.headers).map(([key, value]) => {
|
||||||
headers[key] = value;
|
headers[key] = value
|
||||||
});
|
})
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
o: headers.referer,
|
o: headers.referer,
|
||||||
ts: new Date().getTime(),
|
ts: new Date().getTime(),
|
||||||
r: "",
|
r: '',
|
||||||
en: eventName,
|
en: eventName,
|
||||||
ed: data,
|
ed: data
|
||||||
};
|
}
|
||||||
|
|
||||||
const promise = fetch(
|
const promise = fetch(
|
||||||
`https://${process.env.VERCEL_URL}/_vercel/insights/event`,
|
`https://${process.env.VERCEL_URL}/_vercel/insights/event`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
'content-type': 'application/json',
|
||||||
"user-agent": headers["user-agent"] as string,
|
'user-agent': headers['user-agent'] as string,
|
||||||
"x-forwarded-for": headers["x-forwarded-for"] as string,
|
'x-forwarded-for': headers['x-forwarded-for'] as string,
|
||||||
"x-va-server": "1",
|
'x-va-server': '1'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: "POST",
|
method: 'POST'
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
event.waitUntil(promise);
|
event.waitUntil(promise)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
await promise;
|
await promise
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
||||||
24
lib/fonts.ts
24
lib/fonts.ts
|
|
@ -1,21 +1,21 @@
|
||||||
import {
|
import {
|
||||||
JetBrains_Mono as FontMono,
|
JetBrains_Mono as FontMono,
|
||||||
IBM_Plex_Sans as FontMessage,
|
IBM_Plex_Sans as FontMessage,
|
||||||
Inter as FontSans,
|
Inter as FontSans
|
||||||
} from "next/font/google";
|
} from 'next/font/google'
|
||||||
|
|
||||||
export const fontSans = FontSans({
|
export const fontSans = FontSans({
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
variable: "--font-sans",
|
variable: '--font-sans'
|
||||||
});
|
})
|
||||||
|
|
||||||
export const fontMessage = FontMessage({
|
export const fontMessage = FontMessage({
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
weight: ["400", "500", "600"],
|
weight: ['400', '500', '600'],
|
||||||
variable: "--font-message",
|
variable: '--font-message'
|
||||||
});
|
})
|
||||||
|
|
||||||
export const fontMono = FontMono({
|
export const fontMono = FontMono({
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
variable: "--font-mono",
|
variable: '--font-mono'
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from 'react'
|
||||||
import { useRef } from "react";
|
import { useRef } from 'react'
|
||||||
|
|
||||||
export function useCmdEnterSubmit(): {
|
export function useCmdEnterSubmit(): {
|
||||||
formRef: RefObject<HTMLFormElement>;
|
formRef: RefObject<HTMLFormElement>
|
||||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||||
} {
|
} {
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
|
|
||||||
const handleKeyDown = (
|
const handleKeyDown = (
|
||||||
event: React.KeyboardEvent<HTMLTextAreaElement>
|
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||||
): void => {
|
): void => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === 'Enter') {
|
||||||
formRef.current?.requestSubmit();
|
formRef.current?.requestSubmit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return { formRef, onKeyDown: handleKeyDown };
|
return { formRef, onKeyDown: handleKeyDown }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { useState } from 'react';
|
import { useState } from 'react'
|
||||||
|
|
||||||
export interface useCopyToClipboardProps {
|
export interface useCopyToClipboardProps {
|
||||||
timeout?: number;
|
timeout?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCopyToClipboard({
|
export function useCopyToClipboard({
|
||||||
timeout = 2000,
|
timeout = 2000
|
||||||
}: useCopyToClipboardProps) {
|
}: useCopyToClipboardProps) {
|
||||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
const [isCopied, setIsCopied] = useState<Boolean>(false)
|
||||||
|
|
||||||
const copyToClipboard = (value: string) => {
|
const copyToClipboard = (value: string) => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -16,17 +16,17 @@ export function useCopyToClipboard({
|
||||||
!navigator.clipboard ||
|
!navigator.clipboard ||
|
||||||
!navigator.clipboard.writeText
|
!navigator.clipboard.writeText
|
||||||
) {
|
) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.clipboard.writeText(value).then(() => {
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
setIsCopied(true);
|
setIsCopied(true)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsCopied(false);
|
setIsCopied(false)
|
||||||
}, timeout);
|
}, timeout)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
return { isCopied, copyToClipboard };
|
return { isCopied, copyToClipboard }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
const scrollToEnd = () => {
|
const scrollToEnd = () => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: document.body.scrollHeight,
|
top: document.body.scrollHeight,
|
||||||
left: 0,
|
left: 0
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export function useFollowScroll(shouldFollow: boolean) {
|
export function useFollowScroll(shouldFollow: boolean) {
|
||||||
const shouldFollowRef = useRef(shouldFollow);
|
const shouldFollowRef = useRef(shouldFollow)
|
||||||
const scrollRef = useRef(false);
|
const scrollRef = useRef(false)
|
||||||
const triggeredBySelfRef = useRef(false);
|
const triggeredBySelfRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (triggeredBySelfRef.current) {
|
if (triggeredBySelfRef.current) {
|
||||||
triggeredBySelfRef.current = false;
|
triggeredBySelfRef.current = false
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
scrollRef.current = true;
|
scrollRef.current = true
|
||||||
};
|
}
|
||||||
window.addEventListener("scroll", handleScroll);
|
window.addEventListener('scroll', handleScroll)
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scrollRef.current && shouldFollowRef.current) {
|
if (!scrollRef.current && shouldFollowRef.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
triggeredBySelfRef.current = true;
|
triggeredBySelfRef.current = true
|
||||||
scrollToEnd();
|
scrollToEnd()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shouldFollowRef.current = shouldFollow;
|
shouldFollowRef.current = shouldFollow
|
||||||
if (!shouldFollow) {
|
if (!shouldFollow) {
|
||||||
// Reset scrollRef
|
// Reset scrollRef
|
||||||
scrollRef.current = false;
|
scrollRef.current = false
|
||||||
}
|
}
|
||||||
}, [shouldFollow]);
|
}, [shouldFollow])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
const useLocalStorage = <T>(
|
const useLocalStorage = <T>(
|
||||||
key: string,
|
key: string,
|
||||||
initialValue: T
|
initialValue: T
|
||||||
): [T, (value: T) => void] => {
|
): [T, (value: T) => void] => {
|
||||||
const [storedValue, setStoredValue] = useState(initialValue);
|
const [storedValue, setStoredValue] = useState(initialValue)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Retrieve from localStorage
|
// Retrieve from localStorage
|
||||||
const item = window.localStorage.getItem(key);
|
const item = window.localStorage.getItem(key)
|
||||||
if (item) {
|
if (item) {
|
||||||
setStoredValue(JSON.parse(item));
|
setStoredValue(JSON.parse(item))
|
||||||
}
|
}
|
||||||
}, [key]);
|
}, [key])
|
||||||
|
|
||||||
const setValue = (value: T) => {
|
const setValue = (value: T) => {
|
||||||
// Save state
|
// Save state
|
||||||
setStoredValue(value);
|
setStoredValue(value)
|
||||||
// Save to localStorage
|
// Save to localStorage
|
||||||
window.localStorage.setItem(key, JSON.stringify(value));
|
window.localStorage.setItem(key, JSON.stringify(value))
|
||||||
};
|
}
|
||||||
return [storedValue, setValue];
|
return [storedValue, setValue]
|
||||||
};
|
}
|
||||||
|
|
||||||
export default useLocalStorage;
|
export default useLocalStorage
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
export default function useScroll(threshold: number) {
|
export default function useScroll(threshold: number) {
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
|
||||||
const onScroll = useCallback(() => {
|
const onScroll = useCallback(() => {
|
||||||
setScrolled(window.pageYOffset > threshold);
|
setScrolled(window.pageYOffset > threshold)
|
||||||
}, [threshold]);
|
}, [threshold])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("scroll", onScroll);
|
window.addEventListener('scroll', onScroll)
|
||||||
return () => window.removeEventListener("scroll", onScroll);
|
return () => window.removeEventListener('scroll', onScroll)
|
||||||
}, [onScroll]);
|
}, [onScroll])
|
||||||
|
|
||||||
return scrolled;
|
return scrolled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export default function useWindowSize() {
|
export default function useWindowSize() {
|
||||||
const [windowSize, setWindowSize] = useState<{
|
const [windowSize, setWindowSize] = useState<{
|
||||||
width: number | undefined;
|
width: number | undefined
|
||||||
height: number | undefined;
|
height: number | undefined
|
||||||
}>({
|
}>({
|
||||||
width: undefined,
|
width: undefined,
|
||||||
height: undefined,
|
height: undefined
|
||||||
});
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Handler to call on window resize
|
// Handler to call on window resize
|
||||||
|
|
@ -16,24 +16,23 @@ export default function useWindowSize() {
|
||||||
// Set window width/height to state
|
// Set window width/height to state
|
||||||
setWindowSize({
|
setWindowSize({
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listener
|
// Add event listener
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
// Call handler right away so state gets updated with initial window size
|
// Call handler right away so state gets updated with initial window size
|
||||||
handleResize();
|
handleResize()
|
||||||
|
|
||||||
// Remove event listener on cleanup
|
// Remove event listener on cleanup
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
}, []); // Empty array ensures that effect is only run on mount
|
}, []) // Empty array ensures that effect is only run on mount
|
||||||
|
|
||||||
return {
|
return {
|
||||||
windowSize,
|
windowSize,
|
||||||
isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768,
|
isMobile: typeof windowSize?.width === 'number' && windowSize?.width < 768,
|
||||||
isDesktop:
|
isDesktop: typeof windowSize?.width === 'number' && windowSize?.width >= 768
|
||||||
typeof windowSize?.width === "number" && windowSize?.width >= 768,
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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> {
|
export interface Chat extends Record<string, any> {
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
createdAt: Date;
|
createdAt: Date
|
||||||
userId: string;
|
userId: string
|
||||||
messages: Message[];
|
messages: Message[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
lib/utils.ts
28
lib/utils.ts
|
|
@ -1,34 +1,34 @@
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from 'clsx'
|
||||||
import { customAlphabet } from "nanoid";
|
import { customAlphabet } from 'nanoid'
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nanoid = customAlphabet(
|
export const nanoid = customAlphabet(
|
||||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||||
7
|
7
|
||||||
); // 7-character random string
|
) // 7-character random string
|
||||||
|
|
||||||
export async function fetcher<JSON = any>(
|
export async function fetcher<JSON = any>(
|
||||||
input: RequestInfo,
|
input: RequestInfo,
|
||||||
init?: RequestInit
|
init?: RequestInit
|
||||||
): Promise<JSON> {
|
): Promise<JSON> {
|
||||||
const res = await fetch(input, init);
|
const res = await fetch(input, init)
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const json = await res.json();
|
const json = await res.json()
|
||||||
if (json.error) {
|
if (json.error) {
|
||||||
const error = new Error(json.error) as Error & {
|
const error = new Error(json.error) as Error & {
|
||||||
status: number;
|
status: number
|
||||||
};
|
}
|
||||||
error.status = res.status;
|
error.status = res.status
|
||||||
throw error;
|
throw error
|
||||||
} else {
|
} else {
|
||||||
throw new Error("An unexpected error occurred");
|
throw new Error('An unexpected error occurred')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,5 +61,13 @@
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@auth/core": "0.0.0-manual.527fff6c"
|
"@auth/core": "0.0.0-manual.527fff6c"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"trailingComma": "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue