From 6726770cf20e13266c7dab13e1f488cf3760207e Mon Sep 17 00:00:00 2001 From: dancer <144584931+dancer@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:40:55 +0000 Subject: [PATCH 01/29] feat: persistent chat shell with active chat context --- app/(chat)/api/messages/route.ts | 43 ++++ app/(chat)/chat/[id]/page.tsx | 82 +----- app/(chat)/layout.tsx | 36 ++- app/(chat)/page.tsx | 51 +--- components/chat/data-stream-handler.tsx | 91 +++++++ components/chat/data-stream-provider.tsx | 41 +++ components/chat/shell.tsx | 205 +++++++++++++++ hooks/use-active-chat.tsx | 301 +++++++++++++++++++++++ 8 files changed, 711 insertions(+), 139 deletions(-) create mode 100644 app/(chat)/api/messages/route.ts create mode 100644 components/chat/data-stream-handler.tsx create mode 100644 components/chat/data-stream-provider.tsx create mode 100644 components/chat/shell.tsx create mode 100644 hooks/use-active-chat.tsx 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)/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 ( <>