From 01dc4520d6deab8e110d5fe7adbe4dfa017f717c Mon Sep 17 00:00:00 2001 From: shadcn Date: Sun, 11 Jun 2023 00:23:23 +0400 Subject: [PATCH] feat: reorganize files and implement ui and sidebar --- app/actions.ts | 35 +- app/api/auth/[...nextauth]/route.ts | 2 +- app/api/chat/route.ts | 2 +- app/chat-list.tsx | 44 --- app/chat.tsx | 57 --- app/chat/[id]/page.tsx | 38 +- app/header.tsx | 52 --- app/layout.tsx | 10 +- app/page.tsx | 11 +- app/sidebar-item.tsx | 68 ---- app/sidebar.tsx | 69 ---- auth.ts | 28 +- components/chat-list.tsx | 26 ++ {app => components}/chat-message.tsx | 17 +- components/chat-panel.tsx | 45 +++ components/chat.tsx | 33 ++ {app => components}/empty-screen.tsx | 11 +- {app => components}/external-link.tsx | 0 components/header.tsx | 42 +++ {app => components}/icon.png | Bin components/icons.tsx | 23 ++ app/prompt.tsx => components/prompt-form.tsx | 92 +++-- components/sidebar-item.tsx | 104 ++++++ components/sidebar.tsx | 77 ++++ components/theme-toggle.tsx | 24 +- components/ui/alert-dialog.tsx | 150 ++++++++ components/ui/button.tsx | 11 +- components/ui/dialog.tsx | 128 +++++++ components/ui/dropdown-menu.tsx | 200 ++++++++++ components/ui/input.tsx | 25 ++ components/ui/sheet.tsx | 1 - components/user-menu.tsx | 33 +- {hooks => lib/hooks}/use-chat-store.ts | 0 ...-enter-submit.tsx => use-enter-submit.tsx} | 5 +- package.json | 1 + pnpm-lock.yaml | 345 +++++++++++++++++- 36 files changed, 1347 insertions(+), 462 deletions(-) delete mode 100644 app/chat-list.tsx delete mode 100644 app/chat.tsx delete mode 100644 app/header.tsx delete mode 100644 app/sidebar-item.tsx delete mode 100644 app/sidebar.tsx create mode 100644 components/chat-list.tsx rename {app => components}/chat-message.tsx (95%) create mode 100644 components/chat-panel.tsx create mode 100644 components/chat.tsx rename {app => components}/empty-screen.tsx (86%) rename {app => components}/external-link.tsx (100%) create mode 100644 components/header.tsx rename {app => components}/icon.png (100%) rename app/prompt.tsx => components/prompt-form.tsx (55%) create mode 100644 components/sidebar-item.tsx create mode 100644 components/sidebar.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/input.tsx rename {hooks => lib/hooks}/use-chat-store.ts (100%) rename lib/hooks/{use-command-enter-submit.tsx => use-enter-submit.tsx} (78%) diff --git a/app/actions.ts b/app/actions.ts index aa4c98f..cb28bf5 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,7 +1,40 @@ 'use server' -import { kv } from '@vercel/kv' import { revalidatePath } from 'next/cache' +import { kv } from '@vercel/kv' + +import { type Chat } from '@/lib/types' + +export async function getChats(userId: string) { + try { + const pipeline = kv.pipeline() + const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1) + + for (const chat of chats) { + pipeline.hgetall(chat) + } + + const results = await pipeline.exec() + + return results as Chat[] + } catch (error) { + return [] + } +} + +export async function getChat(id: string, userId: string) { + const chat = await kv.hgetall(`chat:${id}`) + + if (!chat) { + throw new Error('Not found') + } + + if (userId && chat.userId !== userId) { + throw new Error('Unauthorized') + } + + return chat +} export async function removeChat({ id, diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 883210b..4463a8e 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,2 +1,2 @@ export { GET, POST } from '@/auth' -export const runtime = 'edge' +// export const runtime = 'edge' diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 710194d..fa86dd5 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -4,7 +4,7 @@ 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 diff --git a/app/chat-list.tsx b/app/chat-list.tsx deleted file mode 100644 index 491fa78..0000000 --- a/app/chat-list.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { type Message } from 'ai-connector' - -import { ChatMessage } from './chat-message' -import { EmptyScreen } from './empty-screen' - -export interface ChatList { - messages: Message[] -} - -export function ChatList({ messages }: ChatList) { - return ( -
- {/*
-
-
- - Next.js Chatbot -
-
- -
-
-
*/} - {messages.length > 0 ? ( -
- {messages.map(message => ( - - ))} -
- ) : ( -
- -
- )} -
- ) -} diff --git a/app/chat.tsx b/app/chat.tsx deleted file mode 100644 index 71e4e94..0000000 --- a/app/chat.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client' - -import { useRouter } from 'next/navigation' -import { Prompt } from './prompt' -import { useChat, type Message } from 'ai-connector' -import { ChatList } from './chat-list' -import { ExternalLink } from '@/app/external-link' - -export interface ChatProps { - // create?: (input: string) => Chat | undefined; - initialMessages?: Message[] - id?: string -} - -export function Chat({ - id, - // create, - initialMessages -}: ChatProps) { - const router = useRouter() - - const { isLoading, messages, reload, append } = useChat({ - initialMessages: initialMessages as any[], - id - // onCreate: (id: string) => { - // router.push(`/chat/${id}`); - // }, - }) - - return ( -
- -
-
- { - append({ - content: value, - role: 'user' - }) - }} - onRefresh={messages.length ? reload : undefined} - isLoading={isLoading} - /> -

