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
@@ -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 (
-
-
-
-
-
- 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 (
<>
- }>
- {children}
+ }>
+ {children}
>
);
}
-async function SidebarShell({ children }: { children: React.ReactNode }) {
- const [session, cookieStore] = await Promise.all([auth(), cookies()]);
+async function SidebarWrapper({ children }: { children: React.ReactNode }) {
+ const [session, cookieStore] = await Promise.all([getSession(), cookies()]);
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";
return (
-
-
- }>
-
-
-
-
- {children}
-
+ {children}
);
}
diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx
index 67e0859..332ed4b 100644
--- a/app/(chat)/page.tsx
+++ b/app/(chat)/page.tsx
@@ -1,3 +1,52 @@
+import { cookies } from "next/headers";
+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 { generateUUID } from "@/lib/utils";
+
export default function Page() {
- return null;
+ return (
+ }>
+
+
+ );
+}
+
+async function NewChatPage() {
+ const cookieStore = await cookies();
+ const modelIdFromCookie = cookieStore.get("chat-model");
+ const id = generateUUID();
+
+ if (!modelIdFromCookie) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+ >
+ );
}
diff --git a/app/globals.css b/app/globals.css
index 4143774..de6fc04 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,42 +1,138 @@
@import "tailwindcss";
@import "katex/dist/katex.min.css";
+/* include utility classes in streamdown */
@source "../node_modules/streamdown/dist/index.js";
+/* custom variant for setting dark mode programmatically */
@custom-variant dark (&:is(.dark, .dark *));
+/* include plugins */
@plugin "tailwindcss-animate";
@plugin "@tailwindcss/typography";
-@theme inline {
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
+/* define design tokens (light mode) */
+:root {
+ --background: hsl(0 0% 100%);
+ --foreground: hsl(240 10% 3.9%);
+ --card: hsl(0 0% 100%);
+ --card-foreground: hsl(240 10% 3.9%);
+ --popover: hsl(0 0% 100%);
+ --popover-foreground: hsl(240 10% 3.9%);
+ --primary: hsl(240 5.9% 10%);
+ --primary-foreground: hsl(0 0% 98%);
+ --secondary: hsl(240 4.8% 95.9%);
+ --secondary-foreground: hsl(240 5.9% 10%);
+ --muted: hsl(240 4.8% 95.9%);
+ --muted-foreground: hsl(240 3.8% 46.1%);
+ --accent: hsl(240 4.8% 95.9%);
+ --accent-foreground: hsl(240 5.9% 10%);
+ --destructive: hsl(0 84.2% 60.2%);
+ --destructive-foreground: hsl(0 0% 98%);
+ --border: hsl(240 5.9% 90%);
+ --input: hsl(240 5.9% 90%);
+ --ring: hsl(240 10% 3.9%);
+ --chart-1: hsl(12 76% 61%);
+ --chart-2: hsl(173 58% 39%);
+ --chart-3: hsl(197 37% 24%);
+ --chart-4: hsl(43 74% 66%);
+ --chart-5: hsl(27 87% 67%);
+ --sidebar-background: hsl(0 0% 98%);
+ --sidebar-foreground: hsl(240 5.3% 26.1%);
+ --sidebar-primary: hsl(240 5.9% 10%);
+ --sidebar-primary-foreground: hsl(0 0% 98%);
+ --sidebar-accent: hsl(240 4.8% 95.9%);
+ --sidebar-accent-foreground: hsl(240 5.9% 10%);
+ --sidebar-border: hsl(220 13% 91%);
+ --sidebar-ring: hsl(217.2 91.2% 59.8%);
+ /* border radius unit */
+ --radius: 0.5rem;
+ --sidebar: hsl(0 0% 98%);
+}
+
+/* define design tokens (dark mode) */
+.dark {
+ --background: hsl(240 10% 3.9%);
+ --foreground: hsl(0 0% 98%);
+ --card: hsl(240 10% 3.9%);
+ --card-foreground: hsl(0 0% 98%);
+ --popover: hsl(240 10% 3.9%);
+ --popover-foreground: hsl(0 0% 98%);
+ --primary: hsl(0 0% 98%);
+ --primary-foreground: hsl(240 5.9% 10%);
+ --secondary: hsl(240 3.7% 15.9%);
+ --secondary-foreground: hsl(0 0% 98%);
+ --muted: hsl(240 3.7% 15.9%);
+ --muted-foreground: hsl(240 5% 64.9%);
+ --accent: hsl(240 3.7% 15.9%);
+ --accent-foreground: hsl(0 0% 98%);
+ --destructive: hsl(0 62.8% 30.6%);
+ --destructive-foreground: hsl(0 0% 98%);
+ --border: hsl(240 3.7% 15.9%);
+ --input: hsl(240 3.7% 15.9%);
+ --ring: hsl(240 4.9% 83.9%);
+ --chart-1: hsl(220 70% 50%);
+ --chart-2: hsl(160 60% 45%);
+ --chart-3: hsl(30 80% 55%);
+ --chart-4: hsl(280 65% 60%);
+ --chart-5: hsl(340 75% 55%);
+ --sidebar-background: hsl(240 5.9% 10%);
+ --sidebar-foreground: hsl(240 4.8% 95.9%);
+ --sidebar-primary: hsl(224.3 76.3% 48%);
+ --sidebar-primary-foreground: hsl(0 0% 100%);
+ --sidebar-accent: hsl(240 3.7% 15.9%);
+ --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
+ --sidebar-border: hsl(240 3.7% 15.9%);
+ --sidebar-ring: hsl(217.2 91.2% 59.8%);
+ --sidebar: hsl(240 5.9% 10%);
+}
+
+/* define theme */
+@theme {
+ --font-sans: var(--font-geist);
+ --font-mono: var(--font-geist-mono);
+
+ --breakpoint-toast-mobile: 600px;
+
--radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-sm: calc(var(--radius) - 4px);
+
--color-background: var(--background);
--color-foreground: var(--foreground);
+
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
+
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
+
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
+
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
+
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
+
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
+
--color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
+
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
- --color-sidebar: var(--sidebar);
+
+ --color-sidebar: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
@@ -46,156 +142,24 @@
--color-sidebar-ring: var(--sidebar-ring);
}
-:root {
- --radius: 0.625rem;
- --background: oklch(0.985 0 0);
- --foreground: oklch(0.12 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.12 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.12 0 0);
- --primary: oklch(0.12 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.965 0 0);
- --secondary-foreground: oklch(0.38 0 0);
- --muted: oklch(0.94 0 0);
- --muted-foreground: oklch(0.58 0 0);
- --accent: oklch(0.965 0 0);
- --accent-foreground: oklch(0.12 0 0);
- --destructive: oklch(0.55 0.15 25);
- --border: oklch(0.9 0 0);
- --input: oklch(0.9 0 0);
- --ring: oklch(0.5 0 0);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.97 0 0);
- --sidebar-foreground: oklch(0.38 0 0);
- --sidebar-primary: oklch(0.12 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.12 0 0 / 0.06);
- --sidebar-accent-foreground: oklch(0.12 0 0);
- --sidebar-border: oklch(0.88 0 0);
- --sidebar-ring: oklch(0.5 0 0);
-
- --shadow-card: 0 1px 3px oklch(0 0 0 / 0.05), 0 1px 1px oklch(0 0 0 / 0.03);
- --shadow-float:
- 0 8px 24px -6px oklch(0 0 0 / 0.1), 0 2px 8px -2px oklch(0 0 0 / 0.04);
- --shadow-composer: 0 1px 2px oklch(0 0 0 / 0.04);
- --shadow-composer-focus:
- 0 0 0 1px oklch(0 0 0 / 0.06), 0 2px 8px -2px oklch(0 0 0 / 0.06);
- --shadow-inset: inset 0 1px 1px oklch(0 0 0 / 0.03);
- --shadow-glow: 0 0 20px oklch(0 0 0 / 0.08);
-
- --ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
- --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
- --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.dark {
- --background: oklch(0.195 0 0);
- --foreground: oklch(0.94 0 0);
- --card: oklch(0.225 0 0);
- --card-foreground: oklch(0.94 0 0);
- --popover: oklch(0.225 0 0);
- --popover-foreground: oklch(0.94 0 0);
- --primary: oklch(0.94 0 0);
- --primary-foreground: oklch(0.195 0 0);
- --secondary: oklch(0.26 0 0);
- --secondary-foreground: oklch(0.75 0 0);
- --muted: oklch(0.165 0 0);
- --muted-foreground: oklch(0.6 0 0);
- --accent: oklch(0.26 0 0);
- --accent-foreground: oklch(0.94 0 0);
- --destructive: oklch(0.7 0.15 25);
- --border: oklch(0.27 0 0);
- --input: oklch(0.27 0 0);
- --ring: oklch(0.45 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.175 0 0);
- --sidebar-foreground: oklch(0.78 0 0);
- --sidebar-primary: oklch(0.94 0 0);
- --sidebar-primary-foreground: oklch(0.195 0 0);
- --sidebar-accent: oklch(0.94 0 0 / 0.06);
- --sidebar-accent-foreground: oklch(0.94 0 0);
- --sidebar-border: oklch(0.25 0 0);
- --sidebar-ring: oklch(0.45 0 0);
-
- --shadow-card:
- inset 0 1px 0 oklch(1 0 0 / 0.04), 0 1px 2px oklch(0 0 0 / 0.2),
- 0 0.5px 1px oklch(0 0 0 / 0.15);
- --shadow-float:
- 0 0 0 1px oklch(1 0 0 / 0.06), 0 16px 48px -6px oklch(0 0 0 / 0.35),
- 0 6px 12px -2px oklch(0 0 0 / 0.2);
- --shadow-composer:
- 0 1px 3px oklch(0 0 0 / 0.2), inset 0 1px 0 oklch(1 0 0 / 0.03);
- --shadow-composer-focus:
- 0 0 0 1px oklch(1 0 0 / 0.1), 0 4px 16px -4px oklch(0 0 0 / 0.3),
- inset 0 1px 0 oklch(1 0 0 / 0.04);
-}
-
-@layer base {
- * {
- @apply border-border ring-0;
- }
- body {
- @apply bg-background text-foreground;
- font-feature-settings: "ss01", "ss02", "cv01";
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- text-rendering: optimizeLegibility;
- }
-}
+/*
+ The default border color has changed to `currentcolor` in Tailwind CSS v4,
+ so we've added these compatibility styles to make sure everything still
+ looks the same as it did with Tailwind CSS v3.
+ If we ever want to remove these styles, we need to add an explicit border
+ color utility to any element that depends on these defaults.
+*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
- border-color: var(--border);
+ border-color: var(--color-gray-200, currentcolor);
}
}
-@layer base {
- body {
- overflow-x: hidden;
- position: relative;
- }
-
- html {
- overflow-x: hidden;
- }
-
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- letter-spacing: -0.025em;
- line-height: 1.2;
- }
-
- p {
- line-height: 1.6;
- }
-}
-
-button:focus-visible,
-select:focus-visible,
-[role="button"]:focus-visible,
-input:focus-visible,
-textarea:focus-visible {
- outline: none;
-}
-
@utility text-balance {
text-wrap: balance;
}
@@ -212,14 +176,6 @@ textarea:focus-visible {
overscroll-behavior: contain;
}
-@utility no-scrollbar {
- -ms-overflow-style: none;
- scrollbar-width: none;
- &::-webkit-scrollbar {
- display: none;
- }
-}
-
@layer utilities {
:root {
--foreground-rgb: 0, 0, 0;
@@ -236,6 +192,22 @@ textarea:focus-visible {
}
}
+@layer base {
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ overflow-x: hidden;
+ position: relative;
+ }
+
+ html {
+ overflow-x: hidden;
+ }
+}
+
.skeleton {
* {
pointer-events: none !important;
@@ -255,246 +227,92 @@ textarea:focus-visible {
}
}
-@keyframes fade-up {
- from {
- opacity: 0;
- transform: translateY(6px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes fade-in {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-@keyframes shimmer {
- 0% {
- background-position: -200% 0;
- }
- 100% {
- background-position: 200% 0;
- }
-}
-
-@keyframes dot-pulse {
- 0%,
- 60%,
- 100% {
- opacity: 0.3;
- transform: translateY(0);
- }
- 30% {
- opacity: 1;
- transform: translateY(-3px);
- }
-}
-
-@keyframes message-in {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes thinking-dot {
- 0%,
- 60%,
- 100% {
- opacity: 0.3;
- transform: translateY(0);
- }
- 30% {
- opacity: 1;
- transform: translateY(-3px);
- }
-}
-
-@keyframes glow-pulse {
- 0%,
- 100% {
- box-shadow: 0 0 0 0 oklch(0.55 0.12 250 / 0%);
- }
- 50% {
- box-shadow: 0 0 0 3px oklch(0.55 0.12 250 / 8%);
- }
-}
-
-@keyframes subtle-lift {
- from {
- transform: translateY(0);
- box-shadow: var(--shadow-card);
- }
- to {
- transform: translateY(-1px);
- box-shadow: var(--shadow-float);
- }
-}
-
-@utility fade-up {
- animation: fade-up 0.25s var(--ease-spring) both;
-}
-
-@utility fade-in {
- animation: fade-in 0.2s ease both;
-}
-
-@utility shimmer {
- animation: shimmer 2s linear infinite;
- background: linear-gradient(
- 90deg,
- transparent,
- oklch(1 0 0 / 0.04),
- transparent
- );
- background-size: 200% 100%;
-}
-
-@utility dot-pulse {
- animation: dot-pulse 1.4s ease-in-out infinite;
-}
-
-@utility message-fade-in {
- animation: message-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
-}
-
-@utility thinking-dot {
- animation: thinking-dot 1.4s ease-in-out infinite;
-}
-
-@utility composer-glow {
- animation: glow-pulse 2s ease-in-out infinite;
-}
-
.ProseMirror {
outline: none;
}
-.cm-editor {
- @apply bg-transparent! outline-hidden! text-[13px]! leading-[1.6]!;
- font-family:
- "SF Mono", "Cascadia Code", "Fira Code", "JetBrains Mono", ui-monospace,
- monospace !important;
-}
-
+.cm-editor,
.cm-gutters {
- @apply bg-transparent! border-r-0! outline-hidden!;
-}
-
-.cm-gutter.cm-lineNumbers {
- @apply min-w-[3rem] text-muted-foreground/40 text-[11px]!;
-}
-
-.cm-scroller {
- @apply overflow-auto!;
+ @apply bg-background! dark:bg-zinc-800! outline-hidden! selection:bg-zinc-900!;
}
.ͼo.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
.ͼo.cm-selectionBackground,
.ͼo.cm-content::selection {
- background: oklch(0.55 0.12 250 / 0.15) !important;
-}
-
-.dark
- .ͼo.cm-focused
- > .cm-scroller
- > .cm-selectionLayer
- .cm-selectionBackground,
-.dark .ͼo.cm-selectionBackground,
-.dark .ͼo.cm-content::selection {
- background: oklch(0.55 0.12 250 / 0.2) !important;
-}
-
-.cm-activeLine {
- @apply bg-muted/50! rounded-sm!;
+ @apply bg-zinc-200! dark:bg-zinc-900!;
}
+.cm-activeLine,
.cm-activeLineGutter {
@apply bg-transparent!;
}
-.cm-activeLineGutter .cm-gutterElement {
- color: var(--foreground) !important;
- opacity: 0.7;
+.cm-activeLine {
+ @apply rounded-r-sm!;
}
-.cm-gutter.cm-lineNumbers .cm-gutterElement {
- padding-right: 12px !important;
+.cm-lineNumbers {
+ @apply min-w-7;
}
.cm-foldGutter {
@apply min-w-3;
}
-.cm-cursor {
- border-left-color: oklch(0.55 0.12 250) !important;
- border-left-width: 2px !important;
-}
-
-.cm-matchingBracket {
- background: oklch(0.55 0.12 250 / 0.12) !important;
- outline: 1px solid oklch(0.55 0.12 250 / 0.3);
- border-radius: 2px;
+.cm-lineNumbers .cm-activeLineGutter {
+ @apply rounded-l-sm!;
}
.suggestion-highlight {
- @apply cursor-pointer rounded-sm bg-blue-200 transition-colors hover:bg-blue-300 dark:bg-blue-500/40 dark:text-blue-50 dark:hover:bg-blue-400/50;
- user-select: none;
- -webkit-user-select: none;
+ @apply bg-blue-200 hover:bg-blue-300 dark:hover:bg-blue-400/50 dark:text-blue-50 dark:bg-blue-500/40;
+}
+
+/* minimal scrollbar styling */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 3px;
+ transition: background 0.2s ease;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: --alpha(var(--muted-foreground) / 0.5);
+}
+
+::-webkit-scrollbar-corner {
+ background: transparent;
+}
+
+/* firefox scrollbar styling */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border) transparent;
+}
+
+@theme inline {
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
- scrollbar-width: thin;
- scrollbar-color: oklch(0 0 0 / 0.12) transparent;
+ @apply border-border outline-ring/50;
}
-
- .dark * {
- scrollbar-color: oklch(1 0 0 / 0.1) transparent;
- }
-
- *::-webkit-scrollbar {
- width: 4px;
- height: 4px;
- }
-
- *::-webkit-scrollbar-track {
- background: transparent;
- }
-
- *::-webkit-scrollbar-thumb {
- background: oklch(0 0 0 / 0.12);
- border-radius: 9999px;
- }
-
- *::-webkit-scrollbar-thumb:hover {
- background: oklch(0 0 0 / 0.25);
- }
-
- .dark *::-webkit-scrollbar-thumb {
- background: oklch(1 0 0 / 0.1);
- }
-
- .dark *::-webkit-scrollbar-thumb:hover {
- background: oklch(1 0 0 / 0.2);
- }
-
- *::-webkit-scrollbar-corner {
- background: transparent;
+ body {
+ @apply bg-background text-foreground;
}
}
-
-[data-testid="artifact"] {
- isolation: isolate;
-}
diff --git a/app/layout.tsx b/app/layout.tsx
index d427e99..348a4d6 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,10 +1,11 @@
+import { Analytics } from "@vercel/analytics/next";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
+import { Toaster } from "sonner";
import { ThemeProvider } from "@/components/theme-provider";
-import { TooltipProvider } from "@/components/ui/tooltip";
+import "katex/dist/katex.min.css";
import "./globals.css";
-import { SessionProvider } from "next-auth/react";
export const metadata: Metadata = {
metadataBase: new URL("https://chat.vercel.ai"),
@@ -13,7 +14,7 @@ export const metadata: Metadata = {
};
export const viewport = {
- maximumScale: 1,
+ maximumScale: 1, // Disable auto-zoom on mobile Safari
};
const geist = Geist({
@@ -56,6 +57,10 @@ export default function RootLayout({
return (
@@ -74,12 +79,10 @@ export default function RootLayout({
disableTransitionOnChange
enableSystem
>
-
- {children}
-
+
+ {children}
+