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 []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue