Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6726770
feat: persistent chat shell with active chat context
dancer Mar 20, 2026
83938b9
feat: messages component with loading states and scroll reset
dancer Mar 20, 2026
ffd7950
feat: model selector with gateway discovery and per-model routing
dancer Mar 20, 2026
621528b
feat: edit document tool with find-and-replace
dancer Mar 20, 2026
0913643
feat: artifacts use streamText for cross-model compatibility
dancer Mar 20, 2026
38c0a02
feat: artifact panel with auto-scroll, version browsing, and diff
dancer Mar 20, 2026
5281211
feat: suggestion click-to-apply with dialog
dancer Mar 20, 2026
aac95f0
feat: document preview with fade gradients and code icon
dancer Mar 20, 2026
9b21b81
feat: remaining chat components migrated to components/chat
dancer Mar 20, 2026
0b646f2
chore: remove old component files
dancer Mar 20, 2026
c3cbcd9
feat: updated prompts, tool descriptions, and rate limits
dancer Mar 20, 2026
9020933
chore: auth migration from better-auth to next-auth
dancer Mar 20, 2026
a32626f
chore: squash migrations and clean schema
dancer Mar 20, 2026
0cbccee
fix: security — open redirect, timestamp validation, limit bounds
dancer Mar 20, 2026
89b55f4
style: themed toasts, minimal scrollbars, light mode, no focus rings
dancer Mar 20, 2026
4ce9cd4
chore: remove dead code, comments, and unused imports
dancer Mar 20, 2026
84a4e57
chore: upgrade next.js 16.2 with react compiler and experimental feat…
dancer Mar 20, 2026
ec6a00b
chore: update readme, env example, tests, and gitignore
dancer Mar 20, 2026
7ec9943
chore: remove old auth route
dancer Mar 20, 2026
b864aab
fix: lint errors and formatting
dancer Mar 20, 2026
f5b7800
fix: allow sending after rate limit error
dancer Mar 20, 2026
d069903
fix: increase rate limit to 10 per hour
dancer Mar 20, 2026
777e300
fix: update pnpm version in lint workflow
dancer Mar 20, 2026
d9dcb7e
fix: attachments button vision check for both demo and normal mode
dancer Mar 20, 2026
7a9b009
fix: merge per-model capabilities for curated models in demo mode
dancer Mar 20, 2026
b6d9628
feat: slash commands — /new, /clear, /rename, /model, /theme, /delete…
dancer Mar 20, 2026
1c3cd11
fix: security — ownership checks, input validation, filename sanitiza…
dancer Mar 20, 2026
35bf915
fix: reset isMutating after version restore
dancer Mar 20, 2026
aedb476
fix: lint — css specificity order and stable console keys
dancer Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 5 additions & 14 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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=****
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 2 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
next-env.d.ts
tsconfig.tsbuildinfo
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 17 additions & 20 deletions app/(auth)/actions.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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" };
Expand Down Expand Up @@ -59,29 +61,24 @@ 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) {
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" };
}
};
1 change: 1 addition & 0 deletions app/(auth)/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET, POST } from "@/app/(auth)/auth";
26 changes: 26 additions & 0 deletions app/(auth)/api/auth/guest/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
14 changes: 14 additions & 0 deletions app/(auth)/auth.config.ts
Original file line number Diff line number Diff line change
@@ -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;
99 changes: 99 additions & 0 deletions app/(auth)/auth.ts
Original file line number Diff line number Diff line change
@@ -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;
},
},
});
43 changes: 43 additions & 0 deletions app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-dvh w-screen bg-sidebar">
<div className="flex w-full flex-col bg-background p-8 xl:w-[600px] xl:shrink-0 xl:rounded-r-2xl xl:border-r xl:border-border/40 md:p-16">
<Link
className="flex w-fit items-center gap-1.5 text-[13px] text-muted-foreground transition-colors hover:text-foreground"
href="/"
>
<ArrowLeftIcon className="size-3.5" />
Back
</Link>
<div className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center gap-10">
<div className="flex flex-col gap-2">
<div className="mb-2 flex size-9 items-center justify-center rounded-lg bg-muted/60 text-muted-foreground ring-1 ring-border/50">
<SparklesIcon size={14} />
</div>
{children}
</div>
</div>
</div>

<div className="hidden flex-1 flex-col overflow-hidden pl-12 xl:flex">
<div className="flex items-center gap-1.5 pt-8 text-[13px] text-muted-foreground/50">
Powered by
<VercelIcon size={14} />
<span className="font-medium text-muted-foreground">AI Gateway</span>
</div>
<div className="flex-1 pt-4">
<Preview />
</div>
</div>
</div>
);
}
Loading
Loading