feat: support guest session (#919)
This commit is contained in:
parent
24cb2ce19b
commit
9279135355
34 changed files with 741 additions and 288 deletions
|
|
@ -12,17 +12,20 @@ import { useSidebar } from './ui/sidebar';
|
|||
import { memo } from 'react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { type VisibilityType, VisibilitySelector } from './visibility-selector';
|
||||
import type { Session } from 'next-auth';
|
||||
|
||||
function PureChatHeader({
|
||||
chatId,
|
||||
selectedModelId,
|
||||
selectedVisibilityType,
|
||||
isReadonly,
|
||||
session,
|
||||
}: {
|
||||
chatId: string;
|
||||
selectedModelId: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
session: Session;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { open } = useSidebar();
|
||||
|
|
@ -54,6 +57,7 @@ function PureChatHeader({
|
|||
|
||||
{!isReadonly && (
|
||||
<ModelSelector
|
||||
session={session}
|
||||
selectedModelId={selectedModelId}
|
||||
className="order-1 md:order-2"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import { MultimodalInput } from './multimodal-input';
|
|||
import { Messages } from './messages';
|
||||
import type { VisibilityType } from './visibility-selector';
|
||||
import { useArtifactSelector } from '@/hooks/use-artifact';
|
||||
import { toast } from 'sonner';
|
||||
import { unstable_serialize } from 'swr/infinite';
|
||||
import { getChatHistoryPaginationKey } from './sidebar-history';
|
||||
import { toast } from './toast';
|
||||
import type { Session } from 'next-auth';
|
||||
|
||||
export function Chat({
|
||||
id,
|
||||
|
|
@ -22,12 +23,14 @@ export function Chat({
|
|||
selectedChatModel,
|
||||
selectedVisibilityType,
|
||||
isReadonly,
|
||||
session,
|
||||
}: {
|
||||
id: string;
|
||||
initialMessages: Array<UIMessage>;
|
||||
selectedChatModel: string;
|
||||
selectedVisibilityType: VisibilityType;
|
||||
isReadonly: boolean;
|
||||
session: Session;
|
||||
}) {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
|
|
@ -51,8 +54,11 @@ export function Chat({
|
|||
onFinish: () => {
|
||||
mutate(unstable_serialize(getChatHistoryPaginationKey));
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('An error occurred, please try again!');
|
||||
onError: (error) => {
|
||||
toast({
|
||||
type: 'error',
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -72,6 +78,7 @@ export function Chat({
|
|||
selectedModelId={selectedChatModel}
|
||||
selectedVisibilityType={selectedVisibilityType}
|
||||
isReadonly={isReadonly}
|
||||
session={session}
|
||||
/>
|
||||
|
||||
<Messages
|
||||
|
|
|
|||
|
|
@ -14,20 +14,34 @@ import { chatModels } from '@/lib/ai/models';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { CheckCircleFillIcon, ChevronDownIcon } from './icons';
|
||||
import { entitlementsByUserType } from '@/lib/ai/entitlements';
|
||||
import type { Session } from 'next-auth';
|
||||
|
||||
export function ModelSelector({
|
||||
session,
|
||||
selectedModelId,
|
||||
className,
|
||||
}: {
|
||||
session: Session;
|
||||
selectedModelId: string;
|
||||
} & React.ComponentProps<typeof Button>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [optimisticModelId, setOptimisticModelId] =
|
||||
useOptimistic(selectedModelId);
|
||||
|
||||
const userType = session.user.type;
|
||||
const { availableChatModelIds } = entitlementsByUserType[userType];
|
||||
|
||||
const availableChatModels = chatModels.filter((chatModel) =>
|
||||
availableChatModelIds.includes(chatModel.id),
|
||||
);
|
||||
|
||||
const selectedChatModel = useMemo(
|
||||
() => chatModels.find((chatModel) => chatModel.id === optimisticModelId),
|
||||
[optimisticModelId],
|
||||
() =>
|
||||
availableChatModels.find(
|
||||
(chatModel) => chatModel.id === optimisticModelId,
|
||||
),
|
||||
[optimisticModelId, availableChatModels],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -49,7 +63,7 @@ export function ModelSelector({
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="min-w-[300px]">
|
||||
{chatModels.map((chatModel) => {
|
||||
{availableChatModels.map((chatModel) => {
|
||||
const { id } = chatModel;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export function SidebarToggle({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
data-testid="sidebar-toggle-button"
|
||||
onClick={toggleSidebar}
|
||||
variant="outline"
|
||||
className="md:px-2 md:h-fit"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { ChevronUp } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import type { User } from 'next-auth';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import {
|
||||
|
|
@ -17,49 +18,92 @@ import {
|
|||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from './toast';
|
||||
import { LoaderIcon } from './icons';
|
||||
import { guestRegex } from '@/lib/constants';
|
||||
|
||||
export function SidebarUserNav({ user }: { user: User }) {
|
||||
const router = useRouter();
|
||||
const { data, status } = useSession();
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
const isGuest = guestRegex.test(data?.user?.email ?? '');
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton className="data-[state=open]:bg-sidebar-accent bg-background data-[state=open]:text-sidebar-accent-foreground h-10">
|
||||
<Image
|
||||
src={`https://avatar.vercel.sh/${user.email}`}
|
||||
alt={user.email ?? 'User Avatar'}
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<span className="truncate">{user?.email}</span>
|
||||
<ChevronUp className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
{status === 'loading' ? (
|
||||
<SidebarMenuButton className="data-[state=open]:bg-sidebar-accent bg-background data-[state=open]:text-sidebar-accent-foreground h-10 justify-between">
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="size-6 bg-zinc-500/30 rounded-full animate-pulse" />
|
||||
<span className="bg-zinc-500/30 text-transparent rounded-md animate-pulse">
|
||||
Loading auth status
|
||||
</span>
|
||||
</div>
|
||||
<div className="animate-spin text-zinc-500">
|
||||
<LoaderIcon />
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
) : (
|
||||
<SidebarMenuButton
|
||||
data-testid="user-nav-button"
|
||||
className="data-[state=open]:bg-sidebar-accent bg-background data-[state=open]:text-sidebar-accent-foreground h-10"
|
||||
>
|
||||
<Image
|
||||
src={`https://avatar.vercel.sh/${user.email}`}
|
||||
alt={user.email ?? 'User Avatar'}
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<span data-testid="user-email" className="truncate">
|
||||
{isGuest ? 'Guest' : user?.email}
|
||||
</span>
|
||||
<ChevronUp className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
data-testid="user-nav-menu"
|
||||
side="top"
|
||||
className="w-[--radix-popper-anchor-width]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
data-testid="user-nav-item-theme"
|
||||
className="cursor-pointer"
|
||||
onSelect={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
{`Toggle ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<DropdownMenuItem asChild data-testid="user-nav-item-auth">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full cursor-pointer"
|
||||
onClick={() => {
|
||||
signOut({
|
||||
redirectTo: '/',
|
||||
});
|
||||
if (status === 'loading') {
|
||||
toast({
|
||||
type: 'error',
|
||||
description:
|
||||
'Checking authentication status, please try again!',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGuest) {
|
||||
router.push('/login');
|
||||
} else {
|
||||
signOut({
|
||||
redirectTo: '/',
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
{isGuest ? 'Login to your account' : 'Sign out'}
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { toast as sonnerToast } from 'sonner';
|
||||
import { CheckCircleFillIcon, WarningIcon } from './icons';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const iconsByType: Record<'success' | 'error', ReactNode> = {
|
||||
success: <CheckCircleFillIcon />,
|
||||
|
|
@ -18,20 +19,48 @@ export function toast(props: Omit<ToastProps, 'id'>) {
|
|||
function Toast(props: ToastProps) {
|
||||
const { id, type, description } = props;
|
||||
|
||||
const descriptionRef = useRef<HTMLDivElement>(null);
|
||||
const [multiLine, setMultiLine] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = descriptionRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const update = () => {
|
||||
const lineHeight = Number.parseFloat(getComputedStyle(el).lineHeight);
|
||||
const lines = Math.round(el.scrollHeight / lineHeight);
|
||||
setMultiLine(lines > 1);
|
||||
};
|
||||
|
||||
update(); // initial check
|
||||
const ro = new ResizeObserver(update); // re-check on width changes
|
||||
ro.observe(el);
|
||||
|
||||
return () => ro.disconnect();
|
||||
}, [description]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full toast-mobile:w-[356px] justify-center">
|
||||
<div
|
||||
data-testid="toast"
|
||||
key={id}
|
||||
className="bg-zinc-100 p-3 rounded-lg w-full toast-mobile:w-fit flex flex-row gap-2 items-center"
|
||||
className={cn(
|
||||
'bg-zinc-100 p-3 rounded-lg w-full toast-mobile:w-fit flex flex-row gap-3',
|
||||
multiLine ? 'items-start' : 'items-center',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-type={type}
|
||||
className="data-[type=error]:text-red-600 data-[type=success]:text-green-600"
|
||||
className={cn(
|
||||
'data-[type=error]:text-red-600 data-[type=success]:text-green-600',
|
||||
{ 'pt-1': multiLine },
|
||||
)}
|
||||
>
|
||||
{iconsByType[type]}
|
||||
</div>
|
||||
<div className="text-zinc-950 text-sm">{description}</div>
|
||||
<div ref={descriptionRef} className="text-zinc-950 text-sm">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue