diff --git a/.env.example b/.env.example index 8433714..50c7f12 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,24 @@ -# generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` -AUTH_SECRET=**** +# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` +BETTER_AUTH_SECRET=**** +BETTER_AUTH_URL=**** -# required for non-vercel deployments, vercel uses OIDC automatically +# The following keys below are automatically created and +# added to your environment when you deploy on Vercel + +# Instructions to create an AI Gateway API key here: https://vercel.com/ai-gateway +# API key required for non-Vercel deployments +# For Vercel deployments, OIDC tokens are used automatically # https://vercel.com/ai-gateway AI_GATEWAY_API_KEY=**** -# https://vercel.com/docs/vercel-blob + +# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob BLOB_READ_WRITE_TOKEN=**** -# https://vercel.com/docs/postgres +# Instructions to create a PostgreSQL database here: https://vercel.com/docs/postgres POSTGRES_URL=**** + +# Instructions to create a Redis store here: # https://vercel.com/docs/redis REDIS_URL=**** diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8e87279..809a353 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 10.32.1 + version: 9.12.3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: diff --git a/.gitignore b/.gitignore index 344b787..05bfb89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies node_modules .pnp .pnp.js + +# testing coverage + +# next.js .next/ out/ build + +# misc .DS_Store *.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* + + +# local env files .env.local .env.development.local .env.test.local .env.production.local + +# turbo .turbo + .env .vercel .env*.local + +# Playwright /test-results/ /playwright-report/ /blob-report/ /playwright/* -next-env.d.ts -tsconfig.tsbuildinfo + +next-env.d.ts \ No newline at end of file diff --git a/README.md b/README.md index b407018..0c54ca8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + Chatbot

Chatbot

@@ -8,7 +8,7 @@

