From 60400e6188f178758c272f69c475fb1c2f60c00e Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Tue, 18 Mar 2025 06:33:32 -0700 Subject: [PATCH 1/4] feat: add chat config --- app/(auth)/actions.ts | 14 ++- app/(auth)/auth.config.ts | 20 ++-- app/(chat)/api/chat/route.ts | 37 ++++--- app/(chat)/api/document/route.ts | 146 ++++++++++++++++++---------- app/(chat)/chat/[id]/page.tsx | 21 ++-- app/(chat)/page.tsx | 6 +- chat.config.ts | 7 ++ components/artifact.tsx | 3 + components/chat-header.tsx | 38 +++++--- components/chat.tsx | 21 ++-- components/multimodal-input.tsx | 9 +- lib/ai/tools/create-document.ts | 2 +- lib/ai/tools/request-suggestions.ts | 4 +- lib/ai/tools/update-document.ts | 2 +- lib/artifacts/server.ts | 33 +++++-- lib/chat-config.ts | 29 ++++++ playwright.config.ts | 4 +- 17 files changed, 274 insertions(+), 122 deletions(-) create mode 100644 chat.config.ts create mode 100644 lib/chat-config.ts diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts index 84f8ffde43..c5be8b09a4 100644 --- a/app/(auth)/actions.ts +++ b/app/(auth)/actions.ts @@ -1,10 +1,8 @@ 'use server'; import { z } from 'zod'; - import { createUser, getUser } from '@/lib/db/queries'; - -import { signIn } from './auth'; +import { auth, signIn } from './auth'; const authFormSchema = z.object({ email: z.string().email(), @@ -82,3 +80,13 @@ export const register = async ( return { status: 'failed' }; } }; + +export const isAuthenticated = async () => { + const session = await auth(); + return Boolean(session?.user?.id); +}; + +export const getUserId = async () => { + const session = await auth(); + return session?.user?.id; +}; diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index cf1ecdd89e..725b1803c4 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -1,3 +1,4 @@ +import { chatConfig } from '@/lib/chat-config'; import type { NextAuthConfig } from 'next-auth'; export const authConfig = { @@ -12,21 +13,24 @@ export const authConfig = { callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; - const isOnChat = nextUrl.pathname.startsWith('/'); - const isOnRegister = nextUrl.pathname.startsWith('/register'); - const isOnLogin = nextUrl.pathname.startsWith('/login'); + const isOnRegisterPage = nextUrl.pathname.startsWith('/register'); + const isOnLoginPage = nextUrl.pathname.startsWith('/login'); + const isOnChatPage = nextUrl.pathname.startsWith('/'); - if (isLoggedIn && (isOnLogin || isOnRegister)) { + // If logged in, redirect to home page + if (isLoggedIn && (isOnLoginPage || isOnRegisterPage)) { return Response.redirect(new URL('/', nextUrl as unknown as URL)); } - if (isOnRegister || isOnLogin) { - return true; // Always allow access to register and login pages + // Always allow access to register and login pages + if (isOnRegisterPage || isOnLoginPage) { + return true; } - if (isOnChat) { + // Redirect unauthenticated users to login page + if (isOnChatPage && !chatConfig.allowGuestUsage) { if (isLoggedIn) return true; - return false; // Redirect unauthenticated users to login page + return false; } if (isLoggedIn) { diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 830ae2c35e..bd815238fa 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -25,6 +25,7 @@ import { requestSuggestions } from '@/lib/ai/tools/request-suggestions'; import { getWeather } from '@/lib/ai/tools/get-weather'; import { isProductionEnvironment } from '@/lib/constants'; import { myProvider } from '@/lib/ai/providers'; +import { chatConfig } from '@/lib/chat-config'; export const maxDuration = 60; @@ -42,10 +43,13 @@ export async function POST(request: Request) { const session = await auth(); - if (!session || !session.user || !session.user.id) { + if (!chatConfig.allowGuestUsage && !session?.user?.id) { return new Response('Unauthorized', { status: 401 }); } + const userId = session?.user?.id; + const isAuthenticated = !!userId; + const userMessage = getMostRecentUserMessage(messages); if (!userMessage) { @@ -59,25 +63,26 @@ export async function POST(request: Request) { message: userMessage, }); - await saveChat({ id, userId: session.user.id, title }); + if (isAuthenticated) await saveChat({ id, userId, title }); } else { - if (chat.userId !== session.user.id) { + if (chat.userId !== userId) { return new Response('Unauthorized', { status: 401 }); } } - await saveMessages({ - messages: [ - { - chatId: id, - id: userMessage.id, - role: 'user', - parts: userMessage.parts, - attachments: userMessage.experimental_attachments ?? [], - createdAt: new Date(), - }, - ], - }); + if (isAuthenticated) + await saveMessages({ + messages: [ + { + chatId: id, + id: userMessage.id, + role: 'user', + parts: userMessage.parts, + attachments: userMessage.experimental_attachments ?? [], + createdAt: new Date(), + }, + ], + }); return createDataStreamResponse({ execute: (dataStream) => { @@ -107,7 +112,7 @@ export async function POST(request: Request) { }), }, onFinish: async ({ response }) => { - if (session.user?.id) { + if (isAuthenticated) { try { const assistantId = getTrailingMessageId({ messages: response.messages.filter( diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index 09f06687da..e03842405b 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,4 +1,5 @@ import { auth } from '@/app/(auth)/auth'; +import { chatConfig } from '@/lib/chat-config'; import { ArtifactKind } from '@/components/artifact'; import { deleteDocumentsByIdAfterTimestamp, @@ -6,7 +7,7 @@ import { saveDocument, } from '@/lib/db/queries'; -export async function GET(request: Request) { +export async function POST(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); @@ -14,28 +15,49 @@ export async function GET(request: Request) { return new Response('Missing id', { status: 400 }); } - const session = await auth(); + const { + content, + title, + kind, + }: { content: string; title: string; kind: ArtifactKind } = + await request.json(); - if (!session || !session.user) { - return new Response('Unauthorized', { status: 401 }); - } + if (chatConfig.allowGuestUsage) { + const guestUserId = process.env.GUEST_USER_ID; - const documents = await getDocumentsById({ id }); + if (!guestUserId) { + throw new Error('Guest user ID is not set!'); + } - const [document] = documents; + const document = await saveDocument({ + id, + content, + title, + kind, + userId: guestUserId, + }); - if (!document) { - return new Response('Not Found', { status: 404 }); - } + return Response.json(document, { status: 200 }); + } else { + const session = await auth(); - if (document.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); - } + if (!session?.user?.id) { + return new Response('Unauthorized', { status: 401 }); + } - return Response.json(documents, { status: 200 }); + const document = await saveDocument({ + id, + content, + title, + kind, + userId: session.user.id, + }); + + return Response.json(document, { status: 200 }); + } } -export async function POST(request: Request) { +export async function GET(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); @@ -43,32 +65,40 @@ export async function POST(request: Request) { return new Response('Missing id', { status: 400 }); } - const session = await auth(); + if (chatConfig.allowGuestUsage) { + const documents = await getDocumentsById({ id }); + const [document] = documents; - if (!session) { - return new Response('Unauthorized', { status: 401 }); - } + if (!document) { + return new Response('Not Found', { status: 404 }); + } - const { - content, - title, - kind, - }: { content: string; title: string; kind: ArtifactKind } = - await request.json(); + if (document.userId !== process.env.GUEST_USER_ID) { + return new Response('Unauthorized', { status: 401 }); + } - if (session.user?.id) { - const document = await saveDocument({ - id, - content, - title, - kind, - userId: session.user.id, - }); + return Response.json(documents, { status: 200 }); + } else { + const session = await auth(); - return Response.json(document, { status: 200 }); - } + if (!session?.user?.id) { + return new Response('Unauthorized', { status: 401 }); + } - return new Response('Unauthorized', { status: 401 }); + const documents = await getDocumentsById({ id }); + + const [document] = documents; + + if (!document) { + return new Response('Not Found', { status: 404 }); + } + + if (document.userId !== session.user.id) { + return new Response('Unauthorized', { status: 401 }); + } + + return Response.json(documents, { status: 200 }); + } } export async function PATCH(request: Request) { @@ -81,24 +111,40 @@ export async function PATCH(request: Request) { return new Response('Missing id', { status: 400 }); } - const session = await auth(); + if (chatConfig.allowGuestUsage) { + const documents = await getDocumentsById({ id }); + const [document] = documents; - if (!session || !session.user) { - return new Response('Unauthorized', { status: 401 }); - } + if (document.userId !== process.env.GUEST_USER_ID) { + return new Response('Unauthorized', { status: 401 }); + } - const documents = await getDocumentsById({ id }); + await deleteDocumentsByIdAfterTimestamp({ + id, + timestamp: new Date(timestamp), + }); - const [document] = documents; + return new Response('Deleted', { status: 200 }); + } else { + const session = await auth(); - if (document.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); - } + if (!session?.user?.id) { + return new Response('Unauthorized', { status: 401 }); + } + + const documents = await getDocumentsById({ id }); - await deleteDocumentsByIdAfterTimestamp({ - id, - timestamp: new Date(timestamp), - }); + const [document] = documents; - return new Response('Deleted', { status: 200 }); + if (document.userId !== session.user.id) { + return new Response('Unauthorized', { status: 401 }); + } + + await deleteDocumentsByIdAfterTimestamp({ + id, + timestamp: new Date(timestamp), + }); + + return new Response('Deleted', { status: 200 }); + } } diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index 66c027a53b..cf722798a7 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -1,13 +1,13 @@ import { cookies } from 'next/headers'; import { notFound } from 'next/navigation'; -import { auth } from '@/app/(auth)/auth'; import { Chat } from '@/components/chat'; import { getChatById, getMessagesByChatId } from '@/lib/db/queries'; import { DataStreamHandler } from '@/components/data-stream-handler'; import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; import { DBMessage } from '@/lib/db/schema'; import { Attachment, UIMessage } from 'ai'; +import { getUserId, isAuthenticated } from '@/app/(auth)/actions'; export default async function Page(props: { params: Promise<{ id: string }> }) { const params = await props.params; @@ -18,16 +18,12 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { notFound(); } - const session = await auth(); - - if (chat.visibility === 'private') { - if (!session || !session.user) { - return notFound(); - } + if (chat.visibility === 'private' && !isAuthenticated()) { + return notFound(); + } - if (session.user.id !== chat.userId) { - return notFound(); - } + if (chat.visibility === 'private' && (await getUserId()) !== chat.userId) { + return notFound(); } const messagesFromDb = await getMessagesByChatId({ @@ -49,6 +45,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { const cookieStore = await cookies(); const chatModelFromCookie = cookieStore.get('chat-model'); + const isGuest = !(await isAuthenticated()); if (!chatModelFromCookie) { return ( @@ -58,7 +55,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { initialMessages={convertToUIMessages(messagesFromDb)} selectedChatModel={DEFAULT_CHAT_MODEL} selectedVisibilityType={chat.visibility} - isReadonly={session?.user?.id !== chat.userId} + isGuest={isGuest} /> @@ -72,7 +69,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { initialMessages={convertToUIMessages(messagesFromDb)} selectedChatModel={chatModelFromCookie.value} selectedVisibilityType={chat.visibility} - isReadonly={session?.user?.id !== chat.userId} + isGuest={isGuest} /> diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx index 9fbc743ba6..b5deb598d8 100644 --- a/app/(chat)/page.tsx +++ b/app/(chat)/page.tsx @@ -4,12 +4,14 @@ import { Chat } from '@/components/chat'; import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; import { generateUUID } from '@/lib/utils'; import { DataStreamHandler } from '@/components/data-stream-handler'; +import { isAuthenticated } from '../(auth)/actions'; export default async function Page() { const id = generateUUID(); const cookieStore = await cookies(); const modelIdFromCookie = cookieStore.get('chat-model'); + const isGuest = !(await isAuthenticated()); if (!modelIdFromCookie) { return ( @@ -20,7 +22,7 @@ export default async function Page() { initialMessages={[]} selectedChatModel={DEFAULT_CHAT_MODEL} selectedVisibilityType="private" - isReadonly={false} + isGuest={isGuest} /> @@ -35,7 +37,7 @@ export default async function Page() { initialMessages={[]} selectedChatModel={modelIdFromCookie.value} selectedVisibilityType="private" - isReadonly={false} + isGuest={isGuest} /> diff --git a/chat.config.ts b/chat.config.ts new file mode 100644 index 0000000000..3917a26c5c --- /dev/null +++ b/chat.config.ts @@ -0,0 +1,7 @@ +import { ChatConfig } from './lib/chat-config'; + +const config: ChatConfig = { + allowGuestUsage: true, +}; + +export default config; diff --git a/components/artifact.tsx b/components/artifact.tsx index fd3c281cdd..804412c443 100644 --- a/components/artifact.tsx +++ b/components/artifact.tsx @@ -66,6 +66,7 @@ function PureArtifact({ reload, votes, isReadonly, + isGuest, }: { chatId: string; input: string; @@ -81,6 +82,7 @@ function PureArtifact({ handleSubmit: UseChatHelpers['handleSubmit']; reload: UseChatHelpers['reload']; isReadonly: boolean; + isGuest: boolean; }) { const { artifact, setArtifact, metadata, setMetadata } = useArtifact(); @@ -335,6 +337,7 @@ function PureArtifact({ append={append} className="bg-background dark:bg-muted" setMessages={setMessages} + isGuest={isGuest} /> diff --git a/components/chat-header.tsx b/components/chat-header.tsx index 8a4d69e988..c84a51429c 100644 --- a/components/chat-header.tsx +++ b/components/chat-header.tsx @@ -12,17 +12,20 @@ import { useSidebar } from './ui/sidebar'; import { memo } from 'react'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { VisibilityType, VisibilitySelector } from './visibility-selector'; +import { UserIcon } from 'lucide-react'; function PureChatHeader({ chatId, selectedModelId, selectedVisibilityType, isReadonly, + isGuest, }: { chatId: string; selectedModelId: string; selectedVisibilityType: VisibilityType; isReadonly: boolean; + isGuest: boolean; }) { const router = useRouter(); const { open } = useSidebar(); @@ -67,18 +70,31 @@ function PureChatHeader({ /> )} - + + + Deploy with Vercel + + + {isGuest && ( + + )} + ); } diff --git a/components/chat.tsx b/components/chat.tsx index 4926119e71..d323923b92 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -19,13 +19,13 @@ export function Chat({ initialMessages, selectedChatModel, selectedVisibilityType, - isReadonly, + isGuest, }: { id: string; initialMessages: Array; selectedChatModel: string; selectedVisibilityType: VisibilityType; - isReadonly: boolean; + isGuest: boolean; }) { const { mutate } = useSWRConfig(); @@ -47,20 +47,26 @@ export function Chat({ sendExtraMessageFields: true, generateId: generateUUID, onFinish: () => { - mutate('/api/history'); + if (selectedVisibilityType === 'private' && !isGuest) { + mutate('/api/history'); + } }, onError: () => { toast.error('An error occured, please try again!'); }, }); + const canPollVotes = + !isGuest && selectedVisibilityType === 'private' && messages.length >= 2; + const { data: votes } = useSWR>( - messages.length >= 2 ? `/api/vote?chatId=${id}` : null, + canPollVotes ? `/api/vote?chatId=${id}` : null, fetcher, ); const [attachments, setAttachments] = useState>([]); const isArtifactVisible = useArtifactSelector((state) => state.isVisible); + const isReadonly = isGuest || selectedVisibilityType === 'public'; return ( <> @@ -70,6 +76,7 @@ export function Chat({ selectedModelId={selectedChatModel} selectedVisibilityType={selectedVisibilityType} isReadonly={isReadonly} + isGuest={isGuest} />
- {!isReadonly && ( + {selectedVisibilityType === 'private' && ( )} @@ -117,6 +125,7 @@ export function Chat({ reload={reload} votes={votes} isReadonly={isReadonly} + isGuest={isGuest} /> ); diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index d47a7a8ce7..cb62992418 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -22,7 +22,7 @@ import { Button } from './ui/button'; import { Textarea } from './ui/textarea'; import { SuggestedActions } from './suggested-actions'; import equal from 'fast-deep-equal'; -import { UseChatHelpers, UseChatOptions } from '@ai-sdk/react'; +import { UseChatHelpers } from '@ai-sdk/react'; function PureMultimodalInput({ chatId, @@ -36,6 +36,7 @@ function PureMultimodalInput({ setMessages, append, handleSubmit, + isGuest, className, }: { chatId: string; @@ -49,6 +50,7 @@ function PureMultimodalInput({ setMessages: Dispatch>>; append: UseChatHelpers['append']; handleSubmit: UseChatHelpers['handleSubmit']; + isGuest: boolean; className?: string; }) { const textareaRef = useRef(null); @@ -104,7 +106,9 @@ function PureMultimodalInput({ const [uploadQueue, setUploadQueue] = useState>([]); const submitForm = useCallback(() => { - window.history.replaceState({}, '', `/chat/${chatId}`); + if (!isGuest) { + window.history.replaceState({}, '', `/chat/${chatId}`); + } handleSubmit(undefined, { experimental_attachments: attachments, @@ -124,6 +128,7 @@ function PureMultimodalInput({ setLocalStorageInput, width, chatId, + isGuest, ]); const uploadFile = async (file: File) => { diff --git a/lib/ai/tools/create-document.ts b/lib/ai/tools/create-document.ts index 40c9ddd9a6..c4480ba0b5 100644 --- a/lib/ai/tools/create-document.ts +++ b/lib/ai/tools/create-document.ts @@ -8,7 +8,7 @@ import { } from '@/lib/artifacts/server'; interface CreateDocumentProps { - session: Session; + session: Session | null; dataStream: DataStreamWriter; } diff --git a/lib/ai/tools/request-suggestions.ts b/lib/ai/tools/request-suggestions.ts index 6248f4e7ee..ffff6167e1 100644 --- a/lib/ai/tools/request-suggestions.ts +++ b/lib/ai/tools/request-suggestions.ts @@ -7,7 +7,7 @@ import { generateUUID } from '@/lib/utils'; import { myProvider } from '../providers'; interface RequestSuggestionsProps { - session: Session; + session: Session | null; dataStream: DataStreamWriter; } @@ -66,7 +66,7 @@ export const requestSuggestions = ({ suggestions.push(suggestion); } - if (session.user?.id) { + if (session?.user?.id) { const userId = session.user.id; await saveSuggestions({ diff --git a/lib/ai/tools/update-document.ts b/lib/ai/tools/update-document.ts index 1f858fe86d..ace74a1578 100644 --- a/lib/ai/tools/update-document.ts +++ b/lib/ai/tools/update-document.ts @@ -5,7 +5,7 @@ import { getDocumentById, saveDocument } from '@/lib/db/queries'; import { documentHandlersByArtifactKind } from '@/lib/artifacts/server'; interface UpdateDocumentProps { - session: Session; + session: Session | null; dataStream: DataStreamWriter; } diff --git a/lib/artifacts/server.ts b/lib/artifacts/server.ts index a2896e4287..a590e234a7 100644 --- a/lib/artifacts/server.ts +++ b/lib/artifacts/server.ts @@ -7,6 +7,7 @@ import { DataStreamWriter } from 'ai'; import { Document } from '../db/schema'; import { saveDocument } from '../db/queries'; import { Session } from 'next-auth'; +import { chatConfig } from '../chat-config'; export interface SaveDocumentProps { id: string; @@ -20,14 +21,14 @@ export interface CreateDocumentCallbackProps { id: string; title: string; dataStream: DataStreamWriter; - session: Session; + session: Session | null; } export interface UpdateDocumentCallbackProps { document: Document; description: string; dataStream: DataStreamWriter; - session: Session; + session: Session | null; } export interface DocumentHandler { @@ -51,14 +52,24 @@ export function createDocumentHandler(config: { session: args.session, }); - if (args.session?.user?.id) { + const saveDocumentByUserId = async (userId: string) => { await saveDocument({ id: args.id, title: args.title, content: draftContent, kind: config.kind, - userId: args.session.user.id, + userId: userId, }); + }; + + if (args.session?.user?.id) { + await saveDocumentByUserId(args.session.user.id); + } else if (chatConfig.allowGuestUsage) { + if (!process.env.GUEST_USER_ID) { + throw new Error('Guest user ID not set!'); + } + + await saveDocumentByUserId(process.env.GUEST_USER_ID); } return; @@ -71,14 +82,24 @@ export function createDocumentHandler(config: { session: args.session, }); - if (args.session?.user?.id) { + const saveDocumentByUserId = async (userId: string) => { await saveDocument({ id: args.document.id, title: args.document.title, content: draftContent, kind: config.kind, - userId: args.session.user.id, + userId: userId, }); + }; + + if (args.session?.user?.id) { + await saveDocumentByUserId(args.session.user.id); + } else if (chatConfig.allowGuestUsage) { + if (!process.env.GUEST_USER_ID) { + throw new Error('Guest user ID not set!'); + } + + await saveDocumentByUserId(process.env.GUEST_USER_ID); } return; diff --git a/lib/chat-config.ts b/lib/chat-config.ts new file mode 100644 index 0000000000..64f7666e3b --- /dev/null +++ b/lib/chat-config.ts @@ -0,0 +1,29 @@ +import configFromProject from '../chat.config'; + +export interface ChatConfig { + /** + * Whether guests are allowed to use the application without authentication. + * Defaults to true. + * + * Note: You should also set the environment variable GUEST_USER_ID to a valid user ID to enable guest usage. + */ + allowGuestUsage: boolean; +} + +function getConfig() { + if (process.env.PLAYWRIGHT) { + return { + ...configFromProject, + allowGuestUsage: process.env.ALLOW_GUEST_USAGE === 'True', + }; + } + + return { + ...configFromProject, + allowGuestUsage: process.env.GUEST_USER_ID + ? configFromProject.allowGuestUsage + : false, + }; +} + +export const chatConfig: ChatConfig = getConfig(); diff --git a/playwright.config.ts b/playwright.config.ts index bc855c9c57..707386ebc4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -44,9 +44,9 @@ export default defineConfig({ }, /* Configure global timeout for each test */ - timeout: 60 * 1000, // 30 seconds + timeout: 15 * 1000, // 30 seconds expect: { - timeout: 60 * 1000, + timeout: 15 * 1000, }, /* Configure projects */ From f9705c07327a6403ede9e8a531dd54db15b66f11 Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Wed, 19 Mar 2025 13:00:01 -0700 Subject: [PATCH 2/4] feat: add chat config --- app/(auth)/auth.config.ts | 20 ++--- app/(chat)/api/chat/route.ts | 72 ++++++++--------- app/(chat)/api/document/route.ts | 50 ++++++------ app/(chat)/api/files/upload/route.ts | 35 +++++---- chat.config.ts | 7 +- lib/artifacts/server.ts | 30 ++++---- lib/chat-config.ts | 43 ++++++++--- lib/constants.ts | 9 +-- package.json | 2 +- playwright.config.ts | 83 ++++++++++++-------- tests/auth.setup.ts | 28 +++---- tests/chat.guest.test.ts | 111 +++++++++++++++++++++++++++ tests/pages/chat.ts | 69 ++++++++--------- 13 files changed, 352 insertions(+), 207 deletions(-) create mode 100644 tests/chat.guest.test.ts diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index 725b1803c4..6aa150e0c7 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -1,10 +1,10 @@ -import { chatConfig } from '@/lib/chat-config'; -import type { NextAuthConfig } from 'next-auth'; +import { chatConfig } from "@/lib/chat-config"; +import type { NextAuthConfig } from "next-auth"; export const authConfig = { pages: { - signIn: '/login', - newUser: '/', + signIn: "/login", + newUser: "/", }, providers: [ // added later in auth.ts since it requires bcrypt which is only compatible with Node.js @@ -13,13 +13,13 @@ export const authConfig = { callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; - const isOnRegisterPage = nextUrl.pathname.startsWith('/register'); - const isOnLoginPage = nextUrl.pathname.startsWith('/login'); - const isOnChatPage = nextUrl.pathname.startsWith('/'); + const isOnRegisterPage = nextUrl.pathname.startsWith("/register"); + const isOnLoginPage = nextUrl.pathname.startsWith("/login"); + const isOnChatPage = nextUrl.pathname.startsWith("/"); // If logged in, redirect to home page if (isLoggedIn && (isOnLoginPage || isOnRegisterPage)) { - return Response.redirect(new URL('/', nextUrl as unknown as URL)); + return Response.redirect(new URL("/", nextUrl as unknown as URL)); } // Always allow access to register and login pages @@ -28,13 +28,13 @@ export const authConfig = { } // Redirect unauthenticated users to login page - if (isOnChatPage && !chatConfig.allowGuestUsage) { + if (isOnChatPage && !chatConfig.guestUsage.isEnabled) { if (isLoggedIn) return true; return false; } if (isLoggedIn) { - return Response.redirect(new URL('/', nextUrl as unknown as URL)); + return Response.redirect(new URL("/", nextUrl as unknown as URL)); } return true; diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index bd815238fa..7b0eb9811c 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -4,28 +4,28 @@ import { createDataStreamResponse, smoothStream, streamText, -} from 'ai'; -import { auth } from '@/app/(auth)/auth'; -import { systemPrompt } from '@/lib/ai/prompts'; +} from "ai"; +import { auth } from "@/app/(auth)/auth"; +import { systemPrompt } from "@/lib/ai/prompts"; import { deleteChatById, getChatById, saveChat, saveMessages, -} from '@/lib/db/queries'; +} from "@/lib/db/queries"; import { generateUUID, getMostRecentUserMessage, getTrailingMessageId, -} from '@/lib/utils'; -import { generateTitleFromUserMessage } from '../../actions'; -import { createDocument } from '@/lib/ai/tools/create-document'; -import { updateDocument } from '@/lib/ai/tools/update-document'; -import { requestSuggestions } from '@/lib/ai/tools/request-suggestions'; -import { getWeather } from '@/lib/ai/tools/get-weather'; -import { isProductionEnvironment } from '@/lib/constants'; -import { myProvider } from '@/lib/ai/providers'; -import { chatConfig } from '@/lib/chat-config'; +} from "@/lib/utils"; +import { generateTitleFromUserMessage } from "../../actions"; +import { createDocument } from "@/lib/ai/tools/create-document"; +import { updateDocument } from "@/lib/ai/tools/update-document"; +import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; +import { getWeather } from "@/lib/ai/tools/get-weather"; +import { isProductionEnvironment } from "@/lib/constants"; +import { myProvider } from "@/lib/ai/providers"; +import { chatConfig } from "@/lib/chat-config"; export const maxDuration = 60; @@ -43,8 +43,8 @@ export async function POST(request: Request) { const session = await auth(); - if (!chatConfig.allowGuestUsage && !session?.user?.id) { - return new Response('Unauthorized', { status: 401 }); + if (!chatConfig.guestUsage.isEnabled && !session?.user?.id) { + return new Response("Unauthorized", { status: 401 }); } const userId = session?.user?.id; @@ -53,7 +53,7 @@ export async function POST(request: Request) { const userMessage = getMostRecentUserMessage(messages); if (!userMessage) { - return new Response('No user message found', { status: 400 }); + return new Response("No user message found", { status: 400 }); } const chat = await getChatById({ id }); @@ -66,7 +66,7 @@ export async function POST(request: Request) { if (isAuthenticated) await saveChat({ id, userId, title }); } else { if (chat.userId !== userId) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } } @@ -76,7 +76,7 @@ export async function POST(request: Request) { { chatId: id, id: userMessage.id, - role: 'user', + role: "user", parts: userMessage.parts, attachments: userMessage.experimental_attachments ?? [], createdAt: new Date(), @@ -92,15 +92,15 @@ export async function POST(request: Request) { messages, maxSteps: 5, experimental_activeTools: - selectedChatModel === 'chat-model-reasoning' + selectedChatModel === "chat-model-reasoning" ? [] : [ - 'getWeather', - 'createDocument', - 'updateDocument', - 'requestSuggestions', + "getWeather", + "createDocument", + "updateDocument", + "requestSuggestions", ], - experimental_transform: smoothStream({ chunking: 'word' }), + experimental_transform: smoothStream({ chunking: "word" }), experimental_generateMessageId: generateUUID, tools: { getWeather, @@ -116,12 +116,12 @@ export async function POST(request: Request) { try { const assistantId = getTrailingMessageId({ messages: response.messages.filter( - (message) => message.role === 'assistant', + (message) => message.role === "assistant", ), }); if (!assistantId) { - throw new Error('No assistant message found!'); + throw new Error("No assistant message found!"); } const [, assistantMessage] = appendResponseMessages({ @@ -143,13 +143,13 @@ export async function POST(request: Request) { ], }); } catch (error) { - console.error('Failed to save chat'); + console.error("Failed to save chat"); } } }, experimental_telemetry: { isEnabled: isProductionEnvironment, - functionId: 'stream-text', + functionId: "stream-text", }, }); @@ -160,11 +160,11 @@ export async function POST(request: Request) { }); }, onError: () => { - return 'Oops, an error occured!'; + return "Oops, an error occured!"; }, }); } catch (error) { - return new Response('An error occurred while processing your request!', { + return new Response("An error occurred while processing your request!", { status: 404, }); } @@ -172,30 +172,30 @@ export async function POST(request: Request) { export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); + const id = searchParams.get("id"); if (!id) { - return new Response('Not Found', { status: 404 }); + return new Response("Not Found", { status: 404 }); } const session = await auth(); if (!session || !session.user) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } try { const chat = await getChatById({ id }); if (chat.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } await deleteChatById({ id }); - return new Response('Chat deleted', { status: 200 }); + return new Response("Chat deleted", { status: 200 }); } catch (error) { - return new Response('An error occurred while processing your request!', { + return new Response("An error occurred while processing your request!", { status: 500, }); } diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index e03842405b..7ee4f282b2 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,18 +1,18 @@ -import { auth } from '@/app/(auth)/auth'; -import { chatConfig } from '@/lib/chat-config'; -import { ArtifactKind } from '@/components/artifact'; +import { auth } from "@/app/(auth)/auth"; +import { chatConfig } from "@/lib/chat-config"; +import { ArtifactKind } from "@/components/artifact"; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, saveDocument, -} from '@/lib/db/queries'; +} from "@/lib/db/queries"; export async function POST(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); + const id = searchParams.get("id"); if (!id) { - return new Response('Missing id', { status: 400 }); + return new Response("Missing id", { status: 400 }); } const { @@ -22,11 +22,11 @@ export async function POST(request: Request) { }: { content: string; title: string; kind: ArtifactKind } = await request.json(); - if (chatConfig.allowGuestUsage) { + if (chatConfig.guestUsage.isEnabled) { const guestUserId = process.env.GUEST_USER_ID; if (!guestUserId) { - throw new Error('Guest user ID is not set!'); + throw new Error("Guest user ID is not set!"); } const document = await saveDocument({ @@ -42,7 +42,7 @@ export async function POST(request: Request) { const session = await auth(); if (!session?.user?.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } const document = await saveDocument({ @@ -59,22 +59,22 @@ export async function POST(request: Request) { export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); + const id = searchParams.get("id"); if (!id) { - return new Response('Missing id', { status: 400 }); + return new Response("Missing id", { status: 400 }); } - if (chatConfig.allowGuestUsage) { + if (chatConfig.guestUsage.isEnabled) { const documents = await getDocumentsById({ id }); const [document] = documents; if (!document) { - return new Response('Not Found', { status: 404 }); + return new Response("Not Found", { status: 404 }); } if (document.userId !== process.env.GUEST_USER_ID) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } return Response.json(documents, { status: 200 }); @@ -82,7 +82,7 @@ export async function GET(request: Request) { const session = await auth(); if (!session?.user?.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } const documents = await getDocumentsById({ id }); @@ -90,11 +90,11 @@ export async function GET(request: Request) { const [document] = documents; if (!document) { - return new Response('Not Found', { status: 404 }); + return new Response("Not Found", { status: 404 }); } if (document.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } return Response.json(documents, { status: 200 }); @@ -103,20 +103,20 @@ export async function GET(request: Request) { export async function PATCH(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get('id'); + const id = searchParams.get("id"); const { timestamp }: { timestamp: string } = await request.json(); if (!id) { - return new Response('Missing id', { status: 400 }); + return new Response("Missing id", { status: 400 }); } - if (chatConfig.allowGuestUsage) { + if (chatConfig.guestUsage.isEnabled) { const documents = await getDocumentsById({ id }); const [document] = documents; if (document.userId !== process.env.GUEST_USER_ID) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } await deleteDocumentsByIdAfterTimestamp({ @@ -124,12 +124,12 @@ export async function PATCH(request: Request) { timestamp: new Date(timestamp), }); - return new Response('Deleted', { status: 200 }); + return new Response("Deleted", { status: 200 }); } else { const session = await auth(); if (!session?.user?.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } const documents = await getDocumentsById({ id }); @@ -137,7 +137,7 @@ export async function PATCH(request: Request) { const [document] = documents; if (document.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + return new Response("Unauthorized", { status: 401 }); } await deleteDocumentsByIdAfterTimestamp({ @@ -145,6 +145,6 @@ export async function PATCH(request: Request) { timestamp: new Date(timestamp), }); - return new Response('Deleted', { status: 200 }); + return new Response("Deleted", { status: 200 }); } } diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts index 699a4cbef8..f262bea3e5 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/(chat)/api/files/upload/route.ts @@ -1,39 +1,40 @@ -import { put } from '@vercel/blob'; -import { NextResponse } from 'next/server'; -import { z } from 'zod'; +import { put } from "@vercel/blob"; +import { NextResponse } from "next/server"; +import { z } from "zod"; -import { auth } from '@/app/(auth)/auth'; +import { auth } from "@/app/(auth)/auth"; +import { chatConfig } from "@/lib/chat-config"; // Use Blob instead of File since File is not available in Node.js environment const FileSchema = z.object({ file: z .instanceof(Blob) .refine((file) => file.size <= 5 * 1024 * 1024, { - message: 'File size should be less than 5MB', + message: "File size should be less than 5MB", }) // Update the file type based on the kind of files you want to accept - .refine((file) => ['image/jpeg', 'image/png'].includes(file.type), { - message: 'File type should be JPEG or PNG', + .refine((file) => ["image/jpeg", "image/png"].includes(file.type), { + message: "File type should be JPEG or PNG", }), }); export async function POST(request: Request) { const session = await auth(); - if (!session) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + if (!session && !chatConfig.guestUsage.isEnabled) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (request.body === null) { - return new Response('Request body is empty', { status: 400 }); + return new Response("Request body is empty", { status: 400 }); } try { const formData = await request.formData(); - const file = formData.get('file') as Blob; + const file = formData.get("file") as Blob; if (!file) { - return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); + return NextResponse.json({ error: "No file uploaded" }, { status: 400 }); } const validatedFile = FileSchema.safeParse({ file }); @@ -41,27 +42,27 @@ export async function POST(request: Request) { if (!validatedFile.success) { const errorMessage = validatedFile.error.errors .map((error) => error.message) - .join(', '); + .join(", "); return NextResponse.json({ error: errorMessage }, { status: 400 }); } // Get filename from formData since Blob doesn't have name property - const filename = (formData.get('file') as File).name; + const filename = (formData.get("file") as File).name; const fileBuffer = await file.arrayBuffer(); try { const data = await put(`${filename}`, fileBuffer, { - access: 'public', + access: "public", }); return NextResponse.json(data); } catch (error) { - return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); + return NextResponse.json({ error: "Upload failed" }, { status: 500 }); } } catch (error) { return NextResponse.json( - { error: 'Failed to process request' }, + { error: "Failed to process request" }, { status: 500 }, ); } diff --git a/chat.config.ts b/chat.config.ts index 3917a26c5c..e4c8012ac1 100644 --- a/chat.config.ts +++ b/chat.config.ts @@ -1,7 +1,10 @@ -import { ChatConfig } from './lib/chat-config'; +import { ChatConfig } from "./lib/chat-config"; const config: ChatConfig = { - allowGuestUsage: true, + guestUsage: { + isEnabled: true, + userId: process.env.GUEST_USER_ID!, + }, }; export default config; diff --git a/lib/artifacts/server.ts b/lib/artifacts/server.ts index a590e234a7..73579842b9 100644 --- a/lib/artifacts/server.ts +++ b/lib/artifacts/server.ts @@ -1,13 +1,13 @@ -import { codeDocumentHandler } from '@/artifacts/code/server'; -import { imageDocumentHandler } from '@/artifacts/image/server'; -import { sheetDocumentHandler } from '@/artifacts/sheet/server'; -import { textDocumentHandler } from '@/artifacts/text/server'; -import { ArtifactKind } from '@/components/artifact'; -import { DataStreamWriter } from 'ai'; -import { Document } from '../db/schema'; -import { saveDocument } from '../db/queries'; -import { Session } from 'next-auth'; -import { chatConfig } from '../chat-config'; +import { codeDocumentHandler } from "@/artifacts/code/server"; +import { imageDocumentHandler } from "@/artifacts/image/server"; +import { sheetDocumentHandler } from "@/artifacts/sheet/server"; +import { textDocumentHandler } from "@/artifacts/text/server"; +import { ArtifactKind } from "@/components/artifact"; +import { DataStreamWriter } from "ai"; +import { Document } from "../db/schema"; +import { saveDocument } from "../db/queries"; +import { Session } from "next-auth"; +import { chatConfig } from "../chat-config"; export interface SaveDocumentProps { id: string; @@ -64,9 +64,9 @@ export function createDocumentHandler(config: { if (args.session?.user?.id) { await saveDocumentByUserId(args.session.user.id); - } else if (chatConfig.allowGuestUsage) { + } else if (chatConfig.guestUsage.isEnabled) { if (!process.env.GUEST_USER_ID) { - throw new Error('Guest user ID not set!'); + throw new Error("Guest user ID not set!"); } await saveDocumentByUserId(process.env.GUEST_USER_ID); @@ -94,9 +94,9 @@ export function createDocumentHandler(config: { if (args.session?.user?.id) { await saveDocumentByUserId(args.session.user.id); - } else if (chatConfig.allowGuestUsage) { + } else if (chatConfig.guestUsage.isEnabled) { if (!process.env.GUEST_USER_ID) { - throw new Error('Guest user ID not set!'); + throw new Error("Guest user ID not set!"); } await saveDocumentByUserId(process.env.GUEST_USER_ID); @@ -117,4 +117,4 @@ export const documentHandlersByArtifactKind: Array = [ sheetDocumentHandler, ]; -export const artifactKinds = ['text', 'code', 'image', 'sheet'] as const; +export const artifactKinds = ["text", "code", "image", "sheet"] as const; diff --git a/lib/chat-config.ts b/lib/chat-config.ts index 64f7666e3b..945ae58829 100644 --- a/lib/chat-config.ts +++ b/lib/chat-config.ts @@ -1,28 +1,49 @@ -import configFromProject from '../chat.config'; +import configFromProject from "../chat.config"; +import { isTestEnvironment } from "./constants"; export interface ChatConfig { /** * Whether guests are allowed to use the application without authentication. - * Defaults to true. - * - * Note: You should also set the environment variable GUEST_USER_ID to a valid user ID to enable guest usage. */ - allowGuestUsage: boolean; + guestUsage: { + isEnabled: boolean; + userId: string | null; + }; +} + +function getGuestUsageFromEnv() { + if ( + process.env.ALLOW_GUEST_USAGE === "True" && + process.env.GUEST_USER_ID === undefined + ) { + throw new Error("GUEST_USER_ID is required when ALLOW_GUEST_USAGE is true"); + } + + return process.env.ALLOW_GUEST_USAGE === "True" && + process.env.GUEST_USER_ID !== undefined + ? { + isEnabled: true, + userId: process.env.GUEST_USER_ID, + } + : { + isEnabled: false, + userId: null, + }; } -function getConfig() { - if (process.env.PLAYWRIGHT) { +function getConfig(): ChatConfig { + if (isTestEnvironment) { return { ...configFromProject, - allowGuestUsage: process.env.ALLOW_GUEST_USAGE === 'True', + guestUsage: getGuestUsageFromEnv(), }; } return { ...configFromProject, - allowGuestUsage: process.env.GUEST_USER_ID - ? configFromProject.allowGuestUsage - : false, + guestUsage: configFromProject.guestUsage?.userId + ? configFromProject.guestUsage + : getGuestUsageFromEnv(), }; } diff --git a/lib/constants.ts b/lib/constants.ts index 6c27325a5a..5fd719b6c9 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,7 +1,2 @@ -export const isProductionEnvironment = process.env.NODE_ENV === 'production'; - -export const isTestEnvironment = Boolean( - process.env.PLAYWRIGHT_TEST_BASE_URL || - process.env.PLAYWRIGHT || - process.env.CI_PLAYWRIGHT, -); +export const isProductionEnvironment = process.env.NODE_ENV === "production"; +export const isTestEnvironment = Boolean(process.env.PLAYWRIGHT); diff --git a/package.json b/package.json index 9d6604c3a4..ee34b60022 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "db:pull": "drizzle-kit pull", "db:check": "drizzle-kit check", "db:up": "drizzle-kit up", - "test": "export PLAYWRIGHT=True && pnpm exec playwright test --workers=4" + "test": "export PLAYWRIGHT=1 && pnpm exec playwright test --workers=4" }, "dependencies": { "@ai-sdk/fireworks": "0.1.16", diff --git a/playwright.config.ts b/playwright.config.ts index 707386ebc4..9f780cad45 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,29 +1,26 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ -import { config } from 'dotenv'; +import { config } from "dotenv"; config({ - path: '.env.local', + path: [".env.local", ".env.test"], }); -/* Use process.env.PORT by default and fallback to port 3000 */ -const PORT = process.env.PORT || 3000; - /** * Set webServer.url and use.baseURL with the location * of the WebServer respecting the correct set port */ -const baseURL = `http://localhost:${PORT}`; +const baseURL = `http://localhost`; /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './tests', + testDir: "./tests", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -33,14 +30,14 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure global timeout for each test */ @@ -52,43 +49,55 @@ export default defineConfig({ /* Configure projects */ projects: [ { - name: 'setup:auth', + name: "setup:auth", testMatch: /auth.setup.ts/, }, { - name: 'setup:reasoning', + name: "setup:reasoning", testMatch: /reasoning.setup.ts/, - dependencies: ['setup:auth'], + dependencies: ["setup:auth"], use: { - ...devices['Desktop Chrome'], - storageState: 'playwright/.auth/session.json', + ...devices["Desktop Chrome"], + baseURL: `${baseURL}:3000`, + storageState: "playwright/.auth/session.json", }, }, { - name: 'chat', + name: "chat", testMatch: /chat.test.ts/, - dependencies: ['setup:auth'], + dependencies: ["setup:auth"], + use: { + ...devices["Desktop Chrome"], + baseURL: `${baseURL}:3000`, + storageState: "playwright/.auth/session.json", + }, + }, + { + name: "chat (guest)", + testMatch: /chat.guest.test.ts/, use: { - ...devices['Desktop Chrome'], - storageState: 'playwright/.auth/session.json', + ...devices["Desktop Chrome"], + baseURL: `${baseURL}:3001`, }, }, { - name: 'reasoning', + name: "reasoning", testMatch: /reasoning.test.ts/, - dependencies: ['setup:reasoning'], + dependencies: ["setup:reasoning"], use: { - ...devices['Desktop Chrome'], - storageState: 'playwright/.reasoning/session.json', + ...devices["Desktop Chrome"], + storageState: "playwright/.reasoning/session.json", + baseURL: `${baseURL}:3000`, }, }, { - name: 'artifacts', + name: "artifacts", testMatch: /artifacts.test.ts/, - dependencies: ['setup:auth'], + dependencies: ["setup:auth"], use: { - ...devices['Desktop Chrome'], - storageState: 'playwright/.auth/session.json', + ...devices["Desktop Chrome"], + baseURL: `${baseURL}:3000`, + storageState: "playwright/.auth/session.json", }, }, @@ -124,10 +133,18 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - webServer: { - command: 'pnpm dev', - url: baseURL, - timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, - }, + webServer: [ + { + command: "pnpm dev --port=3000", + url: `${baseURL}:3000`, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, + { + command: "export ALLOW_GUEST_USAGE=True && pnpm dev --port=3001", + url: `${baseURL}:3001`, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, + ], }); diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts index 10f4b185ef..a40f3bf359 100644 --- a/tests/auth.setup.ts +++ b/tests/auth.setup.ts @@ -1,23 +1,23 @@ -import path from 'path'; -import { generateId } from 'ai'; -import { getUnixTime } from 'date-fns'; -import { expect, test as setup } from '@playwright/test'; +import path from "path"; +import { generateId } from "ai"; +import { getUnixTime } from "date-fns"; +import { expect, test as setup } from "@playwright/test"; -const authFile = path.join(__dirname, '../playwright/.auth/session.json'); +const authFile = path.join(__dirname, "../playwright/.auth/session.json"); -setup('authenticate', async ({ page }) => { +setup("authenticate", async ({ page }) => { const testEmail = `test-${getUnixTime(new Date())}@playwright.com`; const testPassword = generateId(16); - await page.goto('http://localhost:3000/register'); - await page.getByPlaceholder('user@acme.com').click(); - await page.getByPlaceholder('user@acme.com').fill(testEmail); - await page.getByLabel('Password').click(); - await page.getByLabel('Password').fill(testPassword); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.goto("http://localhost:3000/register"); + await page.getByPlaceholder("user@acme.com").click(); + await page.getByPlaceholder("user@acme.com").fill(testEmail); + await page.getByLabel("Password").click(); + await page.getByLabel("Password").fill(testPassword); + await page.getByRole("button", { name: "Sign Up" }).click(); - await expect(page.getByTestId('toast')).toContainText( - 'Account created successfully!', + await expect(page.getByTestId("toast")).toContainText( + "Account created successfully!", ); await page.context().storageState({ path: authFile }); diff --git a/tests/chat.guest.test.ts b/tests/chat.guest.test.ts new file mode 100644 index 0000000000..5ace347c9d --- /dev/null +++ b/tests/chat.guest.test.ts @@ -0,0 +1,111 @@ +import { ChatPage } from "./pages/chat"; +import { test, expect } from "@playwright/test"; + +test.describe("chat activity", () => { + let chatPage: ChatPage; + + test.beforeEach(async ({ page }) => { + chatPage = new ChatPage(page); + await chatPage.createNewChat(); + }); + + test("send a user message and receive response", async () => { + await chatPage.sendUserMessage("Why is grass green?"); + await chatPage.isGenerationComplete(); + + const assistantMessage = await chatPage.getRecentAssistantMessage(); + expect(assistantMessage.content).toContain("It's just green duh!"); + }); + + test("do not redirect to /chat/:id after submitting message", async () => { + await chatPage.sendUserMessage("Why is grass green?"); + await chatPage.isGenerationComplete(); + + const assistantMessage = await chatPage.getRecentAssistantMessage(); + expect(assistantMessage.content).toContain("It's just green duh!"); + + expect(chatPage.getCurrentURL()).toBe("http://localhost:3001/"); + }); + + test("send a user message from suggestion", async () => { + await chatPage.sendUserMessageFromSuggestion(); + await chatPage.isGenerationComplete(); + + const assistantMessage = await chatPage.getRecentAssistantMessage(); + expect(assistantMessage.content).toContain( + "With Next.js, you can ship fast!", + ); + }); + + test("hide suggested actions after sending message", async () => { + await chatPage.isElementVisible("suggested-actions"); + await chatPage.sendUserMessageFromSuggestion(); + await chatPage.isElementNotVisible("suggested-actions"); + }); + + test("toggle between send/stop button based on activity", async () => { + await expect(chatPage.sendButton).toBeVisible(); + await expect(chatPage.sendButton).toBeDisabled(); + + await chatPage.sendUserMessage("Why is grass green?"); + + await expect(chatPage.sendButton).not.toBeVisible(); + await expect(chatPage.stopButton).toBeVisible(); + + await chatPage.isGenerationComplete(); + + await expect(chatPage.stopButton).not.toBeVisible(); + await expect(chatPage.sendButton).toBeVisible(); + }); + + test("stop generation during submission", async () => { + await chatPage.sendUserMessage("Why is grass green?"); + await expect(chatPage.stopButton).toBeVisible(); + await chatPage.stopButton.click(); + await expect(chatPage.sendButton).toBeVisible(); + }); + + test("upload file and send image attachment with message", async () => { + await chatPage.addImageAttachment(); + + await chatPage.isElementVisible("attachments-preview"); + await chatPage.isElementVisible("input-attachment-loader"); + await chatPage.isElementNotVisible("input-attachment-loader"); + + await chatPage.sendUserMessage("Who painted this?"); + + const userMessage = await chatPage.getRecentUserMessage(); + expect(userMessage.attachments).toHaveLength(1); + + await chatPage.isGenerationComplete(); + + const assistantMessage = await chatPage.getRecentAssistantMessage(); + expect(assistantMessage.content).toBe("This painting is by Monet!"); + }); + + test("call weather tool", async () => { + await chatPage.sendUserMessage("What's the weather in sf?"); + await chatPage.isGenerationComplete(); + + const assistantMessage = await chatPage.getRecentAssistantMessage(); + + expect(assistantMessage.content).toBe( + "The current temperature in San Francisco is 17°C.", + ); + }); + + test("do not show message actions", async () => { + await chatPage.sendUserMessage("What's the weather in sf?"); + await chatPage.isGenerationComplete(); + + const assistantMessage = await chatPage.getRecentAssistantMessage(); + + expect(assistantMessage.content).toBe( + "The current temperature in San Francisco is 17°C.", + ); + + expect( + assistantMessage.element.getByTestId("message-actions"), + ).not.toBeVisible(); + }); +}); diff --git a/tests/pages/chat.ts b/tests/pages/chat.ts index 61a43b8db9..cfa4a5c91e 100644 --- a/tests/pages/chat.ts +++ b/tests/pages/chat.ts @@ -1,25 +1,25 @@ -import fs from 'fs'; -import path from 'path'; -import { chatModels } from '@/lib/ai/models'; -import { expect, Page } from '@playwright/test'; +import fs from "fs"; +import path from "path"; +import { chatModels } from "@/lib/ai/models"; +import { expect, Page } from "@playwright/test"; export class ChatPage { constructor(private page: Page) {} public get sendButton() { - return this.page.getByTestId('send-button'); + return this.page.getByTestId("send-button"); } public get stopButton() { - return this.page.getByTestId('stop-button'); + return this.page.getByTestId("stop-button"); } public get multimodalInput() { - return this.page.getByTestId('multimodal-input'); + return this.page.getByTestId("multimodal-input"); } async createNewChat() { - await this.page.goto('/'); + await this.page.goto("/"); } public getCurrentURL(): string { @@ -34,7 +34,7 @@ export class ChatPage { async isGenerationComplete() { const response = await this.page.waitForResponse((response) => - response.url().includes('/api/chat'), + response.url().includes("/api/chat"), ); await response.finished(); @@ -42,7 +42,7 @@ export class ChatPage { async isVoteComplete() { const response = await this.page.waitForResponse((response) => - response.url().includes('/api/vote'), + response.url().includes("/api/vote"), ); await response.finished(); @@ -56,7 +56,7 @@ export class ChatPage { async sendUserMessageFromSuggestion() { await this.page - .getByRole('button', { name: 'What are the advantages of' }) + .getByRole("button", { name: "What are the advantages of" }) .click(); } @@ -69,27 +69,27 @@ export class ChatPage { } async addImageAttachment() { - this.page.on('filechooser', async (fileChooser) => { + this.page.on("filechooser", async (fileChooser) => { const filePath = path.join( process.cwd(), - 'public', - 'images', - 'mouth of the seine, monet.jpg', + "public", + "images", + "mouth of the seine, monet.jpg", ); const imageBuffer = fs.readFileSync(filePath); await fileChooser.setFiles({ - name: 'mouth of the seine, monet.jpg', - mimeType: 'image/jpeg', + name: "mouth of the seine, monet.jpg", + mimeType: "image/jpeg", buffer: imageBuffer, }); }); - await this.page.getByTestId('attachments-button').click(); + await this.page.getByTestId("attachments-button").click(); } public async getSelectedModel() { - const modelId = await this.page.getByTestId('model-selector').innerText(); + const modelId = await this.page.getByTestId("model-selector").innerText(); return modelId; } @@ -102,29 +102,29 @@ export class ChatPage { throw new Error(`Model with id ${chatModelId} not found`); } - await this.page.getByTestId('model-selector').click(); + await this.page.getByTestId("model-selector").click(); await this.page.getByTestId(`model-selector-item-${chatModelId}`).click(); expect(await this.getSelectedModel()).toBe(chatModel.name); } async getRecentAssistantMessage() { const messageElements = await this.page - .getByTestId('message-assistant') + .getByTestId("message-assistant") .all(); const lastMessageElement = messageElements[messageElements.length - 1]; const content = await lastMessageElement - .getByTestId('message-content') + .getByTestId("message-content") .innerText() .catch(() => null); const reasoningElement = await lastMessageElement - .getByTestId('message-reasoning') + .getByTestId("message-reasoning") .isVisible() .then(async (visible) => visible ? await lastMessageElement - .getByTestId('message-reasoning') + .getByTestId("message-reasoning") .innerText() : null, ) @@ -136,31 +136,31 @@ export class ChatPage { reasoning: reasoningElement, async toggleReasoningVisibility() { await lastMessageElement - .getByTestId('message-reasoning-toggle') + .getByTestId("message-reasoning-toggle") .click(); }, async upvote() { - await lastMessageElement.getByTestId('message-upvote').click(); + await lastMessageElement.getByTestId("message-upvote").click(); }, async downvote() { - await lastMessageElement.getByTestId('message-downvote').click(); + await lastMessageElement.getByTestId("message-downvote").click(); }, }; } async getRecentUserMessage() { - const messageElements = await this.page.getByTestId('message-user').all(); + const messageElements = await this.page.getByTestId("message-user").all(); const lastMessageElement = messageElements[messageElements.length - 1]; const content = await lastMessageElement.innerText(); const hasAttachments = await lastMessageElement - .getByTestId('message-attachments') + .getByTestId("message-attachments") .isVisible() .catch(() => false); const attachments = hasAttachments - ? await lastMessageElement.getByTestId('message-attachments').all() + ? await lastMessageElement.getByTestId("message-attachments").all() : []; const page = this.page; @@ -170,12 +170,9 @@ export class ChatPage { content, attachments, async edit(newMessage: string) { - await page.getByTestId('message-edit-button').click(); - await page.getByTestId('message-editor').fill(newMessage); - await page.getByTestId('message-editor-send-button').click(); - await expect( - page.getByTestId('message-editor-send-button'), - ).not.toBeVisible(); + await page.getByTestId("message-edit-button").click(); + await page.getByTestId("message-editor").fill(newMessage); + await page.getByTestId("message-editor-send-button").click(); }, }; } From 222c65b3bd72aa47c8c95e61516ce5388b02cfcd Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Wed, 19 Mar 2025 15:10:06 -0700 Subject: [PATCH 3/4] add tests --- app/(auth)/auth.config.ts | 20 +-- app/(chat)/api/chat/route.ts | 70 ++++---- app/(chat)/api/document/route.ts | 44 ++--- app/(chat)/api/files/upload/route.ts | 34 ++-- chat.config.ts | 6 +- components/chat-header.tsx | 7 +- components/chat.tsx | 5 +- components/message-actions.tsx | 239 +++++++++++++++------------ components/message.tsx | 20 ++- components/messages.tsx | 3 + components/visibility-selector.tsx | 1 + lib/artifacts/server.ts | 26 +-- lib/chat-config.ts | 30 +++- lib/constants.ts | 2 +- middleware.ts | 2 +- playwright.config.ts | 54 +++--- tests/auth.setup.ts | 28 ++-- tests/chat.guest.test.ts | 73 ++++---- tests/chat.test.ts | 7 + tests/pages/chat.ts | 66 ++++---- 20 files changed, 400 insertions(+), 337 deletions(-) diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index 6aa150e0c7..6e9f501704 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -1,10 +1,10 @@ -import { chatConfig } from "@/lib/chat-config"; -import type { NextAuthConfig } from "next-auth"; +import { chatConfig } from '@/lib/chat-config'; +import type { NextAuthConfig } from 'next-auth'; export const authConfig = { pages: { - signIn: "/login", - newUser: "/", + signIn: '/login', + newUser: '/', }, providers: [ // added later in auth.ts since it requires bcrypt which is only compatible with Node.js @@ -13,13 +13,13 @@ export const authConfig = { callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; - const isOnRegisterPage = nextUrl.pathname.startsWith("/register"); - const isOnLoginPage = nextUrl.pathname.startsWith("/login"); - const isOnChatPage = nextUrl.pathname.startsWith("/"); + const isOnRegisterPage = nextUrl.pathname.startsWith('/register'); + const isOnLoginPage = nextUrl.pathname.startsWith('/login'); + const isOnChatPage = nextUrl.pathname.startsWith('/'); // If logged in, redirect to home page if (isLoggedIn && (isOnLoginPage || isOnRegisterPage)) { - return Response.redirect(new URL("/", nextUrl as unknown as URL)); + return Response.redirect(new URL('/', nextUrl as unknown as URL)); } // Always allow access to register and login pages @@ -33,10 +33,6 @@ export const authConfig = { return false; } - if (isLoggedIn) { - return Response.redirect(new URL("/", nextUrl as unknown as URL)); - } - return true; }, }, diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 7b0eb9811c..84741373f5 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -4,28 +4,28 @@ import { createDataStreamResponse, smoothStream, streamText, -} from "ai"; -import { auth } from "@/app/(auth)/auth"; -import { systemPrompt } from "@/lib/ai/prompts"; +} from 'ai'; +import { auth } from '@/app/(auth)/auth'; +import { systemPrompt } from '@/lib/ai/prompts'; import { deleteChatById, getChatById, saveChat, saveMessages, -} from "@/lib/db/queries"; +} from '@/lib/db/queries'; import { generateUUID, getMostRecentUserMessage, getTrailingMessageId, -} from "@/lib/utils"; -import { generateTitleFromUserMessage } from "../../actions"; -import { createDocument } from "@/lib/ai/tools/create-document"; -import { updateDocument } from "@/lib/ai/tools/update-document"; -import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; -import { getWeather } from "@/lib/ai/tools/get-weather"; -import { isProductionEnvironment } from "@/lib/constants"; -import { myProvider } from "@/lib/ai/providers"; -import { chatConfig } from "@/lib/chat-config"; +} from '@/lib/utils'; +import { generateTitleFromUserMessage } from '../../actions'; +import { createDocument } from '@/lib/ai/tools/create-document'; +import { updateDocument } from '@/lib/ai/tools/update-document'; +import { requestSuggestions } from '@/lib/ai/tools/request-suggestions'; +import { getWeather } from '@/lib/ai/tools/get-weather'; +import { isProductionEnvironment } from '@/lib/constants'; +import { myProvider } from '@/lib/ai/providers'; +import { chatConfig } from '@/lib/chat-config'; export const maxDuration = 60; @@ -44,7 +44,7 @@ export async function POST(request: Request) { const session = await auth(); if (!chatConfig.guestUsage.isEnabled && !session?.user?.id) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } const userId = session?.user?.id; @@ -53,7 +53,7 @@ export async function POST(request: Request) { const userMessage = getMostRecentUserMessage(messages); if (!userMessage) { - return new Response("No user message found", { status: 400 }); + return new Response('No user message found', { status: 400 }); } const chat = await getChatById({ id }); @@ -66,7 +66,7 @@ export async function POST(request: Request) { if (isAuthenticated) await saveChat({ id, userId, title }); } else { if (chat.userId !== userId) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } } @@ -76,7 +76,7 @@ export async function POST(request: Request) { { chatId: id, id: userMessage.id, - role: "user", + role: 'user', parts: userMessage.parts, attachments: userMessage.experimental_attachments ?? [], createdAt: new Date(), @@ -92,15 +92,15 @@ export async function POST(request: Request) { messages, maxSteps: 5, experimental_activeTools: - selectedChatModel === "chat-model-reasoning" + selectedChatModel === 'chat-model-reasoning' ? [] : [ - "getWeather", - "createDocument", - "updateDocument", - "requestSuggestions", + 'getWeather', + 'createDocument', + 'updateDocument', + 'requestSuggestions', ], - experimental_transform: smoothStream({ chunking: "word" }), + experimental_transform: smoothStream({ chunking: 'word' }), experimental_generateMessageId: generateUUID, tools: { getWeather, @@ -116,12 +116,12 @@ export async function POST(request: Request) { try { const assistantId = getTrailingMessageId({ messages: response.messages.filter( - (message) => message.role === "assistant", + (message) => message.role === 'assistant', ), }); if (!assistantId) { - throw new Error("No assistant message found!"); + throw new Error('No assistant message found!'); } const [, assistantMessage] = appendResponseMessages({ @@ -143,13 +143,13 @@ export async function POST(request: Request) { ], }); } catch (error) { - console.error("Failed to save chat"); + console.error('Failed to save chat'); } } }, experimental_telemetry: { isEnabled: isProductionEnvironment, - functionId: "stream-text", + functionId: 'stream-text', }, }); @@ -160,11 +160,11 @@ export async function POST(request: Request) { }); }, onError: () => { - return "Oops, an error occured!"; + return 'Oops, an error occured!'; }, }); } catch (error) { - return new Response("An error occurred while processing your request!", { + return new Response('An error occurred while processing your request!', { status: 404, }); } @@ -172,30 +172,30 @@ export async function POST(request: Request) { export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); + const id = searchParams.get('id'); if (!id) { - return new Response("Not Found", { status: 404 }); + return new Response('Not Found', { status: 404 }); } const session = await auth(); if (!session || !session.user) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } try { const chat = await getChatById({ id }); if (chat.userId !== session.user.id) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } await deleteChatById({ id }); - return new Response("Chat deleted", { status: 200 }); + return new Response('Chat deleted', { status: 200 }); } catch (error) { - return new Response("An error occurred while processing your request!", { + return new Response('An error occurred while processing your request!', { status: 500, }); } diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index 7ee4f282b2..f470a9d9a9 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,18 +1,18 @@ -import { auth } from "@/app/(auth)/auth"; -import { chatConfig } from "@/lib/chat-config"; -import { ArtifactKind } from "@/components/artifact"; +import { auth } from '@/app/(auth)/auth'; +import { chatConfig } from '@/lib/chat-config'; +import { ArtifactKind } from '@/components/artifact'; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, saveDocument, -} from "@/lib/db/queries"; +} from '@/lib/db/queries'; export async function POST(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); + const id = searchParams.get('id'); if (!id) { - return new Response("Missing id", { status: 400 }); + return new Response('Missing id', { status: 400 }); } const { @@ -26,7 +26,7 @@ export async function POST(request: Request) { const guestUserId = process.env.GUEST_USER_ID; if (!guestUserId) { - throw new Error("Guest user ID is not set!"); + throw new Error('Guest user ID is not set!'); } const document = await saveDocument({ @@ -42,7 +42,7 @@ export async function POST(request: Request) { const session = await auth(); if (!session?.user?.id) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } const document = await saveDocument({ @@ -59,10 +59,10 @@ export async function POST(request: Request) { export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); + const id = searchParams.get('id'); if (!id) { - return new Response("Missing id", { status: 400 }); + return new Response('Missing id', { status: 400 }); } if (chatConfig.guestUsage.isEnabled) { @@ -70,11 +70,11 @@ export async function GET(request: Request) { const [document] = documents; if (!document) { - return new Response("Not Found", { status: 404 }); + return new Response('Not Found', { status: 404 }); } if (document.userId !== process.env.GUEST_USER_ID) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } return Response.json(documents, { status: 200 }); @@ -82,7 +82,7 @@ export async function GET(request: Request) { const session = await auth(); if (!session?.user?.id) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } const documents = await getDocumentsById({ id }); @@ -90,11 +90,11 @@ export async function GET(request: Request) { const [document] = documents; if (!document) { - return new Response("Not Found", { status: 404 }); + return new Response('Not Found', { status: 404 }); } if (document.userId !== session.user.id) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } return Response.json(documents, { status: 200 }); @@ -103,12 +103,12 @@ export async function GET(request: Request) { export async function PATCH(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); + const id = searchParams.get('id'); const { timestamp }: { timestamp: string } = await request.json(); if (!id) { - return new Response("Missing id", { status: 400 }); + return new Response('Missing id', { status: 400 }); } if (chatConfig.guestUsage.isEnabled) { @@ -116,7 +116,7 @@ export async function PATCH(request: Request) { const [document] = documents; if (document.userId !== process.env.GUEST_USER_ID) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } await deleteDocumentsByIdAfterTimestamp({ @@ -124,12 +124,12 @@ export async function PATCH(request: Request) { timestamp: new Date(timestamp), }); - return new Response("Deleted", { status: 200 }); + return new Response('Deleted', { status: 200 }); } else { const session = await auth(); if (!session?.user?.id) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } const documents = await getDocumentsById({ id }); @@ -137,7 +137,7 @@ export async function PATCH(request: Request) { const [document] = documents; if (document.userId !== session.user.id) { - return new Response("Unauthorized", { status: 401 }); + return new Response('Unauthorized', { status: 401 }); } await deleteDocumentsByIdAfterTimestamp({ @@ -145,6 +145,6 @@ export async function PATCH(request: Request) { timestamp: new Date(timestamp), }); - return new Response("Deleted", { status: 200 }); + return new Response('Deleted', { status: 200 }); } } diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts index f262bea3e5..25b8d39d47 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/(chat)/api/files/upload/route.ts @@ -1,20 +1,20 @@ -import { put } from "@vercel/blob"; -import { NextResponse } from "next/server"; -import { z } from "zod"; +import { put } from '@vercel/blob'; +import { NextResponse } from 'next/server'; +import { z } from 'zod'; -import { auth } from "@/app/(auth)/auth"; -import { chatConfig } from "@/lib/chat-config"; +import { auth } from '@/app/(auth)/auth'; +import { chatConfig } from '@/lib/chat-config'; // Use Blob instead of File since File is not available in Node.js environment const FileSchema = z.object({ file: z .instanceof(Blob) .refine((file) => file.size <= 5 * 1024 * 1024, { - message: "File size should be less than 5MB", + message: 'File size should be less than 5MB', }) // Update the file type based on the kind of files you want to accept - .refine((file) => ["image/jpeg", "image/png"].includes(file.type), { - message: "File type should be JPEG or PNG", + .refine((file) => ['image/jpeg', 'image/png'].includes(file.type), { + message: 'File type should be JPEG or PNG', }), }); @@ -22,19 +22,19 @@ export async function POST(request: Request) { const session = await auth(); if (!session && !chatConfig.guestUsage.isEnabled) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } if (request.body === null) { - return new Response("Request body is empty", { status: 400 }); + return new Response('Request body is empty', { status: 400 }); } try { const formData = await request.formData(); - const file = formData.get("file") as Blob; + const file = formData.get('file') as Blob; if (!file) { - return NextResponse.json({ error: "No file uploaded" }, { status: 400 }); + return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); } const validatedFile = FileSchema.safeParse({ file }); @@ -42,27 +42,27 @@ export async function POST(request: Request) { if (!validatedFile.success) { const errorMessage = validatedFile.error.errors .map((error) => error.message) - .join(", "); + .join(', '); return NextResponse.json({ error: errorMessage }, { status: 400 }); } // Get filename from formData since Blob doesn't have name property - const filename = (formData.get("file") as File).name; + const filename = (formData.get('file') as File).name; const fileBuffer = await file.arrayBuffer(); try { const data = await put(`${filename}`, fileBuffer, { - access: "public", + access: 'public', }); return NextResponse.json(data); } catch (error) { - return NextResponse.json({ error: "Upload failed" }, { status: 500 }); + return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); } } catch (error) { return NextResponse.json( - { error: "Failed to process request" }, + { error: 'Failed to process request' }, { status: 500 }, ); } diff --git a/chat.config.ts b/chat.config.ts index e4c8012ac1..2a41e58ec1 100644 --- a/chat.config.ts +++ b/chat.config.ts @@ -1,9 +1,9 @@ -import { ChatConfig } from "./lib/chat-config"; +import { ChatConfig } from './lib/chat-config'; const config: ChatConfig = { guestUsage: { - isEnabled: true, - userId: process.env.GUEST_USER_ID!, + isEnabled: false, + userId: null, }, }; diff --git a/components/chat-header.tsx b/components/chat-header.tsx index c84a51429c..b9ed42d0d7 100644 --- a/components/chat-header.tsx +++ b/components/chat-header.tsx @@ -32,6 +32,9 @@ function PureChatHeader({ const { width: windowWidth } = useWindowSize(); + const isVisibilitySelectorHidden = isReadonly || isGuest; + const isModelSelectorVisible = !isReadonly; + return (
@@ -55,14 +58,14 @@ function PureChatHeader({ )} - {!isReadonly && ( + {isModelSelectorVisible && ( )} - {!isReadonly && ( + {!isVisibilitySelectorHidden && ( >([]); const isArtifactVisible = useArtifactSelector((state) => state.isVisible); - const isReadonly = isGuest || selectedVisibilityType === 'public'; + const isReadonly = selectedVisibilityType === 'public'; return ( <> @@ -86,7 +86,8 @@ export function Chat({ messages={messages} setMessages={setMessages} reload={reload} - isReadonly={isReadonly || isGuest} + isReadonly={isReadonly} + isGuest={isGuest} isArtifactVisible={isArtifactVisible} /> diff --git a/components/message-actions.tsx b/components/message-actions.tsx index 1a92407dc6..f3982f0390 100644 --- a/components/message-actions.tsx +++ b/components/message-actions.tsx @@ -14,18 +14,23 @@ import { } from './ui/tooltip'; import { memo } from 'react'; import equal from 'fast-deep-equal'; -import { toast } from 'sonner'; +import { toast } from './toast'; +import { toast as sonnerToast } from 'sonner'; export function PureMessageActions({ chatId, message, vote, isLoading, + isReadonly, + isGuest, }: { chatId: string; message: Message; vote: Vote | undefined; isLoading: boolean; + isReadonly: boolean; + isGuest: boolean; }) { const { mutate } = useSWRConfig(); const [_, copyToClipboard] = useCopyToClipboard(); @@ -63,111 +68,133 @@ export function PureMessageActions({ Copy - - - - - Upvote Response - - - - - - - Downvote Response - + {!isReadonly && ( + + + + + Upvote Response + + )} + + {!isReadonly && ( + + + + + Downvote Response + + )} ); diff --git a/components/message.tsx b/components/message.tsx index 6b49e89185..5abbf24510 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -28,6 +28,7 @@ const PurePreviewMessage = ({ setMessages, reload, isReadonly, + isGuest, }: { chatId: string; message: UIMessage; @@ -36,6 +37,7 @@ const PurePreviewMessage = ({ setMessages: UseChatHelpers['setMessages']; reload: UseChatHelpers['reload']; isReadonly: boolean; + isGuest: boolean; }) => { const [mode, setMode] = useState<'view' | 'edit'>('view'); @@ -214,15 +216,15 @@ const PurePreviewMessage = ({ } })} - {!isReadonly && ( - - )} + diff --git a/components/messages.tsx b/components/messages.tsx index 045f3ae556..0ffe6ebd66 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -16,6 +16,7 @@ interface MessagesProps { reload: UseChatHelpers['reload']; isReadonly: boolean; isArtifactVisible: boolean; + isGuest: boolean; } function PureMessages({ @@ -26,6 +27,7 @@ function PureMessages({ setMessages, reload, isReadonly, + isGuest, }: MessagesProps) { const [messagesContainerRef, messagesEndRef] = useScrollToBottom(); @@ -51,6 +53,7 @@ function PureMessages({ setMessages={setMessages} reload={reload} isReadonly={isReadonly} + isGuest={isGuest} /> ))} diff --git a/components/visibility-selector.tsx b/components/visibility-selector.tsx index 7fd4059062..2440bfc71c 100644 --- a/components/visibility-selector.tsx +++ b/components/visibility-selector.tsx @@ -70,6 +70,7 @@ export function VisibilitySelector({ )} >