From cb941daca941148ad54642cf655a58972f6f885c Mon Sep 17 00:00:00 2001 From: shadcn Date: Fri, 16 Jun 2023 21:52:01 +0400 Subject: [PATCH] fix: actions --- app/actions.ts | 57 +++++-------- app/api/chat/route.ts | 3 +- app/chat/[id]/page.tsx | 27 ++++-- components/chat-message-actions.tsx | 3 +- components/chat-panel.tsx | 1 - components/chat.tsx | 3 +- components/clear-history.tsx | 12 ++- components/footer.tsx | 2 +- components/header.tsx | 17 ++-- components/providers.tsx | 1 + components/sidebar-actions.tsx | 14 +++- components/sidebar-list.tsx | 1 - components/ui/icons.tsx | 20 ++++- components/ui/select.tsx | 123 ++++++++++++++++++++++++++++ lib/types.ts | 7 +- package.json | 1 + pnpm-lock.yaml | 50 +++++++++++ 17 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 components/ui/select.tsx diff --git a/app/actions.ts b/app/actions.ts index 451da5d..90c6973 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,9 +1,10 @@ 'use server' import { revalidatePath } from 'next/cache' +import { redirect } from 'next/navigation' import { kv } from '@vercel/kv' -import { auth } from '@/auth' +import { auth } from '@/auth' import { type Chat } from '@/lib/types' export async function getChats(userId?: string | null) { @@ -32,12 +33,8 @@ export async function getChats(userId?: string | null) { 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') + if (!chat || (userId && chat.userId !== userId)) { + return null } return chat @@ -47,41 +44,36 @@ export async function removeChat({ id, path }: { id: string; path: string }) { const session = await auth() if (!session) { - throw new Error('Unauthorized') + return { + error: 'Unauthorized' + } } const uid = await kv.hget(`chat:${id}`, 'userId') if (uid !== session?.user?.id) { - throw new Error('Unauthorized') + return { + error: 'Unauthorized' + } } await kv.del(`chat:${id}`) await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`) revalidatePath('/') - revalidatePath(path) + return revalidatePath(path) } export async function clearChats() { const session = await auth() - if (!session) { - throw new Error('Unauthorized') - } - - if (!session.user?.id) { - throw new Error('Unauthorized') - } - - const chats: string[] = await kv.zrange( - `user:chat:${session.user.id}`, - 0, - -1, - { - rev: true + if (!session?.user?.id) { + return { + error: 'Unauthorized' } - ) + } + + const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1) const pipeline = kv.pipeline() @@ -93,6 +85,7 @@ export async function clearChats() { await pipeline.exec() revalidatePath('/') + return redirect('/') } export async function getSharedChat(id: string) { @@ -108,16 +101,10 @@ export async function getSharedChat(id: string) { export async function shareChat(chat: Chat) { const session = await auth() - if (!session) { - throw new Error('Unauthorized') - } - - if (!session.user?.id) { - throw new Error('Unauthorized') - } - - if (chat.userId !== session.user?.id) { - throw new Error('Unauthorized') + if (!session?.user?.id || session.user.id !== chat.userId) { + return { + error: 'Unauthorized' + } } const payload = { diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 58008c3..beec80f 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,8 +1,9 @@ import { kv } from '@vercel/kv' import { OpenAIStream, StreamingTextResponse } from 'ai-connector' import { Configuration, OpenAIApi } from 'openai-edge' -import { nanoid } from '@/lib/utils' + import { auth } from '@/auth' +import { nanoid } from '@/lib/utils' export const runtime = 'edge' diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index a3504bc..c1a569f 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -1,8 +1,9 @@ import { type Metadata } from 'next' -import { auth } from '@/auth' +import { notFound, redirect } from 'next/navigation' -import { Chat } from '@/components/chat' +import { auth } from '@/auth' import { getChat } from '@/app/actions' +import { Chat } from '@/components/chat' // export const runtime = 'edge' export const preferredRegion = 'home' @@ -16,16 +17,30 @@ export interface ChatPageProps { export async function generateMetadata({ params }: ChatPageProps): Promise { - const { user } = await auth() - const chat = await getChat(params.id, user?.id ?? '') + const session = await auth() + + if (!session?.user) { + return {} + } + + const chat = await getChat(params.id, session.user.id) return { title: chat?.title.slice(0, 50) ?? 'Chat' } } export default async function ChatPage({ params }: ChatPageProps) { - const { user } = await auth() - const chat = await getChat(params.id, user?.id ?? '') + const session = await auth() + + if (!session?.user) { + redirect('/sign-in') + } + + const chat = await getChat(params.id, session.user.id) + + if (!chat) { + notFound() + } return } diff --git a/components/chat-message-actions.tsx b/components/chat-message-actions.tsx index 732206a..b42c22f 100644 --- a/components/chat-message-actions.tsx +++ b/components/chat-message-actions.tsx @@ -1,10 +1,11 @@ 'use client' +import { type Message } from 'ai-connector' + import { Button } from '@/components/ui/button' import { IconCheck, IconCopy } from '@/components/ui/icons' import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' import { cn } from '@/lib/utils' -import { type Message } from 'ai-connector' interface ChatMessageActionsProps extends React.ComponentProps<'div'> { message: Message diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index b48e346..85ff951 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -1,7 +1,6 @@ import { type UseChatHelpers } from 'ai-connector/react' import { Button } from '@/components/ui/button' -import { ExternalLink } from '@/components/external-link' import { PromptForm } from '@/components/prompt-form' import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' import { IconRefresh, IconStop } from '@/components/ui/icons' diff --git a/components/chat.tsx b/components/chat.tsx index 46049ea..165a77b 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -10,11 +10,10 @@ import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' export interface ChatProps extends React.ComponentProps<'div'> { initialMessages?: Message[] - id?: string } -export function Chat({ id, title, initialMessages, className }: ChatProps) { +export function Chat({ id, initialMessages, className }: ChatProps) { const { messages, append, reload, stop, isLoading, input, setInput } = useChat({ initialMessages, diff --git a/components/clear-history.tsx b/components/clear-history.tsx index 1d1f7ed..16e3643 100644 --- a/components/clear-history.tsx +++ b/components/clear-history.tsx @@ -2,7 +2,9 @@ import * as React from 'react' import { useRouter } from 'next/navigation' +import { toast } from 'react-hot-toast' +import { ServerActionResult } from '@/lib/types' import { Button } from '@/components/ui/button' import { AlertDialog, @@ -18,7 +20,7 @@ import { import { IconSpinner } from '@/components/ui/icons' interface ClearHistoryProps { - clearChats: () => Promise + clearChats: () => ServerActionResult } export function ClearHistory({ clearChats }: ClearHistoryProps) { @@ -49,7 +51,13 @@ export function ClearHistory({ clearChats }: ClearHistoryProps) { onClick={event => { event.preventDefault() startTransition(async () => { - await clearChats() + const result = await clearChats() + + if (result && 'error' in result) { + toast.error(result.error) + return + } + setOpen(false) router.push('/') }) diff --git a/components/footer.tsx b/components/footer.tsx index 4b65095..0a8fce3 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { ExternalLink } from '@/components/external-link' import { cn } from '@/lib/utils' +import { ExternalLink } from '@/components/external-link' export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { return ( diff --git a/components/header.tsx b/components/header.tsx index 469fc60..0353809 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,19 +1,18 @@ -import { Suspense, use } from 'react' +import * as React from 'react' +import Link from 'next/link' import { cn } from '@/lib/utils' +import { auth } from '@/auth' +import { clearChats } from '@/app/actions' import { buttonVariants } from '@/components/ui/button' import { Sidebar } from '@/components/sidebar' import { SidebarList } from '@/components/sidebar-list' import { IconGitHub, IconSeparator, IconVercel } from '@/components/ui/icons' - import { SidebarFooter } from '@/components/sidebar-footer' import { ThemeToggle } from '@/components/theme-toggle' import { ClearHistory } from '@/components/clear-history' -import { clearChats } from '@/app/actions' -import Link from 'next/link' -import { auth } from '@/auth' -import { UserMenu } from './user-menu' -import { LoginButton } from './login-button' +import { UserMenu } from '@/components/user-menu' +import { LoginButton } from '@/components/login-button' export async function Header() { const session = await auth() @@ -22,10 +21,10 @@ export async function Header() {
{session?.user ? ( - }> + }> {/* @ts-ignore */} - + diff --git a/components/providers.tsx b/components/providers.tsx index b4b9652..8922264 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -3,6 +3,7 @@ import * as React from 'react' import { ThemeProvider as NextThemesProvider } from 'next-themes' import { ThemeProviderProps } from 'next-themes/dist/types' + import { TooltipProvider } from '@/components/ui/tooltip' export function Providers({ children, ...props }: ThemeProviderProps) { diff --git a/components/sidebar-actions.tsx b/components/sidebar-actions.tsx index f50795a..4c6a34b 100644 --- a/components/sidebar-actions.tsx +++ b/components/sidebar-actions.tsx @@ -41,7 +41,7 @@ import { interface SidebarActionsProps { chat: Chat - removeChat: (args: { id: string; path: string }) => Promise + removeChat: (args: { id: string; path: string }) => ServerActionResult shareChat: (chat: Chat) => ServerActionResult } @@ -150,8 +150,8 @@ export function SidebarActions({ const result = await shareChat(chat) - if (!('id' in result)) { - toast.error(result.message) + if (result && 'error' in result) { + toast.error(result.error) return } @@ -189,10 +189,16 @@ export function SidebarActions({ onClick={event => { event.preventDefault() startRemoveTransition(async () => { - await removeChat({ + const result = await removeChat({ id: chat.id, path: chat.path }) + + if (result && 'error' in result) { + toast.error(result.error) + return + } + setDeleteDialogOpen(false) router.push('/') }) diff --git a/components/sidebar-list.tsx b/components/sidebar-list.tsx index 301e705..de2049a 100644 --- a/components/sidebar-list.tsx +++ b/components/sidebar-list.tsx @@ -1,6 +1,5 @@ import { getChats, removeChat, shareChat } from '@/app/actions' import { SidebarActions } from '@/components/sidebar-actions' - import { SidebarItem } from '@/components/sidebar-item' export interface SidebarListProps { diff --git a/components/ui/icons.tsx b/components/ui/icons.tsx index 52b941a..41ab4ea 100644 --- a/components/ui/icons.tsx +++ b/components/ui/icons.tsx @@ -459,6 +459,23 @@ function IconExternalLink({ ) } +function IconChevronUpDown({ + className, + ...props +}: React.ComponentProps<'svg'>) { + return ( + + + + ) +} + export { IconEdit, IconNextChat, @@ -485,5 +502,6 @@ export { IconClose, IconShare, IconUsers, - IconExternalLink + IconExternalLink, + IconChevronUpDown } diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..77f12c2 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,123 @@ +'use client' + +import * as React from 'react' +import * as SelectPrimitive from '@radix-ui/react-select' + +import { cn } from '@/lib/utils' +import { + IconArrowDown, + IconCheck, + IconChevronUpDown +} from '@/components/ui/icons' + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + {children} + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator +} diff --git a/lib/types.ts b/lib/types.ts index fc2a907..4434987 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -10,4 +10,9 @@ export interface Chat extends Record { sharePath?: string } -export type ServerActionResult = Promise +export type ServerActionResult = Promise< + | Result + | { + error: string + } +> diff --git a/package.json b/package.json index d8ffe5a..00a295f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc8531a..0e14b53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-separator': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) @@ -458,6 +461,12 @@ packages: tslib: 2.5.0 dev: true + /@radix-ui/number@1.0.1: + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -897,6 +906,47 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-select@1.2.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.6)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.6 + '@types/react-dom': 18.2.4 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.6)(react@18.2.0) + dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} peerDependencies: