diff --git a/.env.example b/.env.example
index 50c7f12f7f..8433714cc5 100644
--- a/.env.example
+++ b/.env.example
@@ -1,24 +1,15 @@
-# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
-BETTER_AUTH_SECRET=****
-BETTER_AUTH_URL=****
+# generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
+AUTH_SECRET=****
-# 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
+# required for non-vercel deployments, vercel uses OIDC automatically
# https://vercel.com/ai-gateway
AI_GATEWAY_API_KEY=****
-
-# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob
+# https://vercel.com/docs/vercel-blob
BLOB_READ_WRITE_TOKEN=****
-# Instructions to create a PostgreSQL database here: https://vercel.com/docs/postgres
+# 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 809a353d6b..8e872793b3 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: 9.12.3
+ version: 10.32.1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
diff --git a/.gitignore b/.gitignore
index 05bfb89a1e..344b787948 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,46 +1,27 @@
-# 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
\ No newline at end of file
+next-env.d.ts
+tsconfig.tsbuildinfo
diff --git a/README.md b/README.md
index 0c54ca8e7a..d493dc430f 100644
--- a/README.md
+++ b/README.md
@@ -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. The default model is [OpenAI](https://openai.com) GPT-4.1 Mini, with support for Anthropic, Google, and xAI models.
+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.
### AI Gateway Authentication
diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts
index fb817ad50f..024ff518ed 100644
--- a/app/(auth)/actions.ts
+++ b/app/(auth)/actions.ts
@@ -1,7 +1,10 @@
"use server";
import { z } from "zod";
-import { auth } from "@/lib/auth";
+
+import { createUser, getUser } from "@/lib/db/queries";
+
+import { signIn } from "./auth";
const authFormSchema = z.object({
email: z.string().email(),
@@ -22,11 +25,10 @@ export const login = async (
password: formData.get("password"),
});
- await auth.api.signInEmail({
- body: {
- email: validatedData.email,
- password: validatedData.password,
- },
+ await signIn("credentials", {
+ email: validatedData.email,
+ password: validatedData.password,
+ redirect: false,
});
return { status: "success" };
@@ -59,17 +61,17 @@ export const register = async (
password: formData.get("password"),
});
- const result = await auth.api.signUpEmail({
- body: {
- email: validatedData.email,
- password: validatedData.password,
- name: validatedData.email,
- },
- });
+ const [user] = await getUser(validatedData.email);
- if (!result) {
- return { status: "failed" };
+ 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,
+ });
return { status: "success" };
} catch (error) {
@@ -77,11 +79,6 @@ export const register = async (
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
new file mode 100644
index 0000000000..d104b65e6d
--- /dev/null
+++ b/app/(auth)/api/auth/[...nextauth]/route.ts
@@ -0,0 +1 @@
+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
new file mode 100644
index 0000000000..97ce3d2c14
--- /dev/null
+++ b/app/(auth)/api/auth/guest/route.ts
@@ -0,0 +1,26 @@
+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
new file mode 100644
index 0000000000..50561014a0
--- /dev/null
+++ b/app/(auth)/auth.config.ts
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 0000000000..b75369fe3d
--- /dev/null
+++ b/app/(auth)/auth.ts
@@ -0,0 +1,99 @@
+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
new file mode 100644
index 0000000000..d14e92cedc
--- /dev/null
+++ b/app/(auth)/layout.tsx
@@ -0,0 +1,43 @@
+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 d2445e2611..ea4c602eeb 100644
--- a/app/(auth)/login/page.tsx
+++ b/app/(auth)/login/page.tsx
@@ -2,36 +2,30 @@
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/auth-form";
-import { SubmitButton } from "@/components/submit-button";
-import { toast } from "@/components/toast";
-import { useSession } from "@/lib/client";
+import { AuthForm } from "@/components/chat/auth-form";
+import { SubmitButton } from "@/components/chat/submit-button";
+import { toast } from "@/components/chat/toast";
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 { refetch } = useSession();
+ const { update: updateSession } = useSession();
- // biome-ignore lint/correctness/useExhaustiveDependencies: router and refetch are stable refs
+ // biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession 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",
@@ -39,8 +33,8 @@ export default function Page() {
});
} else if (state.status === "success") {
setIsSuccessful(true);
- refetch();
- router.push("/");
+ updateSession();
+ router.refresh();
}
}, [state.status]);
@@ -50,30 +44,23 @@ export default function Page() {
};
return (
-
-
-
-
- Sign In
-
-
- Use your email and password to sign in
-
-
-
- Sign in
-
- {"Don't have an account? "}
-
- Sign up
-
- {" for free."}
-
-
-
-
+ <>
+ Welcome back
+
+ Sign in to your account to continue
+
+
+ Sign in
+
+ {"No account? "}
+
+ Sign up
+
+
+
+ >
);
}
diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx
index 46c73e9f13..f2abbc8620 100644
--- a/app/(auth)/register/page.tsx
+++ b/app/(auth)/register/page.tsx
@@ -2,29 +2,26 @@
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/auth-form";
-import { SubmitButton } from "@/components/submit-button";
-import { toast } from "@/components/toast";
-import { useSession } from "@/lib/client";
+import { AuthForm } from "@/components/chat/auth-form";
+import { SubmitButton } from "@/components/chat/submit-button";
+import { toast } from "@/components/chat/toast";
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 { refetch } = useSession();
+ const { update: updateSession } = useSession();
- // biome-ignore lint/correctness/useExhaustiveDependencies: router and refetch are stable refs
+ // biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession are stable refs
useEffect(() => {
if (state.status === "user_exists") {
toast({ type: "error", description: "Account already exists!" });
@@ -36,11 +33,10 @@ export default function Page() {
description: "Failed validating your submission!",
});
} else if (state.status === "success") {
- toast({ type: "success", description: "Account created successfully!" });
-
+ toast({ type: "success", description: "Account created!" });
setIsSuccessful(true);
- refetch();
- router.push("/");
+ updateSession();
+ router.refresh();
}
}, [state.status]);
@@ -50,30 +46,21 @@ export default function Page() {
};
return (
-
-
-
-
- Sign Up
-
-
- Create an account with your email and password
-
-
-
- Sign Up
-
- {"Already have an account? "}
-
- Sign in
-
- {" instead."}
-
-
-
-
+ <>
+ Create account
+ Get started for free
+
+ Sign up
+
+ {"Have an account? "}
+
+ Sign in
+
+
+
+ >
);
}
diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts
index 7846b806bc..2955a53fbc 100644
--- a/app/(chat)/actions.ts
+++ b/app/(chat)/actions.ts
@@ -2,11 +2,14 @@
import { generateText, type UIMessage } from "ai";
import { cookies } from "next/headers";
-import type { VisibilityType } from "@/components/visibility-selector";
+import { auth } from "@/app/(auth)/auth";
+import type { VisibilityType } from "@/components/chat/visibility-selector";
+import { titleModel } from "@/lib/ai/models";
import { titlePrompt } from "@/lib/ai/prompts";
import { getTitleModel } from "@/lib/ai/providers";
import {
deleteMessagesByChatIdAfterTimestamp,
+ getChatById,
getMessageById,
updateChatVisibilityById,
} from "@/lib/db/queries";
@@ -26,6 +29,9 @@ export async function generateTitleFromUserMessage({
model: getTitleModel(),
system: titlePrompt,
prompt: getTextFromMessage(message),
+ providerOptions: {
+ gateway: { order: titleModel.gatewayOrder },
+ },
});
return text
.replace(/^[#*"\s]+/, "")
@@ -34,7 +40,20 @@ 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,
@@ -49,5 +68,15 @@ 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
deleted file mode 100644
index 83ab371a71..0000000000
--- a/app/(chat)/api/auth/[...all]/route.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-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 c91a8c32b6..ac52197803 100644
--- a/app/(chat)/api/chat/route.ts
+++ b/app/(chat)/api/chat/route.ts
@@ -10,15 +10,21 @@ 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 } from "@/lib/ai/models";
+import {
+ allowedModelIds,
+ chatModels,
+ DEFAULT_CHAT_MODEL,
+ getCapabilities,
+} 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,
@@ -67,20 +73,20 @@ export async function POST(request: Request) {
const [, session] = await Promise.all([
checkBotId().catch(() => null),
- getSession(),
+ auth(),
]);
if (!session?.user) {
return new ChatbotError("unauthorized:chat").toResponse();
}
- if (!allowedModelIds.has(selectedChatModel)) {
- return new ChatbotError("bad_request:api").toResponse();
- }
+ const chatModel = allowedModelIds.has(selectedChatModel)
+ ? selectedChatModel
+ : DEFAULT_CHAT_MODEL;
await checkIpRateLimit(ipAddress(request));
- const userType: UserType = getUserType(session.user);
+ const userType: UserType = session.user.type;
const messageCount = await getMessageCountByUserId({
id: session.user.id,
@@ -101,9 +107,7 @@ export async function POST(request: Request) {
if (chat.userId !== session.user.id) {
return new ChatbotError("forbidden:chat").toResponse();
}
- if (!isToolApprovalFlow) {
- messagesFromDb = await getMessagesByChatId({ id });
- }
+ messagesFromDb = await getMessagesByChatId({ id });
} else if (message?.role === "user") {
await saveChat({
id,
@@ -114,9 +118,43 @@ export async function POST(request: Request) {
titlePromise = generateTitleFromUserMessage({ message });
}
- const uiMessages = isToolApprovalFlow
- ? (messages as ChatMessage[])
- : [...convertToUIMessages(messagesFromDb), message as ChatMessage];
+ 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 { longitude, latitude, city, country } = geolocation(request);
@@ -142,10 +180,11 @@ export async function POST(request: Request) {
});
}
- const isReasoningModel =
- selectedChatModel.endsWith("-thinking") ||
- (selectedChatModel.includes("reasoning") &&
- !selectedChatModel.includes("non-reasoning"));
+ 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 modelMessages = await convertToModelMessages(uiMessages);
@@ -153,30 +192,46 @@ export async function POST(request: Request) {
originalMessages: isToolApprovalFlow ? uiMessages : undefined,
execute: async ({ writer: dataStream }) => {
const result = streamText({
- model: getLanguageModel(selectedChatModel),
- system: systemPrompt({ selectedChatModel, requestHints }),
+ model: getLanguageModel(chatModel),
+ system: systemPrompt({ requestHints, supportsTools }),
messages: modelMessages,
stopWhen: stepCountIs(5),
- experimental_activeTools: isReasoningModel
- ? []
- : [
- "getWeather",
- "createDocument",
- "updateDocument",
- "requestSuggestions",
- ],
- providerOptions: isReasoningModel
- ? {
- anthropic: {
- thinking: { type: "enabled", budgetTokens: 10_000 },
- },
- }
- : undefined,
+ experimental_activeTools:
+ isReasoningModel && !supportsTools
+ ? []
+ : [
+ "getWeather",
+ "createDocument",
+ "editDocument",
+ "updateDocument",
+ "requestSuggestions",
+ ],
+ providerOptions: {
+ ...(modelConfig?.gatewayOrder && {
+ gateway: { order: modelConfig.gatewayOrder },
+ }),
+ ...(modelConfig?.reasoningEffort && {
+ openai: { reasoningEffort: modelConfig.reasoningEffort },
+ }),
+ },
tools: {
getWeather,
- createDocument: createDocument({ session, dataStream }),
- updateDocument: updateDocument({ session, dataStream }),
- requestSuggestions: requestSuggestions({ session, dataStream }),
+ createDocument: createDocument({
+ session,
+ dataStream,
+ modelId: chatModel,
+ }),
+ editDocument: editDocument({ dataStream, session }),
+ updateDocument: updateDocument({
+ session,
+ dataStream,
+ modelId: chatModel,
+ }),
+ requestSuggestions: requestSuggestions({
+ session,
+ dataStream,
+ modelId: chatModel,
+ }),
},
experimental_telemetry: {
isEnabled: isProductionEnvironment,
@@ -262,7 +317,7 @@ export async function POST(request: Request) {
);
}
} catch (_) {
- // ignore redis errors
+ /* non-critical */
}
},
});
@@ -295,7 +350,7 @@ export async function DELETE(request: Request) {
return new ChatbotError("bad_request:api").toResponse();
}
- const session = await getSession();
+ const session = await auth();
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 60a708acda..35f785b846 100644
--- a/app/(chat)/api/chat/schema.ts
+++ b/app/(chat)/api/chat/schema.ts
@@ -20,18 +20,16 @@ const userMessageSchema = z.object({
parts: z.array(partSchema),
});
-// For tool approval flows, we accept all messages (more permissive schema)
-const messageSchema = z.object({
+const toolApprovalMessageSchema = z.object({
id: z.string(),
- role: z.string(),
- parts: z.array(z.any()),
+ role: z.enum(["user", "assistant"]),
+ parts: z.array(z.record(z.unknown())),
});
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(messageSchema).optional(),
+ messages: z.array(toolApprovalMessageSchema).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 36a550f03d..ee66e3057f 100644
--- a/app/(chat)/api/document/route.ts
+++ b/app/(chat)/api/document/route.ts
@@ -1,12 +1,21 @@
-import type { ArtifactKind } from "@/components/artifact";
-import { getSession } from "@/lib/auth";
+import { z } from "zod";
+import { auth } from "@/app/(auth)/auth";
+import type { ArtifactKind } from "@/components/chat/artifact";
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");
@@ -18,7 +27,7 @@ export async function GET(request: Request) {
).toResponse();
}
- const session = await getSession();
+ const session = await auth();
if (!session?.user) {
return new ChatbotError("unauthorized:document").toResponse();
@@ -50,18 +59,29 @@ export async function POST(request: Request) {
).toResponse();
}
- const session = await getSession();
+ const session = await auth();
if (!session?.user) {
return new ChatbotError("not_found:document").toResponse();
}
- const {
- content,
- title,
- kind,
- }: { content: string; title: string; kind: ArtifactKind } =
- await request.json();
+ 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 documents = await getDocumentsById({ id });
@@ -73,6 +93,11 @@ 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,
@@ -103,7 +128,7 @@ export async function DELETE(request: Request) {
).toResponse();
}
- const session = await getSession();
+ const session = await auth();
if (!session?.user) {
return new ChatbotError("unauthorized:document").toResponse();
@@ -117,9 +142,18 @@ 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: new Date(timestamp),
+ timestamp: parsedTimestamp,
});
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 b81566d5b2..b2270331e3 100644
--- a/app/(chat)/api/files/upload/route.ts
+++ b/app/(chat)/api/files/upload/route.ts
@@ -2,23 +2,21 @@ import { put } from "@vercel/blob";
import { NextResponse } from "next/server";
import { z } from "zod";
-import { getSession } from "@/lib/auth";
+import { auth } from "@/app/(auth)/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 getSession();
+ const session = await auth();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -46,12 +44,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(`${filename}`, fileBuffer, {
+ const data = await put(`${safeName}`, fileBuffer, {
access: "public",
});
diff --git a/app/(chat)/api/history/route.ts b/app/(chat)/api/history/route.ts
index 4e80080b39..064a385473 100644
--- a/app/(chat)/api/history/route.ts
+++ b/app/(chat)/api/history/route.ts
@@ -1,12 +1,15 @@
import type { NextRequest } from "next/server";
-import { getSession } from "@/lib/auth";
+import { auth } from "@/app/(auth)/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 = Number.parseInt(searchParams.get("limit") || "10", 10);
+ const limit = Math.min(
+ Math.max(Number.parseInt(searchParams.get("limit") || "10", 10), 1),
+ 50
+ );
const startingAfter = searchParams.get("starting_after");
const endingBefore = searchParams.get("ending_before");
@@ -17,7 +20,7 @@ export async function GET(request: NextRequest) {
).toResponse();
}
- const session = await getSession();
+ const session = await auth();
if (!session?.user) {
return new ChatbotError("unauthorized:chat").toResponse();
@@ -34,7 +37,7 @@ export async function GET(request: NextRequest) {
}
export async function DELETE() {
- const session = await getSession();
+ const session = await auth();
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
new file mode 100644
index 0000000000..cda98dedb2
--- /dev/null
+++ b/app/(chat)/api/messages/route.ts
@@ -0,0 +1,43 @@
+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
new file mode 100644
index 0000000000..de1d12a822
--- /dev/null
+++ b/app/(chat)/api/models/route.ts
@@ -0,0 +1,20 @@
+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 babfb9d58d..303f45ed26 100644
--- a/app/(chat)/api/suggestions/route.ts
+++ b/app/(chat)/api/suggestions/route.ts
@@ -1,4 +1,4 @@
-import { getSession } from "@/lib/auth";
+import { auth } from "@/app/(auth)/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 getSession();
+ const session = await auth();
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 4f69e2cad1..726ba56465 100644
--- a/app/(chat)/api/vote/route.ts
+++ b/app/(chat)/api/vote/route.ts
@@ -1,7 +1,14 @@
-import { getSession } from "@/lib/auth";
+import { z } from "zod";
+import { auth } from "@/app/(auth)/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");
@@ -13,7 +20,7 @@ export async function GET(request: Request) {
).toResponse();
}
- const session = await getSession();
+ const session = await auth();
if (!session?.user) {
return new ChatbotError("unauthorized:vote").toResponse();
@@ -35,21 +42,23 @@ export async function GET(request: Request) {
}
export async function PATCH(request: Request) {
- const {
- chatId,
- messageId,
- type,
- }: { chatId: string; messageId: string; type: "up" | "down" } =
- await request.json();
-
- if (!chatId || !messageId || !type) {
+ let chatId: string;
+ let messageId: string;
+ let type: "up" | "down";
+
+ try {
+ const parsed = voteSchema.parse(await request.json());
+ chatId = parsed.chatId;
+ messageId = parsed.messageId;
+ type = parsed.type;
+ } catch {
return new ChatbotError(
"bad_request:api",
"Parameters chatId, messageId, and type are required."
).toResponse();
}
- const session = await getSession();
+ const session = await auth();
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 f8c10fa151..67e0859135 100644
--- a/app/(chat)/chat/[id]/page.tsx
+++ b/app/(chat)/chat/[id]/page.tsx
@@ -1,81 +1,3 @@
-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 (
- <>
-
-
- >
- );
+export default function Page() {
+ return null;
}
diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx
index aca0f0fe37..8ea30ecf77 100644
--- a/app/(chat)/layout.tsx
+++ b/app/(chat)/layout.tsx
@@ -1,35 +1,53 @@
import { cookies } from "next/headers";
import Script from "next/script";
import { Suspense } from "react";
-import { AppSidebar } from "@/components/app-sidebar";
-import { DataStreamProvider } from "@/components/data-stream-provider";
+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 { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
-import { getSession } from "@/lib/auth";
+import { ActiveChatProvider } from "@/hooks/use-active-chat";
+import { auth } from "../(auth)/auth";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
- }>
- {children}
+ }>
+ {children}
>
);
}
-async function SidebarWrapper({ children }: { children: React.ReactNode }) {
- const [session, cookieStore] = await Promise.all([getSession(), cookies()]);
+async function SidebarShell({ children }: { children: React.ReactNode }) {
+ const [session, cookieStore] = await Promise.all([auth(), 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 332ed4bfdd..67e0859135 100644
--- a/app/(chat)/page.tsx
+++ b/app/(chat)/page.tsx
@@ -1,52 +1,3 @@
-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 (
- }>
-
-
- );
-}
-
-async function NewChatPage() {
- const cookieStore = await cookies();
- const modelIdFromCookie = cookieStore.get("chat-model");
- const id = generateUUID();
-
- if (!modelIdFromCookie) {
- return (
- <>
-
-
- >
- );
- }
-
- return (
- <>
-
-
- >
- );
+ return null;
}
diff --git a/app/globals.css b/app/globals.css
index 2d37c032dc..4143774692 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,13 +1,10 @@
@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";
@@ -51,100 +48,154 @@
:root {
--radius: 0.625rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
+ --background: oklch(0.985 0 0);
+ --foreground: oklch(0.12 0 0);
--card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
+ --card-foreground: oklch(0.12 0 0);
--popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(57.61% 0.2508 258.23);
- --primary-foreground: oklch(1 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 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.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
+ --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.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 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.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(57.61% 0.2508 258.23);
- --primary-foreground: oklch(1 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
+ --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.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
+ --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 outline-ring/50;
+ @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(--color-gray-200, currentcolor);
+ border-color: var(--border);
+ }
+}
+
+@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;
}
@@ -161,6 +212,14 @@
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;
@@ -177,17 +236,6 @@
}
}
-@layer base {
- body {
- overflow-x: hidden;
- position: relative;
- }
-
- html {
- overflow-x: hidden;
- }
-}
-
.skeleton {
* {
pointer-events: none !important;
@@ -207,92 +255,246 @@
}
}
+@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,
+.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-gutters {
- @apply bg-background! dark:bg-neutral-800! outline-hidden! selection:bg-neutral-900!;
+ @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!;
}
.ͼo.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
.ͼo.cm-selectionBackground,
.ͼo.cm-content::selection {
- @apply bg-neutral-200! dark:bg-neutral-900!;
+ 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!;
}
-.cm-activeLine,
.cm-activeLineGutter {
@apply bg-transparent!;
}
-.cm-activeLine {
- @apply rounded-r-sm!;
+.cm-activeLineGutter .cm-gutterElement {
+ color: var(--foreground) !important;
+ opacity: 0.7;
}
-.cm-lineNumbers {
- @apply min-w-7;
+.cm-gutter.cm-lineNumbers .cm-gutterElement {
+ padding-right: 12px !important;
}
.cm-foldGutter {
@apply min-w-3;
}
-.cm-lineNumbers .cm-activeLineGutter {
- @apply rounded-l-sm!;
+.cm-cursor {
+ border-left-color: oklch(0.55 0.12 250) !important;
+ border-left-width: 2px !important;
}
-.suggestion-highlight {
- @apply bg-blue-200 hover:bg-blue-300 dark:hover:bg-blue-400/50 dark:text-blue-50 dark:bg-blue-500/40;
+.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;
}
-/* minimal scrollbar styling */
-::-webkit-scrollbar {
- width: 6px;
- height: 6px;
+.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;
}
-::-webkit-scrollbar-track {
- background: transparent;
-}
+@layer base {
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: oklch(0 0 0 / 0.12) transparent;
+ }
-::-webkit-scrollbar-thumb {
- background: var(--border);
- border-radius: 3px;
- transition: background 0.2s ease;
-}
+ .dark * {
+ scrollbar-color: oklch(1 0 0 / 0.1) transparent;
+ }
-::-webkit-scrollbar-thumb:hover {
- background: --alpha(var(--muted-foreground) / 0.5);
-}
+ *::-webkit-scrollbar {
+ width: 4px;
+ height: 4px;
+ }
-::-webkit-scrollbar-corner {
- background: transparent;
-}
+ *::-webkit-scrollbar-track {
+ background: transparent;
+ }
-/* firefox scrollbar styling */
-* {
- scrollbar-width: thin;
- scrollbar-color: var(--border) transparent;
-}
+ *::-webkit-scrollbar-thumb {
+ background: oklch(0 0 0 / 0.12);
+ border-radius: 9999px;
+ }
-@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);
-}
+ *::-webkit-scrollbar-thumb:hover {
+ background: oklch(0 0 0 / 0.25);
+ }
-@layer base {
- * {
- @apply border-border outline-ring/50;
+ .dark *::-webkit-scrollbar-thumb {
+ background: oklch(1 0 0 / 0.1);
}
- body {
- @apply bg-background text-foreground;
+
+ .dark *::-webkit-scrollbar-thumb:hover {
+ background: oklch(1 0 0 / 0.2);
}
+
+ *::-webkit-scrollbar-corner {
+ background: transparent;
+ }
+}
+
+[data-testid="artifact"] {
+ isolation: isolate;
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 3df2418eb1..d427e99ea7 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,12 +1,10 @@
-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 "katex/dist/katex.min.css";
+import { TooltipProvider } from "@/components/ui/tooltip";
import "./globals.css";
-import { TooltipProvider } from "@/components/ui/tooltip";
+import { SessionProvider } from "next-auth/react";
export const metadata: Metadata = {
metadataBase: new URL("https://chat.vercel.ai"),
@@ -15,7 +13,7 @@ export const metadata: Metadata = {
};
export const viewport = {
- maximumScale: 1, // Disable auto-zoom on mobile Safari
+ maximumScale: 1,
};
const geist = Geist({
@@ -58,10 +56,6 @@ export default function RootLayout({
return (
@@ -74,18 +68,18 @@ export default function RootLayout({
/>
-
-
+
-
- {children}
-
-
-
+ {children}
+
+