- Read Docs · + Read Docs · Features · Model Providers · Deploy Your Own · @@ -36,7 +36,7 @@ ## Model Providers -This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. Models are configured in `lib/ai/models.ts` with per-model provider routing. Included models: Mistral, Moonshot, DeepSeek, OpenAI, and xAI. +This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. The default model is [OpenAI](https://openai.com) GPT-4.1 Mini, with support for Anthropic, Google, and xAI models. ### AI Gateway Authentication diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts index 024ff51..fb817ad 100644 --- a/app/(auth)/actions.ts +++ b/app/(auth)/actions.ts @@ -1,10 +1,7 @@ "use server"; import { z } from "zod"; - -import { createUser, getUser } from "@/lib/db/queries"; - -import { signIn } from "./auth"; +import { auth } from "@/lib/auth"; const authFormSchema = z.object({ email: z.string().email(), @@ -25,10 +22,11 @@ export const login = async ( password: formData.get("password"), }); - await signIn("credentials", { - email: validatedData.email, - password: validatedData.password, - redirect: false, + await auth.api.signInEmail({ + body: { + email: validatedData.email, + password: validatedData.password, + }, }); return { status: "success" }; @@ -61,24 +59,29 @@ export const register = async ( password: formData.get("password"), }); - const [user] = await getUser(validatedData.email); - - if (user) { - return { status: "user_exists" } as RegisterActionState; - } - await createUser(validatedData.email, validatedData.password); - await signIn("credentials", { - email: validatedData.email, - password: validatedData.password, - redirect: false, + const result = await auth.api.signUpEmail({ + body: { + email: validatedData.email, + password: validatedData.password, + name: validatedData.email, + }, }); + if (!result) { + return { status: "failed" }; + } + return { status: "success" }; } catch (error) { if (error instanceof z.ZodError) { return { status: "invalid_data" }; } + const message = error instanceof Error ? error.message : ""; + if (message.includes("already exists") || message.includes("UNIQUE")) { + return { status: "user_exists" }; + } + return { status: "failed" }; } }; diff --git a/app/(auth)/api/auth/[...nextauth]/route.ts b/app/(auth)/api/auth/[...nextauth]/route.ts deleted file mode 100644 index d104b65..0000000 --- a/app/(auth)/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET, POST } from "@/app/(auth)/auth"; diff --git a/app/(auth)/api/auth/guest/route.ts b/app/(auth)/api/auth/guest/route.ts deleted file mode 100644 index 97ce3d2..0000000 --- a/app/(auth)/api/auth/guest/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextResponse } from "next/server"; -import { getToken } from "next-auth/jwt"; -import { signIn } from "@/app/(auth)/auth"; -import { isDevelopmentEnvironment } from "@/lib/constants"; - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const rawRedirect = searchParams.get("redirectUrl") || "/"; - const redirectUrl = - rawRedirect.startsWith("/") && !rawRedirect.startsWith("//") - ? rawRedirect - : "/"; - - const token = await getToken({ - req: request, - secret: process.env.AUTH_SECRET, - secureCookie: !isDevelopmentEnvironment, - }); - - if (token) { - const base = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - return NextResponse.redirect(new URL(`${base}/`, request.url)); - } - - return signIn("guest", { redirect: true, redirectTo: redirectUrl }); -} diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts deleted file mode 100644 index 5056101..0000000 --- a/app/(auth)/auth.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextAuthConfig } from "next-auth"; - -const base = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; - -export const authConfig = { - basePath: "/api/auth", - trustHost: true, - pages: { - signIn: `${base}/login`, - newUser: `${base}/`, - }, - providers: [], - callbacks: {}, -} satisfies NextAuthConfig; diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts deleted file mode 100644 index b75369f..0000000 --- a/app/(auth)/auth.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { compare } from "bcrypt-ts"; -import NextAuth, { type DefaultSession } from "next-auth"; -import type { DefaultJWT } from "next-auth/jwt"; -import Credentials from "next-auth/providers/credentials"; -import { DUMMY_PASSWORD } from "@/lib/constants"; -import { createGuestUser, getUser } from "@/lib/db/queries"; -import { authConfig } from "./auth.config"; - -export type UserType = "guest" | "regular"; - -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - type: UserType; - } & DefaultSession["user"]; - } - - interface User { - id?: string; - email?: string | null; - type: UserType; - } -} - -declare module "next-auth/jwt" { - interface JWT extends DefaultJWT { - id: string; - type: UserType; - } -} - -export const { - handlers: { GET, POST }, - auth, - signIn, - signOut, -} = NextAuth({ - ...authConfig, - providers: [ - Credentials({ - credentials: { - email: { label: "Email", type: "email" }, - password: { label: "Password", type: "password" }, - }, - async authorize(credentials) { - const email = String(credentials.email ?? ""); - const password = String(credentials.password ?? ""); - const users = await getUser(email); - - if (users.length === 0) { - await compare(password, DUMMY_PASSWORD); - return null; - } - - const [user] = users; - - if (!user.password) { - await compare(password, DUMMY_PASSWORD); - return null; - } - - const passwordsMatch = await compare(password, user.password); - - if (!passwordsMatch) { - return null; - } - - return { ...user, type: "regular" }; - }, - }), - Credentials({ - id: "guest", - credentials: {}, - async authorize() { - const [guestUser] = await createGuestUser(); - return { ...guestUser, type: "guest" }; - }, - }), - ], - callbacks: { - jwt({ token, user }) { - if (user) { - token.id = user.id as string; - token.type = user.type; - } - - return token; - }, - session({ session, token }) { - if (session.user) { - session.user.id = token.id; - session.user.type = token.type; - } - - return session; - }, - }, -}); diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx deleted file mode 100644 index d14e92c..0000000 --- a/app/(auth)/layout.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { ArrowLeftIcon } from "lucide-react"; -import Link from "next/link"; -import { SparklesIcon, VercelIcon } from "@/components/chat/icons"; -import { Preview } from "@/components/chat/preview"; - -export default function AuthLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( -