- This is an open source AI chatbot app built with{' '} - Next.js and{' '} - - Vercel KV - - . -

-
-
-
- ) -} diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index 3f1e08e..3d78942 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -1,15 +1,12 @@ -import { Sidebar } from '@/app/sidebar' - -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 { auth } from '@/auth' +import { getChat } from '@/app/actions' +import { Chat } from '@/components/chat' -export const runtime = 'edge' +// export const runtime = 'edge' export const preferredRegion = 'home' + export interface ChatPageProps { params: { id: string @@ -30,28 +27,5 @@ export default async function ChatPage({ params }: ChatPageProps) { const session = await auth() const chat = await getChat(params.id, session?.user?.email ?? '') - return ( -
- {/* @ts-ignore */} - -
- -
-
- ) -} - -ChatPage.displayName = 'ChatPage' - -async function getChat(id: string, userId: string) { - const chat = await kv.hgetall(`chat:${id}`) - if (!chat) { - throw new Error('Not found') - } - - if (userId && chat.userId !== userId) { - throw new Error('Unauthorized') - } - - return chat + return } diff --git a/app/header.tsx b/app/header.tsx deleted file mode 100644 index 7cce3f7..0000000 --- a/app/header.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { GitHub, Vercel } from '@/components/icons' -import './globals.css' - -import { Sidebar } from '@/app/sidebar' -import { buttonVariants } from '@/components/ui/button' -import { cn } from '@/lib/utils' -import { Plus } from 'lucide-react' -import Link from 'next/link' -import { UserMenu } from '@/components/user-menu' -import { auth } from '@/auth' - -export async function Header() { - const session = await auth() - - return ( -
-
- {/* @ts-ignore */} - - -
- -
- ) -} diff --git a/app/layout.tsx b/app/layout.tsx index 496838e..2bd830f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,11 +1,11 @@ import { Metadata } from 'next' -import './globals.css' -import { TailwindIndicator } from '@/components/tailwind-indicator' -import { ThemeProvider } from '@/components/theme-provider' +import '@/app/globals.css' import { fontMono, fontSans } from '@/lib/fonts' import { cn } from '@/lib/utils' -import { Header } from '@/app/header' +import { TailwindIndicator } from '@/components/tailwind-indicator' +import { ThemeProvider } from '@/components/theme-provider' +import { Header } from '@/components/header' export const metadata: Metadata = { title: { @@ -43,7 +43,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
{/* @ts-ignore */}
-
{children}
+
{children}
diff --git a/app/page.tsx b/app/page.tsx index f934585..ee1ff4c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,8 @@ -import { type Message } from 'ai-connector' -import { Chat } from './chat' +import { Chat } from '@/components/chat' -export const runtime = 'edge' +// export const runtime = 'edge' export const preferredRegion = 'home' export default async function IndexPage() { - return ( -
- -
- ) + return } diff --git a/app/sidebar-item.tsx b/app/sidebar-item.tsx deleted file mode 100644 index f0598e6..0000000 --- a/app/sidebar-item.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'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' - -export function SidebarItem({ - title, - href, - id, - userId -}: { - title: string - href: string - id: string - userId: string -}) { - const pathname = usePathname() - const router = useRouter() - const active = pathname === href - const [isPending, startTransition] = useTransition() - - if (!id) return null - - return ( - - -
- {title} -
- - - ) -} diff --git a/app/sidebar.tsx b/app/sidebar.tsx deleted file mode 100644 index acb2ea6..0000000 --- a/app/sidebar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Chat } from '@/lib/types' -import { type Session } from '@auth/nextjs/types' -import { kv } from '@vercel/kv' -import { Sidebar as SidebarIcon } from 'lucide-react' -import { SidebarItem } from './sidebar-item' - -import { auth } from '@/auth' -import { NextChat } from '@/components/icons' -import { Button } from '@/components/ui/button' -import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' - -export interface SidebarProps {} - -export async function Sidebar({}: SidebarProps) { - const session = await auth() - - return ( - - - - - -
- - Chatbot -
- {/* @ts-ignore */} - -
-
- ) -} - -async function SidebarList({ session }: { session?: Session }) { - const results: Chat[] = await getChats(session?.user?.email ?? '') - - return results.map(c => ( - - )) -} - -async function getChats(userId: string) { - try { - const pipeline = kv.pipeline() - const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1) - - for (const chat of chats) { - pipeline.hgetall(chat) - } - - const results = await pipeline.exec() - - return results as Chat[] - } catch (error) { - return [] - } -} diff --git a/auth.ts b/auth.ts index a55a7da..afc23f5 100644 --- a/auth.ts +++ b/auth.ts @@ -1,27 +1,27 @@ -import NextAuth from "@auth/nextjs"; -import GitHub from "@auth/nextjs/providers/github"; -import { NextResponse } from "next/server"; +import NextAuth from '@auth/nextjs' +import GitHub from '@auth/nextjs/providers/github' +import { NextResponse } from 'next/server' export const { handlers: { GET, POST }, auth, - CSRF_experimental, + CSRF_experimental } = NextAuth({ // @ts-ignore providers: [GitHub], - session: { strategy: "jwt" }, + session: { strategy: 'jwt' }, async authorized({ request, auth }: any) { - const url = request.nextUrl; + const url = request.nextUrl - if (request.method === "POST") { - const { authToken } = (await request.json()) ?? {}; + if (request.method === 'POST') { + const { authToken } = (await request.json()) ?? {} // If the request has a valid auth token, it is authorized - const valid = true; - if (valid) return true; - return NextResponse.json("Invalid auth token", { status: 401 }); + const valid = true + if (valid) return true + return NextResponse.json('Invalid auth token', { status: 401 }) } // Logged in users are authenticated, otherwise redirect to login page - return !!auth; - }, -}); + return !!auth + } +}) diff --git a/components/chat-list.tsx b/components/chat-list.tsx new file mode 100644 index 0000000..2fae8fa --- /dev/null +++ b/components/chat-list.tsx @@ -0,0 +1,26 @@ +'use client' + +import { type Message } from 'ai-connector' + +import { ChatMessage } from '@/components/chat-message' +import { EmptyScreen } from '@/components/empty-screen' + +export interface ChatList { + messages: Message[] +} + +export function ChatList({ messages }: ChatList) { + return ( +
+ {messages.length > 0 ? ( + messages.map(message => ( + + )) + ) : ( +
+ +
+ )} +
+ ) +} diff --git a/app/chat-message.tsx b/components/chat-message.tsx similarity index 95% rename from app/chat-message.tsx rename to components/chat-message.tsx index 96aae87..5578c14 100644 --- a/app/chat-message.tsx +++ b/components/chat-message.tsx @@ -1,11 +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 { User } from 'lucide-react' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' + +import { cn } from '@/lib/utils' +import { fontMessage } from '@/lib/fonts' +import CodeBlock from '@/components/ui/codeblock' +import { MemoizedReactMarkdown } from '@/components/markdown' import { OpenAI } from '@/components/icons' export interface ChatMessageProps { @@ -16,7 +17,7 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) { return (
diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx new file mode 100644 index 0000000..b4eb45a --- /dev/null +++ b/components/chat-panel.tsx @@ -0,0 +1,45 @@ +'use client' + +import { ExternalLink } from '@/components/external-link' +import { UseChatHelpers } from 'ai-connector' +import { PromptForm } from './prompt-form' + +export interface ChatPanelProps + extends Pick { + id?: string +} + +export function ChatPanel({ + id, + append, + isLoading, + reload, + messages +}: ChatPanelProps) { + return ( +
+
+
+ { + append({ + content: value, + role: 'user' + }) + }} + onRefresh={messages.length ? reload : undefined} + isLoading={isLoading} + /> +

+ This is an open source AI chatbot app built with{' '} + Next.js and{' '} + + Vercel KV + + . +

+
+
+
+ ) +} diff --git a/components/chat.tsx b/components/chat.tsx new file mode 100644 index 0000000..d207ade --- /dev/null +++ b/components/chat.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useChat, type Message } from 'ai-connector' +import { ChatList } from '@/components/chat-list' +import { ChatPanel } from '@/components/chat-panel' + +export interface ChatProps { + // create?: (input: string) => Chat | undefined; + initialMessages?: Message[] + id?: string +} + +export function Chat({ id, initialMessages }: ChatProps) { + const { messages, append, reload, isLoading } = useChat({ + initialMessages, + id + }) + + return ( +
+
+ + +
+
+ ) +} diff --git a/app/empty-screen.tsx b/components/empty-screen.tsx similarity index 86% rename from app/empty-screen.tsx rename to components/empty-screen.tsx index 61a54c9..4fe4bca 100644 --- a/app/empty-screen.tsx +++ b/components/empty-screen.tsx @@ -1,7 +1,8 @@ -import { cn } from '@/lib/utils' -import { ExternalLink } from './external-link' import { ArrowRight } from 'lucide-react' -import { useChatStore } from '@/hooks/use-chat-store' + +import { cn } from '@/lib/utils' +import { useChatStore } from '@/lib/hooks/use-chat-store' +import { ExternalLink } from '@/components/external-link' import { Button } from '@/components/ui/button' import { OpenAI } from '@/components/icons' @@ -24,10 +25,10 @@ export function EmptyScreen({ className }: React.ComponentProps<'div'>) { const { setDefaultMessage } = useChatStore() return ( -
+
diff --git a/app/external-link.tsx b/components/external-link.tsx similarity index 100% rename from app/external-link.tsx rename to components/external-link.tsx diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..1bab4c9 --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,42 @@ +import { cn } from '@/lib/utils' +import { auth } from '@/auth' +import { getChats } from '@/app/actions' +import { buttonVariants } from '@/components/ui/button' +import { UserMenu } from '@/components/user-menu' +import { GitHub, Separator, Vercel } from '@/components/icons' +import { Sidebar } from '@/components/sidebar' + +export async function Header() { + const session = await auth() + const chats = session?.user?.email ? await getChats(session.user.email) : [] + + return ( +
+
+ {/* @ts-ignore */} + + + +
+ +
+ ) +} diff --git a/app/icon.png b/components/icon.png similarity index 100% rename from app/icon.png rename to components/icon.png diff --git a/components/icons.tsx b/components/icons.tsx index dfbdcf1..6a08c63 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -1,6 +1,7 @@ 'use client' import * as React from 'react' + import { cn } from '@/lib/utils' export function NextChat({ @@ -135,3 +136,25 @@ export function GitHub({ className, ...props }: React.ComponentProps<'svg'>) { ) } + +export function Separator({ + className, + ...props +}: React.ComponentProps<'svg'>) { + return ( + + ) +} diff --git a/app/prompt.tsx b/components/prompt-form.tsx similarity index 55% rename from app/prompt.tsx rename to components/prompt-form.tsx index ebfab29..82dad38 100644 --- a/app/prompt.tsx +++ b/components/prompt-form.tsx @@ -1,21 +1,22 @@ 'use client' import * as React from 'react' -import { CornerDownLeft, RefreshCcw, StopCircle } from 'lucide-react' +import { CornerDownLeft, Plus, RefreshCcw, StopCircle } from 'lucide-react' import { useState } from 'react' import Textarea from 'react-textarea-autosize' -import { Button } from '@/components/ui/button' +import { Button, buttonVariants } from '@/components/ui/button' import { fontMessage } from '@/lib/fonts' -import { useCmdEnterSubmit } from '@/lib/hooks/use-command-enter-submit' +import { useEnterSubmit } from '@/lib/hooks/use-enter-submit' import { cn } from '@/lib/utils' -import { useChatStore } from '@/hooks/use-chat-store' +import { useChatStore } from '@/lib/hooks/use-chat-store' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import Link from 'next/link' export interface PromptProps { onSubmit: (value: string) => void @@ -24,7 +25,7 @@ export interface PromptProps { isLoading: boolean } -export function Prompt({ +export function PromptForm({ onSubmit, onRefresh, onAbort, @@ -32,7 +33,7 @@ export function Prompt({ }: PromptProps) { const { defaultMessage } = useChatStore() const [input, setInput] = useState(defaultMessage) - const { formRef, onKeyDown } = useCmdEnterSubmit() + const { formRef, onKeyDown } = useEnterSubmit() const inputRef = React.useRef(null) React.useEffect(() => { @@ -69,45 +70,40 @@ export function Prompt({
) : null} - {/* */}
-
-