From f9652b452a4dc006af947c29301f4845efec892a Mon Sep 17 00:00:00 2001 From: dancer <144584931+dancer@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:37:02 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20v1=20=E2=80=94=20persistent=20shell,=20?= =?UTF-8?q?model=20gateway,=20artifact=20improvements=20(#1462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 19 +- .github/workflows/lint.yml | 2 +- .gitignore | 23 +- README.md | 2 +- app/(auth)/actions.ts | 37 +- app/(auth)/api/auth/[...nextauth]/route.ts | 1 + app/(auth)/api/auth/guest/route.ts | 26 + app/(auth)/auth.config.ts | 14 + app/(auth)/auth.ts | 99 +++ app/(auth)/layout.tsx | 43 ++ app/(auth)/login/page.tsx | 69 +- app/(auth)/register/page.tsx | 65 +- app/(chat)/actions.ts | 31 +- app/(chat)/api/auth/[...all]/route.ts | 4 - app/(chat)/api/chat/route.ts | 133 +++- app/(chat)/api/chat/schema.ts | 10 +- app/(chat)/api/document/route.ts | 58 +- app/(chat)/api/files/upload/route.ts | 10 +- app/(chat)/api/history/route.ts | 11 +- app/(chat)/api/messages/route.ts | 43 ++ app/(chat)/api/models/route.ts | 20 + app/(chat)/api/suggestions/route.ts | 4 +- app/(chat)/api/vote/route.ts | 29 +- app/(chat)/chat/[id]/page.tsx | 82 +-- app/(chat)/layout.tsx | 36 +- app/(chat)/page.tsx | 51 +- app/globals.css | 458 ++++++++---- app/layout.tsx | 34 +- artifacts/code/client.tsx | 20 +- artifacts/code/server.ts | 82 +-- artifacts/image/client.tsx | 6 +- artifacts/sheet/client.tsx | 8 +- artifacts/sheet/server.ts | 77 +- artifacts/text/client.tsx | 27 +- artifacts/text/server.ts | 40 +- components/ai-elements/message.tsx | 4 +- components/ai-elements/model-selector.tsx | 58 +- components/ai-elements/reasoning.tsx | 60 +- components/ai-elements/tool.tsx | 4 +- components/artifact-actions.tsx | 107 --- components/artifact.tsx | 535 -------------- components/chat.tsx | 288 -------- components/{ => chat}/app-sidebar.tsx | 79 +- components/chat/artifact-actions.tsx | 119 ++++ .../{ => chat}/artifact-close-button.tsx | 11 +- components/{ => chat}/artifact-messages.tsx | 0 components/chat/artifact.tsx | 482 +++++++++++++ components/{ => chat}/auth-form.tsx | 27 +- components/{ => chat}/chat-header.tsx | 36 +- components/{ => chat}/code-editor.tsx | 69 +- components/{ => chat}/console.tsx | 65 +- components/{ => chat}/create-artifact.tsx | 0 components/{ => chat}/data-stream-handler.tsx | 1 - .../{ => chat}/data-stream-provider.tsx | 0 components/{ => chat}/diffview.tsx | 26 +- components/{ => chat}/document-preview.tsx | 151 ++-- components/chat/document-skeleton.tsx | 38 + components/{ => chat}/document.tsx | 10 +- components/chat/greeting.tsx | 24 + components/{ => chat}/icons.tsx | 0 components/{ => chat}/image-editor.tsx | 0 components/{ => chat}/message-actions.tsx | 33 +- components/chat/message-editor.tsx | 33 + components/{ => chat}/message-reasoning.tsx | 2 +- components/chat/message.tsx | 387 ++++++++++ components/{ => chat}/messages.tsx | 47 +- components/{ => chat}/multimodal-input.tsx | 400 +++++++++-- components/{ => chat}/preview-attachment.tsx | 30 +- components/chat/preview.tsx | 59 ++ components/{ => chat}/sheet-editor.tsx | 8 +- components/chat/shell.tsx | 205 ++++++ .../{ => chat}/sidebar-history-item.tsx | 32 +- components/{ => chat}/sidebar-history.tsx | 87 ++- components/{ => chat}/sidebar-toggle.tsx | 2 +- components/chat/sidebar-user-nav.tsx | 121 ++++ components/{ => chat}/sign-out-form.tsx | 11 +- components/chat/slash-commands.tsx | 137 ++++ components/{ => chat}/submit-button.tsx | 4 +- components/{ => chat}/suggested-actions.tsx | 30 +- components/chat/suggestion.tsx | 78 ++ components/{ => chat}/text-editor.tsx | 88 ++- components/{ => chat}/toast.tsx | 8 +- components/{ => chat}/toolbar.tsx | 152 ++-- components/chat/version-footer.tsx | 148 ++++ components/{ => chat}/visibility-selector.tsx | 7 +- components/{ => chat}/weather.tsx | 0 components/document-skeleton.tsx | 39 - components/greeting.tsx | 29 - components/message-editor.tsx | 112 --- components/message.tsx | 395 ---------- components/sidebar-user-nav.tsx | 136 ---- components/suggestion.tsx | 77 -- components/ui/alert-dialog.tsx | 2 +- components/ui/button.tsx | 2 +- components/ui/command.tsx | 5 +- components/ui/dropdown-menu.tsx | 6 +- components/ui/popover.tsx | 40 ++ components/ui/sheet.tsx | 4 +- components/ui/sidebar.tsx | 96 +-- components/version-footer.tsx | 107 --- drizzle.config.ts | 3 +- hooks/use-active-chat.tsx | 301 ++++++++ hooks/use-artifact.ts | 2 +- hooks/use-auto-resume.ts | 5 +- hooks/use-chat-visibility.ts | 4 +- hooks/use-messages.tsx | 2 + hooks/use-scroll-to-bottom.tsx | 17 +- lib/ai/entitlements.ts | 13 +- lib/ai/models.mock.ts | 50 -- lib/ai/models.test.ts | 64 +- lib/ai/models.ts | 195 +++-- lib/ai/prompts.ts | 128 ++-- lib/ai/providers.ts | 41 +- lib/ai/tools/create-document.ts | 27 +- lib/ai/tools/edit-document.ts | 100 +++ lib/ai/tools/get-weather.ts | 1 - lib/ai/tools/request-suggestions.ts | 16 +- lib/ai/tools/update-document.ts | 27 +- lib/artifacts/server.ts | 15 +- lib/auth.ts | 58 -- lib/client.ts | 9 - lib/constants.ts | 13 + lib/db/helpers/01-core-to-parts.ts | 253 ------- lib/db/migrate.ts | 8 +- lib/db/migrations/0000_initial.sql | 67 ++ lib/db/migrations/0000_keen_devos.sql | 18 - .../migrations/0001_sparkling_blue_marvel.sql | 39 - lib/db/migrations/0002_wandering_riptide.sql | 35 - lib/db/migrations/0003_cloudy_glorian.sql | 1 - lib/db/migrations/0004_odd_slayback.sql | 1 - lib/db/migrations/0005_wooden_whistler.sql | 33 - .../migrations/0006_marvelous_frog_thor.sql | 12 - lib/db/migrations/0007_flowery_ben_parker.sql | 1 - lib/db/migrations/0008_flat_forgotten_one.sql | 1 - lib/db/migrations/0010_better_auth.sql | 49 -- lib/db/migrations/meta/0000_snapshot.json | 90 --- lib/db/migrations/meta/0001_snapshot.json | 236 ------ lib/db/migrations/meta/0002_snapshot.json | 339 --------- lib/db/migrations/meta/0003_snapshot.json | 346 --------- lib/db/migrations/meta/0004_snapshot.json | 353 --------- lib/db/migrations/meta/0005_snapshot.json | 462 ------------ lib/db/migrations/meta/0006_snapshot.json | 506 ------------- lib/db/migrations/meta/0007_snapshot.json | 512 ------------- lib/db/migrations/meta/0008_snapshot.json | 506 ------------- lib/db/migrations/meta/_journal.json | 60 +- lib/db/queries.ts | 95 ++- lib/db/schema.ts | 98 +-- lib/db/utils.ts | 16 + lib/editor/functions.tsx | 21 +- lib/editor/react-renderer.tsx | 18 +- lib/editor/suggestions.tsx | 84 +-- lib/errors.ts | 2 +- lib/types.ts | 4 +- lib/utils.ts | 29 - next.config.ts | 17 +- package.json | 12 +- pnpm-lock.yaml | 673 ++++++------------ proxy.ts | 36 +- public/preview.png | Bin 0 -> 39834 bytes tests/e2e/model-selector.test.ts | 22 +- tests/pages/chat.ts | 2 +- 161 files changed, 5166 insertions(+), 8009 deletions(-) create mode 100644 app/(auth)/api/auth/[...nextauth]/route.ts create mode 100644 app/(auth)/api/auth/guest/route.ts create mode 100644 app/(auth)/auth.config.ts create mode 100644 app/(auth)/auth.ts create mode 100644 app/(auth)/layout.tsx delete mode 100644 app/(chat)/api/auth/[...all]/route.ts create mode 100644 app/(chat)/api/messages/route.ts create mode 100644 app/(chat)/api/models/route.ts delete mode 100644 components/artifact-actions.tsx delete mode 100644 components/artifact.tsx delete mode 100644 components/chat.tsx rename components/{ => chat}/app-sidebar.tsx (61%) create mode 100644 components/chat/artifact-actions.tsx rename components/{ => chat}/artifact-close-button.tsx (68%) rename components/{ => chat}/artifact-messages.tsx (100%) create mode 100644 components/chat/artifact.tsx rename components/{ => chat}/auth-form.tsx (53%) rename components/{ => chat}/chat-header.tsx (50%) rename components/{ => chat}/code-editor.tsx (69%) rename components/{ => chat}/console.tsx (70%) rename components/{ => chat}/create-artifact.tsx (100%) rename components/{ => chat}/data-stream-handler.tsx (98%) rename components/{ => chat}/data-stream-provider.tsx (100%) rename components/{ => chat}/diffview.tsx (75%) rename components/{ => chat}/document-preview.tsx (61%) create mode 100644 components/chat/document-skeleton.tsx rename components/{ => chat}/document.tsx (92%) create mode 100644 components/chat/greeting.tsx rename components/{ => chat}/icons.tsx (100%) rename components/{ => chat}/image-editor.tsx (100%) rename components/{ => chat}/message-actions.tsx (83%) create mode 100644 components/chat/message-editor.tsx rename components/{ => chat}/message-reasoning.tsx (95%) create mode 100644 components/chat/message.tsx rename components/{ => chat}/messages.tsx (63%) rename components/{ => chat}/multimodal-input.tsx (52%) rename components/{ => chat}/preview-attachment.tsx (57%) create mode 100644 components/chat/preview.tsx rename components/{ => chat}/sheet-editor.tsx (93%) create mode 100644 components/chat/shell.tsx rename components/{ => chat}/sidebar-history-item.tsx (78%) rename components/{ => chat}/sidebar-history.tsx (81%) rename components/{ => chat}/sidebar-toggle.tsx (95%) create mode 100644 components/chat/sidebar-user-nav.tsx rename components/{ => chat}/sign-out-form.tsx (62%) create mode 100644 components/chat/slash-commands.tsx rename components/{ => chat}/submit-button.tsx (88%) rename components/{ => chat}/suggested-actions.tsx (60%) create mode 100644 components/chat/suggestion.tsx rename components/{ => chat}/text-editor.tsx (59%) rename components/{ => chat}/toast.tsx (89%) rename components/{ => chat}/toolbar.tsx (82%) create mode 100644 components/chat/version-footer.tsx rename components/{ => chat}/visibility-selector.tsx (90%) rename components/{ => chat}/weather.tsx (100%) delete mode 100644 components/document-skeleton.tsx delete mode 100644 components/greeting.tsx delete mode 100644 components/message-editor.tsx delete mode 100644 components/message.tsx delete mode 100644 components/sidebar-user-nav.tsx delete mode 100644 components/suggestion.tsx create mode 100644 components/ui/popover.tsx delete mode 100644 components/version-footer.tsx create mode 100644 hooks/use-active-chat.tsx create mode 100644 lib/ai/tools/edit-document.ts delete mode 100644 lib/auth.ts delete mode 100644 lib/client.ts delete mode 100644 lib/db/helpers/01-core-to-parts.ts create mode 100644 lib/db/migrations/0000_initial.sql delete mode 100644 lib/db/migrations/0000_keen_devos.sql delete mode 100644 lib/db/migrations/0001_sparkling_blue_marvel.sql delete mode 100644 lib/db/migrations/0002_wandering_riptide.sql delete mode 100644 lib/db/migrations/0003_cloudy_glorian.sql delete mode 100644 lib/db/migrations/0004_odd_slayback.sql delete mode 100644 lib/db/migrations/0005_wooden_whistler.sql delete mode 100644 lib/db/migrations/0006_marvelous_frog_thor.sql delete mode 100644 lib/db/migrations/0007_flowery_ben_parker.sql delete mode 100644 lib/db/migrations/0008_flat_forgotten_one.sql delete mode 100644 lib/db/migrations/0010_better_auth.sql delete mode 100644 lib/db/migrations/meta/0000_snapshot.json delete mode 100644 lib/db/migrations/meta/0001_snapshot.json delete mode 100644 lib/db/migrations/meta/0002_snapshot.json delete mode 100644 lib/db/migrations/meta/0003_snapshot.json delete mode 100644 lib/db/migrations/meta/0004_snapshot.json delete mode 100644 lib/db/migrations/meta/0005_snapshot.json delete mode 100644 lib/db/migrations/meta/0006_snapshot.json delete mode 100644 lib/db/migrations/meta/0007_snapshot.json delete mode 100644 lib/db/migrations/meta/0008_snapshot.json create mode 100644 lib/db/utils.ts create mode 100644 public/preview.png diff --git a/.env.example b/.env.example index 50c7f12..8433714 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 809a353..8e87279 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 05bfb89..344b787 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 0c54ca8..d493dc4 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 fb817ad..024ff51 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 0000000..d104b65 --- /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 0000000..97ce3d2 --- /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 0000000..5056101 --- /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 0000000..b75369f --- /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 0000000..d14e92c --- /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 ( +
+
+ + + Back + +
+
+
+ +
+ {children} +
+
+
+ +
+
+ Powered by + + AI Gateway +
+
+ +
+
+
+ ); +} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index d2445e2..ea4c602 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 46c73e9..f2abbc8 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 7846b80..2955a53 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 83ab371..0000000 --- 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 c91a8c3..ac52197 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 60a708a..35f785b 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 36a550f..ee66e30 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 b81566d..b227033 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 4e80080..064a385 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 0000000..cda98de --- /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 0000000..de1d12a --- /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 babfb9d..303f45e 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 4f69e2c..726ba56 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(); + let chatId: string; + let messageId: string; + let type: "up" | "down"; - if (!chatId || !messageId || !type) { + 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 f8c10fa..67e0859 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 aca0f0f..8ea30ec 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 ( <>