-
- - - Back - -
-
-
- -
- {children} -
-
-
- -
-
- Powered by - - AI Gateway -
-
- -
-
-
- ); -} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index ea4c602..dcd3f04 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -2,30 +2,36 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; import { useActionState, useEffect, useState } from "react"; -import { AuthForm } from "@/components/chat/auth-form"; -import { SubmitButton } from "@/components/chat/submit-button"; -import { toast } from "@/components/chat/toast"; +import { AuthForm } from "@/components/auth-form"; +import { SubmitButton } from "@/components/submit-button"; +import { toast } from "@/components/toast"; +import { useSession } from "@/lib/client"; import { type LoginActionState, login } from "../actions"; export default function Page() { const router = useRouter(); + const [email, setEmail] = useState(""); const [isSuccessful, setIsSuccessful] = useState(false); const [state, formAction] = useActionState( login, - { status: "idle" } + { + status: "idle", + } ); - const { update: updateSession } = useSession(); + const { refetch } = useSession(); - // biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession are stable refs + // biome-ignore lint/correctness/useExhaustiveDependencies: router and refetch are stable refs useEffect(() => { if (state.status === "failed") { - toast({ type: "error", description: "Invalid credentials!" }); + toast({ + type: "error", + description: "Invalid credentials!", + }); } else if (state.status === "invalid_data") { toast({ type: "error", @@ -33,8 +39,8 @@ export default function Page() { }); } else if (state.status === "success") { setIsSuccessful(true); - updateSession(); - router.refresh(); + refetch(); + router.push("/"); } }, [state.status]); @@ -44,23 +50,28 @@ export default function Page() { }; return ( - <> -

Welcome back

-

- Sign in to your account to continue -

- - Sign in -

- {"No account? "} - - Sign up - -

-
- +
+
+
+

Sign In

+

+ Use your email and password to sign in +

+
+ + Sign in +

+ {"Don't have an account? "} + + Sign up + + {" for free."} +

+
+
+
); } diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index f2abbc8..4f4444a 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -2,26 +2,29 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; import { useActionState, useEffect, useState } from "react"; -import { AuthForm } from "@/components/chat/auth-form"; -import { SubmitButton } from "@/components/chat/submit-button"; -import { toast } from "@/components/chat/toast"; +import { AuthForm } from "@/components/auth-form"; +import { SubmitButton } from "@/components/submit-button"; +import { toast } from "@/components/toast"; +import { useSession } from "@/lib/client"; import { type RegisterActionState, register } from "../actions"; export default function Page() { const router = useRouter(); + const [email, setEmail] = useState(""); const [isSuccessful, setIsSuccessful] = useState(false); const [state, formAction] = useActionState( register, - { status: "idle" } + { + status: "idle", + } ); - const { update: updateSession } = useSession(); + const { refetch } = useSession(); - // biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession are stable refs + // biome-ignore lint/correctness/useExhaustiveDependencies: router and refetch are stable refs useEffect(() => { if (state.status === "user_exists") { toast({ type: "error", description: "Account already exists!" }); @@ -33,10 +36,11 @@ export default function Page() { description: "Failed validating your submission!", }); } else if (state.status === "success") { - toast({ type: "success", description: "Account created!" }); + toast({ type: "success", description: "Account created successfully!" }); + setIsSuccessful(true); - updateSession(); - router.refresh(); + refetch(); + router.push("/"); } }, [state.status]); @@ -46,21 +50,28 @@ export default function Page() { }; return ( - <> -

Create account

-

Get started for free

- - Sign up -

- {"Have an account? "} - - Sign in - -

-
- +
+
+
+

Sign Up

+

+ Create an account with your email and password +

+
+ + Sign Up +

+ {"Already have an account? "} + + Sign in + + {" instead."} +

+
+
+
); } diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts index 2955a53..7846b80 100644 --- a/app/(chat)/actions.ts +++ b/app/(chat)/actions.ts @@ -2,14 +2,11 @@ import { generateText, type UIMessage } from "ai"; import { cookies } from "next/headers"; -import { auth } from "@/app/(auth)/auth"; -import type { VisibilityType } from "@/components/chat/visibility-selector"; -import { titleModel } from "@/lib/ai/models"; +import type { VisibilityType } from "@/components/visibility-selector"; import { titlePrompt } from "@/lib/ai/prompts"; import { getTitleModel } from "@/lib/ai/providers"; import { deleteMessagesByChatIdAfterTimestamp, - getChatById, getMessageById, updateChatVisibilityById, } from "@/lib/db/queries"; @@ -29,9 +26,6 @@ export async function generateTitleFromUserMessage({ model: getTitleModel(), system: titlePrompt, prompt: getTextFromMessage(message), - providerOptions: { - gateway: { order: titleModel.gatewayOrder }, - }, }); return text .replace(/^[#*"\s]+/, "") @@ -40,20 +34,7 @@ export async function generateTitleFromUserMessage({ } export async function deleteTrailingMessages({ id }: { id: string }) { - const session = await auth(); - if (!session?.user?.id) { - throw new Error("Unauthorized"); - } - const [message] = await getMessageById({ id }); - if (!message) { - throw new Error("Message not found"); - } - - const chat = await getChatById({ id: message.chatId }); - if (!chat || chat.userId !== session.user.id) { - throw new Error("Unauthorized"); - } await deleteMessagesByChatIdAfterTimestamp({ chatId: message.chatId, @@ -68,15 +49,5 @@ export async function updateChatVisibility({ chatId: string; visibility: VisibilityType; }) { - const session = await auth(); - if (!session?.user?.id) { - throw new Error("Unauthorized"); - } - - const chat = await getChatById({ id: chatId }); - if (!chat || chat.userId !== session.user.id) { - throw new Error("Unauthorized"); - } - await updateChatVisibilityById({ chatId, visibility }); } diff --git a/app/(chat)/api/auth/[...all]/route.ts b/app/(chat)/api/auth/[...all]/route.ts new file mode 100644 index 0000000..83ab371 --- /dev/null +++ b/app/(chat)/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { toNextJsHandler } from "better-auth/next-js"; +import { auth } from "@/lib/auth"; + +export const { GET, POST } = toNextJsHandler(auth); diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index eb70454..c91a8c3 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -10,21 +10,15 @@ import { import { checkBotId } from "botid/server"; import { after } from "next/server"; import { createResumableStreamContext } from "resumable-stream"; -import { auth, type UserType } from "@/app/(auth)/auth"; import { entitlementsByUserType } from "@/lib/ai/entitlements"; -import { - allowedModelIds, - chatModels, - DEFAULT_CHAT_MODEL, - getCapabilities, -} from "@/lib/ai/models"; +import { allowedModelIds } from "@/lib/ai/models"; import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; import { getLanguageModel } from "@/lib/ai/providers"; import { createDocument } from "@/lib/ai/tools/create-document"; -import { editDocument } from "@/lib/ai/tools/edit-document"; import { getWeather } from "@/lib/ai/tools/get-weather"; import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; import { updateDocument } from "@/lib/ai/tools/update-document"; +import { getSession, getUserType, type UserType } from "@/lib/auth"; import { isProductionEnvironment } from "@/lib/constants"; import { createStreamId, @@ -73,20 +67,20 @@ export async function POST(request: Request) { const [, session] = await Promise.all([ checkBotId().catch(() => null), - auth(), + getSession(), ]); if (!session?.user) { return new ChatbotError("unauthorized:chat").toResponse(); } - const chatModel = allowedModelIds.has(selectedChatModel) - ? selectedChatModel - : DEFAULT_CHAT_MODEL; + if (!allowedModelIds.has(selectedChatModel)) { + return new ChatbotError("bad_request:api").toResponse(); + } await checkIpRateLimit(ipAddress(request)); - const userType: UserType = session.user.type; + const userType: UserType = getUserType(session.user); const messageCount = await getMessageCountByUserId({ id: session.user.id, @@ -107,7 +101,9 @@ export async function POST(request: Request) { if (chat.userId !== session.user.id) { return new ChatbotError("forbidden:chat").toResponse(); } - messagesFromDb = await getMessagesByChatId({ id }); + if (!isToolApprovalFlow) { + messagesFromDb = await getMessagesByChatId({ id }); + } } else if (message?.role === "user") { await saveChat({ id, @@ -118,43 +114,9 @@ export async function POST(request: Request) { titlePromise = generateTitleFromUserMessage({ message }); } - let uiMessages: ChatMessage[]; - - if (isToolApprovalFlow && messages) { - const dbMessages = convertToUIMessages(messagesFromDb); - const approvalStates = new Map( - messages.flatMap( - (m) => - m.parts - ?.filter( - (p: Record) => - p.state === "approval-responded" || - p.state === "output-denied" - ) - .map((p: Record) => [ - String(p.toolCallId ?? ""), - p, - ]) ?? [] - ) - ); - uiMessages = dbMessages.map((msg) => ({ - ...msg, - parts: msg.parts.map((part) => { - if ( - "toolCallId" in part && - approvalStates.has(String(part.toolCallId)) - ) { - return { ...part, ...approvalStates.get(String(part.toolCallId)) }; - } - return part; - }), - })) as ChatMessage[]; - } else { - uiMessages = [ - ...convertToUIMessages(messagesFromDb), - message as ChatMessage, - ]; - } + const uiMessages = isToolApprovalFlow + ? (messages as ChatMessage[]) + : [...convertToUIMessages(messagesFromDb), message as ChatMessage]; const { longitude, latitude, city, country } = geolocation(request); @@ -180,11 +142,10 @@ export async function POST(request: Request) { }); } - const modelConfig = chatModels.find((m) => m.id === chatModel); - const modelCapabilities = await getCapabilities(); - const capabilities = modelCapabilities[chatModel]; - const isReasoningModel = capabilities?.reasoning === true; - const supportsTools = capabilities?.tools === true; + const isReasoningModel = + selectedChatModel.endsWith("-thinking") || + (selectedChatModel.includes("reasoning") && + !selectedChatModel.includes("non-reasoning")); const modelMessages = await convertToModelMessages(uiMessages); @@ -192,46 +153,30 @@ export async function POST(request: Request) { originalMessages: isToolApprovalFlow ? uiMessages : undefined, execute: async ({ writer: dataStream }) => { const result = streamText({ - model: getLanguageModel(chatModel), - system: systemPrompt({ requestHints, supportsTools }), + model: getLanguageModel(selectedChatModel), + system: systemPrompt({ selectedChatModel, requestHints }), messages: modelMessages, stopWhen: stepCountIs(5), - experimental_activeTools: - isReasoningModel && !supportsTools - ? [] - : [ - "getWeather", - "createDocument", - "editDocument", - "updateDocument", - "requestSuggestions", - ], - providerOptions: { - ...(modelConfig?.gatewayOrder && { - gateway: { order: modelConfig.gatewayOrder }, - }), - ...(modelConfig?.reasoningEffort && { - openai: { reasoningEffort: modelConfig.reasoningEffort }, - }), - }, + experimental_activeTools: isReasoningModel + ? [] + : [ + "getWeather", + "createDocument", + "updateDocument", + "requestSuggestions", + ], + providerOptions: isReasoningModel + ? { + anthropic: { + thinking: { type: "enabled", budgetTokens: 10_000 }, + }, + } + : undefined, tools: { getWeather, - createDocument: createDocument({ - session, - dataStream, - modelId: chatModel, - }), - editDocument: editDocument({ dataStream, session }), - updateDocument: updateDocument({ - session, - dataStream, - modelId: chatModel, - }), - requestSuggestions: requestSuggestions({ - session, - dataStream, - modelId: chatModel, - }), + createDocument: createDocument({ session, dataStream }), + updateDocument: updateDocument({ session, dataStream }), + requestSuggestions: requestSuggestions({ session, dataStream }), }, experimental_telemetry: { isEnabled: isProductionEnvironment, @@ -244,13 +189,9 @@ export async function POST(request: Request) { ); if (titlePromise) { - try { - const title = await titlePromise; - dataStream.write({ type: "data-chat-title", data: title }); - updateChatTitleById({ chatId: id, title }); - } catch (_) { - /* non-fatal */ - } + const title = await titlePromise; + dataStream.write({ type: "data-chat-title", data: title }); + updateChatTitleById({ chatId: id, title }); } }, generateId: generateUUID, @@ -321,7 +262,7 @@ export async function POST(request: Request) { ); } } catch (_) { - /* non-critical */ + // ignore redis errors } }, }); @@ -354,7 +295,7 @@ export async function DELETE(request: Request) { return new ChatbotError("bad_request:api").toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:chat").toResponse(); diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index 35f785b..60a708a 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -20,16 +20,18 @@ const userMessageSchema = z.object({ parts: z.array(partSchema), }); -const toolApprovalMessageSchema = z.object({ +// For tool approval flows, we accept all messages (more permissive schema) +const messageSchema = z.object({ id: z.string(), - role: z.enum(["user", "assistant"]), - parts: z.array(z.record(z.unknown())), + role: z.string(), + parts: z.array(z.any()), }); export const postRequestBodySchema = z.object({ id: z.string().uuid(), + // Either a single new message or all messages (for tool approvals) message: userMessageSchema.optional(), - messages: z.array(toolApprovalMessageSchema).optional(), + messages: z.array(messageSchema).optional(), selectedChatModel: z.string(), selectedVisibilityType: z.enum(["public", "private"]), }); diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index ee66e30..36a550f 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,21 +1,12 @@ -import { z } from "zod"; -import { auth } from "@/app/(auth)/auth"; -import type { ArtifactKind } from "@/components/chat/artifact"; +import type { ArtifactKind } from "@/components/artifact"; +import { getSession } from "@/lib/auth"; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, saveDocument, - updateDocumentContent, } from "@/lib/db/queries"; import { ChatbotError } from "@/lib/errors"; -const documentSchema = z.object({ - content: z.string(), - title: z.string(), - kind: z.enum(["text", "code", "image", "sheet"]), - isManualEdit: z.boolean().optional(), -}); - export async function GET(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get("id"); @@ -27,7 +18,7 @@ export async function GET(request: Request) { ).toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:document").toResponse(); @@ -59,29 +50,18 @@ export async function POST(request: Request) { ).toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("not_found:document").toResponse(); } - let content: string; - let title: string; - let kind: ArtifactKind; - let isManualEdit: boolean | undefined; - - try { - const parsed = documentSchema.parse(await request.json()); - content = parsed.content; - title = parsed.title; - kind = parsed.kind; - isManualEdit = parsed.isManualEdit; - } catch { - return new ChatbotError( - "bad_request:api", - "Invalid request body." - ).toResponse(); - } + const { + content, + title, + kind, + }: { content: string; title: string; kind: ArtifactKind } = + await request.json(); const documents = await getDocumentsById({ id }); @@ -93,11 +73,6 @@ export async function POST(request: Request) { } } - if (isManualEdit && documents.length > 0) { - const result = await updateDocumentContent({ id, content }); - return Response.json(result, { status: 200 }); - } - const document = await saveDocument({ id, content, @@ -128,7 +103,7 @@ export async function DELETE(request: Request) { ).toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:document").toResponse(); @@ -142,18 +117,9 @@ export async function DELETE(request: Request) { return new ChatbotError("forbidden:document").toResponse(); } - const parsedTimestamp = new Date(timestamp); - - if (Number.isNaN(parsedTimestamp.getTime())) { - return new ChatbotError( - "bad_request:api", - "Invalid timestamp." - ).toResponse(); - } - const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({ id, - timestamp: parsedTimestamp, + timestamp: new Date(timestamp), }); return Response.json(documentsDeleted, { status: 200 }); diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts index b227033..b81566d 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/(chat)/api/files/upload/route.ts @@ -2,21 +2,23 @@ import { put } from "@vercel/blob"; import { NextResponse } from "next/server"; import { z } from "zod"; -import { auth } from "@/app/(auth)/auth"; +import { getSession } from "@/lib/auth"; +// 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", }) + // 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", }), }); export async function POST(request: Request) { - const session = await auth(); + const session = await getSession(); if (!session) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -44,12 +46,12 @@ export async function POST(request: Request) { 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 safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_"); const fileBuffer = await file.arrayBuffer(); try { - const data = await put(`${safeName}`, fileBuffer, { + const data = await put(`${filename}`, fileBuffer, { access: "public", }); diff --git a/app/(chat)/api/history/route.ts b/app/(chat)/api/history/route.ts index 064a385..4e80080 100644 --- a/app/(chat)/api/history/route.ts +++ b/app/(chat)/api/history/route.ts @@ -1,15 +1,12 @@ import type { NextRequest } from "next/server"; -import { auth } from "@/app/(auth)/auth"; +import { getSession } from "@/lib/auth"; import { deleteAllChatsByUserId, getChatsByUserId } from "@/lib/db/queries"; import { ChatbotError } from "@/lib/errors"; export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; - const limit = Math.min( - Math.max(Number.parseInt(searchParams.get("limit") || "10", 10), 1), - 50 - ); + const limit = Number.parseInt(searchParams.get("limit") || "10", 10); const startingAfter = searchParams.get("starting_after"); const endingBefore = searchParams.get("ending_before"); @@ -20,7 +17,7 @@ export async function GET(request: NextRequest) { ).toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:chat").toResponse(); @@ -37,7 +34,7 @@ export async function GET(request: NextRequest) { } export async function DELETE() { - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:chat").toResponse(); diff --git a/app/(chat)/api/messages/route.ts b/app/(chat)/api/messages/route.ts deleted file mode 100644 index cda98de..0000000 --- a/app/(chat)/api/messages/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { auth } from "@/app/(auth)/auth"; -import { getChatById, getMessagesByChatId } from "@/lib/db/queries"; -import { convertToUIMessages } from "@/lib/utils"; - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const chatId = searchParams.get("chatId"); - - if (!chatId) { - return Response.json({ error: "chatId required" }, { status: 400 }); - } - - const [session, chat, messages] = await Promise.all([ - auth(), - getChatById({ id: chatId }), - getMessagesByChatId({ id: chatId }), - ]); - - if (!chat) { - return Response.json({ - messages: [], - visibility: "private", - userId: null, - isReadonly: false, - }); - } - - if ( - chat.visibility === "private" && - (!session?.user || session.user.id !== chat.userId) - ) { - return Response.json({ error: "forbidden" }, { status: 403 }); - } - - const isReadonly = !session?.user || session.user.id !== chat.userId; - - return Response.json({ - messages: convertToUIMessages(messages), - visibility: chat.visibility, - userId: chat.userId, - isReadonly, - }); -} diff --git a/app/(chat)/api/models/route.ts b/app/(chat)/api/models/route.ts deleted file mode 100644 index de1d12a..0000000 --- a/app/(chat)/api/models/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getAllGatewayModels, getCapabilities, isDemo } from "@/lib/ai/models"; - -export async function GET() { - const headers = { - "Cache-Control": "public, max-age=86400, s-maxage=86400", - }; - - const curatedCapabilities = await getCapabilities(); - - if (isDemo) { - const models = await getAllGatewayModels(); - const capabilities = Object.fromEntries( - models.map((m) => [m.id, curatedCapabilities[m.id] ?? m.capabilities]) - ); - - return Response.json({ capabilities, models }, { headers }); - } - - return Response.json(curatedCapabilities, { headers }); -} diff --git a/app/(chat)/api/suggestions/route.ts b/app/(chat)/api/suggestions/route.ts index 303f45e..babfb9d 100644 --- a/app/(chat)/api/suggestions/route.ts +++ b/app/(chat)/api/suggestions/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@/app/(auth)/auth"; +import { getSession } from "@/lib/auth"; import { getSuggestionsByDocumentId } from "@/lib/db/queries"; import { ChatbotError } from "@/lib/errors"; @@ -13,7 +13,7 @@ export async function GET(request: Request) { ).toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:suggestions").toResponse(); diff --git a/app/(chat)/api/vote/route.ts b/app/(chat)/api/vote/route.ts index 726ba56..4f69e2c 100644 --- a/app/(chat)/api/vote/route.ts +++ b/app/(chat)/api/vote/route.ts @@ -1,14 +1,7 @@ -import { z } from "zod"; -import { auth } from "@/app/(auth)/auth"; +import { getSession } from "@/lib/auth"; import { getChatById, getVotesByChatId, voteMessage } from "@/lib/db/queries"; import { ChatbotError } from "@/lib/errors"; -const voteSchema = z.object({ - chatId: z.string(), - messageId: z.string(), - type: z.enum(["up", "down"]), -}); - export async function GET(request: Request) { const { searchParams } = new URL(request.url); const chatId = searchParams.get("chatId"); @@ -20,7 +13,7 @@ export async function GET(request: Request) { ).toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:vote").toResponse(); @@ -42,23 +35,21 @@ export async function GET(request: Request) { } export async function PATCH(request: Request) { - let chatId: string; - let messageId: string; - let type: "up" | "down"; + const { + chatId, + messageId, + type, + }: { chatId: string; messageId: string; type: "up" | "down" } = + await request.json(); - try { - const parsed = voteSchema.parse(await request.json()); - chatId = parsed.chatId; - messageId = parsed.messageId; - type = parsed.type; - } catch { + if (!chatId || !messageId || !type) { return new ChatbotError( "bad_request:api", "Parameters chatId, messageId, and type are required." ).toResponse(); } - const session = await auth(); + const session = await getSession(); if (!session?.user) { return new ChatbotError("unauthorized:vote").toResponse(); diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index 67e0859..f8c10fa 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -1,3 +1,81 @@ -export default function Page() { - return null; +import { cookies } from "next/headers"; +import { notFound, redirect } from "next/navigation"; +import { Suspense } from "react"; +import { Chat } from "@/components/chat"; +import { DataStreamHandler } from "@/components/data-stream-handler"; +import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; +import { getSession } from "@/lib/auth"; +import { getChatById, getMessagesByChatId } from "@/lib/db/queries"; +import { convertToUIMessages } from "@/lib/utils"; + +export default function Page(props: { params: Promise<{ id: string }> }) { + return ( + }> + + + ); +} + +async function ChatPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const chat = await getChatById({ id }); + + if (!chat) { + redirect("/"); + } + + const session = await getSession(); + + if (!session) { + redirect("/login"); + } + + if (chat.visibility === "private") { + if (!session.user) { + return notFound(); + } + + if (session.user.id !== chat.userId) { + return notFound(); + } + } + + const messagesFromDb = await getMessagesByChatId({ + id, + }); + + const uiMessages = convertToUIMessages(messagesFromDb); + + const cookieStore = await cookies(); + const chatModelFromCookie = cookieStore.get("chat-model"); + + if (!chatModelFromCookie) { + return ( + <> + + + + ); + } + + return ( + <> + + + + ); } diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index 8ea30ec..aca0f0f 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -1,53 +1,35 @@ import { cookies } from "next/headers"; import Script from "next/script"; import { Suspense } from "react"; -import { Toaster } from "sonner"; -import { AppSidebar } from "@/components/chat/app-sidebar"; -import { DataStreamProvider } from "@/components/chat/data-stream-provider"; -import { ChatShell } from "@/components/chat/shell"; +import { AppSidebar } from "@/components/app-sidebar"; +import { DataStreamProvider } from "@/components/data-stream-provider"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; -import { ActiveChatProvider } from "@/hooks/use-active-chat"; -import { auth } from "../(auth)/auth"; +import { getSession } from "@/lib/auth"; export default function Layout({ children }: { children: React.ReactNode }) { return ( <>