├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── ai ├── index.ts ├── models.ts └── prompts.ts ├── app ├── (auth) │ ├── actions.ts │ └── auth.ts ├── (chat) │ └── actions.ts ├── [locale] │ ├── chat │ │ ├── [model] │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── layout.tsx │ ├── not-found.tsx │ └── page.tsx └── api │ ├── auth │ ├── [...nextauth] │ │ └── route.ts │ └── signup │ │ └── route.ts │ ├── chat │ └── route.ts │ ├── files │ └── upload │ │ └── route.ts │ ├── history │ └── route.ts │ ├── updateUser │ └── route.ts │ └── vote │ └── route.ts ├── assets └── fonts │ ├── CalSans-SemiBold.ttf │ ├── CalSans-SemiBold.woff2 │ ├── GeistVF.woff2 │ ├── Inter-Bold.ttf │ ├── Inter-Regular.ttf │ ├── index.ts │ └── satoshi-variable.woff2 ├── components.json ├── components ├── app-nav-projects.tsx ├── app-nav-user.tsx ├── app-sidebar.tsx ├── auth-form.tsx ├── chat │ ├── chat-header.tsx │ ├── chat-ui.tsx │ ├── message-actions.tsx │ ├── message-codeblock.tsx │ ├── message.tsx │ ├── messages.tsx │ ├── multimodal-input.tsx │ ├── overview.tsx │ └── preview-attachment.tsx ├── data-stream-handler.tsx ├── layout │ ├── footer.tsx │ ├── mode-toggle.tsx │ ├── navbar.tsx │ ├── theme-provider.tsx │ ├── toogle-theme.tsx │ └── user-account-nav.tsx ├── locale-switcher.tsx ├── login-form.tsx ├── markdown-react.tsx ├── marketing │ └── hero.tsx ├── modals │ ├── providers.tsx │ └── sign-in-modal.tsx ├── model-selector.tsx ├── register-form.tsx ├── shared │ ├── ai-model-icon.tsx │ ├── blur-image.tsx │ ├── breadcrumbs.tsx │ ├── card-model.tsx │ ├── card-skeleton.tsx │ ├── copy-button.tsx │ ├── icons.tsx │ ├── max-width-wrapper.tsx │ ├── mdx-components.tsx │ ├── section-skeleton.tsx │ ├── section.tsx │ └── user-avatar.tsx ├── sidebar-history.tsx ├── sidebar-toggle.tsx ├── social-link.tsx ├── theme-provider.tsx └── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button-copy.tsx │ ├── button-loading.tsx │ ├── button.tsx │ ├── button2.tsx │ ├── card.tsx │ ├── collapsible.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── modal.tsx │ ├── navigation-menu.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── switch.tsx │ ├── textarea.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ └── tooltip.tsx ├── config ├── footer-navs.ts └── site.ts ├── db ├── queries.ts └── schema.ts ├── drizzle.config.ts ├── global.d.ts ├── hooks ├── use-copy-to-clipboard.tsx ├── use-media-query.ts ├── use-mobile.tsx ├── use-mounted.ts ├── use-scroll-to-bottom.ts └── use-user-message-id.ts ├── i18n ├── request.ts └── routing.ts ├── lib ├── chat-utils.ts ├── navigation.ts ├── s3.ts └── utils.ts ├── messages └── en.json ├── middleware.ts ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── public ├── circles.svg ├── fonts │ ├── geist-mono.woff2 │ └── geist.woff2 ├── globe.svg ├── hero-image-2.png └── models │ ├── gemini-icon.webp │ └── gpt-icon.png ├── styles ├── globals.css ├── loader.css ├── markdown.css └── spinner1.css ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"], 3 | "rules": { 4 | "@next/next/no-html-link-for-pages": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 candytools-ai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chooat AI Chatbot 2 | 3 | An Open-Source AI Chat Platform Powered by Leading AI Models 4 | 5 | Welcome to [Chooat](https://chooat.com) AI Chat, an open-source project designed to provide a seamless and powerful AI chat experience. 6 | 7 | 8 | ## 🚀 Features 9 | - Multi-Model Integration: Chat with the best AI models, including ChatGPT, Claude, and Gemini, all in one platform. 10 | - Dynamic Conversations: Supports natural, responsive, and context-aware interactions. 11 | - Customizable Architecture: Easily integrate other AI models or adjust configurations to suit your needs. 12 | - User-Friendly Interface: Designed for accessibility and simplicity, suitable for both developers and end-users. 13 | - Scalable Backend: Optimized for performance and scalability to handle multiple chat sessions simultaneously. 14 | 15 | 16 | ## 🎯 Why Chooat AI Chat? 17 | 18 | Chooat AI Chat is built to help developers, researchers, and enthusiasts: 19 | - Quickly set up an AI chat platform. 20 | - Experiment with multi-model conversational AI. 21 | - Customize and extend the platform for their specific use cases. 22 | 23 | 24 | ## 🛠️ Tech Stack 25 | 26 | - Next.js 27 | - Vercel AI SDK 28 | - Next-Auth for user auth 29 | - Drizzle ORM + postgres for data processing 30 | - Cloudflare R2 31 | - TailwindCSS 32 | - Shadcn UI 33 | 34 | 35 | ## 🚀 Quick Start 36 | 37 | Follow these steps to get started: 38 | 39 | 1. Clone the repository: 40 | 41 | ``` 42 | git clone https://github.com/candytools-ai/chooat-chat-ai.git 43 | cd chooat-ai-chat 44 | ``` 45 | 46 | 2. Install dependencies: 47 | 48 | ``` 49 | pnpm install 50 | ``` 51 | 52 | 3. Set up API keys: 53 | 54 | - Obtain API keys for your preferred AI models (e.g., OpenAI, Anthropic). 55 | - Add your keys to the .env.local file : 56 | 57 | ``` 58 | # GOOGLE 59 | GOOGLE_ID= 60 | GOOGLE_SECRET= 61 | 62 | # GITHUB 63 | GITHUB_ID= 64 | GITHUB_SECRET= 65 | 66 | AUTH_SECRET= 67 | NEXTAUTH_SECRET= 68 | 69 | POSTGRES_URL= 70 | AUTH_DRIZZLE_URL= 71 | 72 | OPENROUTER_API_KEY= 73 | GOOGLE_GENERATIVE_AI_API_KEY= 74 | 75 | R2_ACCOUNT_ID= 76 | R2_BUCKET= 77 | R2_ACCESS_KEY_ID= 78 | R2_SECRET_ACCESS_KEY= 79 | R2_DOMAIN_URL= 80 | ``` 81 | 82 | 83 | 4. Run the app: 84 | 85 | ``` 86 | pnpm dev 87 | ``` 88 | 89 | 90 | 5. Access the platform: 91 | Open your browser and navigate to http://localhost:3000. 92 | 93 | 94 | Deploy on Vercel (Don't forget to setup env) 95 | 96 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/candytools-ai/chooat-chat-ai.git&project-name=chooat-chat-ai&repository-name=chooat-chat-ai) 97 | 98 | 99 | ## 🤝 Contributing 100 | 101 | We welcome contributions to Chooat AI Chat! Here’s how you can help: 102 | 1. Fork the repository. 103 | 2. Create a new branch for your feature or bug fix. 104 | 3. Submit a pull request with a clear description of your changes. 105 | 106 | 107 | ## 📄 License 108 | 109 | This project is licensed under the MIT License. See the LICENSE file for details. 110 | 111 | 112 | ## 🧑‍💻 Link Me 113 | 114 | For questions, discussions, or support, join our community forum or connect with us on Twitter. 115 | Twitter: [https://x.com/ChooatAI](https://x.com/ChooatAI) -------------------------------------------------------------------------------- /ai/index.ts: -------------------------------------------------------------------------------- 1 | import { createGoogleGenerativeAI } from '@ai-sdk/google'; 2 | import { createOpenAI } from '@ai-sdk/openai'; 3 | 4 | const openrouter = createOpenAI({ 5 | name: 'openrouter', 6 | apiKey: process.env.OPENROUTER_API_KEY, 7 | baseURL: "https://openrouter.ai/api/v1", 8 | fetch: async (url, options) => { 9 | return await fetch(url, options); 10 | } 11 | }); 12 | 13 | const google = createGoogleGenerativeAI({ 14 | // custom settings 15 | apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY 16 | }); 17 | 18 | export const customModel = (apiIdentifier: string) => { 19 | if (apiIdentifier.includes("gemini")) { 20 | return google(apiIdentifier); 21 | } else { 22 | return openrouter(apiIdentifier); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /ai/models.ts: -------------------------------------------------------------------------------- 1 | // Define your models here. 2 | 3 | export interface Model { 4 | id: string; // model id 5 | label: string; // model name 6 | path: string; // model path 7 | image: { 8 | light: string; 9 | dark: string; 10 | } 11 | apiIdentifier: string; 12 | fileAccept?: string[] 13 | formats?: string 14 | } 15 | 16 | export const models: Array = [ 17 | { 18 | id: "gpt-4o", 19 | label: "GPT 4o", 20 | path: "gpt-4o", 21 | image: { 22 | light: "/models/gpt-icon.png", 23 | dark: "/models/gpt-icon.png", 24 | }, 25 | apiIdentifier: "openai/gpt-4o", 26 | fileAccept: ["image/jpeg", "image/png", "image/webp"], 27 | formats: "PNG, JPG, WEBP, TXT", 28 | }, 29 | { 30 | id: "gpt-4o-mini", 31 | label: "GPT 4o mini", 32 | path: "gpt-4o-mini", 33 | image: { 34 | light: "/models/gpt-icon.png", 35 | dark: "/models/gpt-icon.png", 36 | }, 37 | apiIdentifier: "openai/gpt-4o-mini", 38 | fileAccept: ["image/jpeg", "image/png", "image/webp"], 39 | formats: "PNG, JPG, WEBP, TXT", 40 | }, 41 | { 42 | id: "gemini-pro-1-5", 43 | label: "Gemini pro 1.5", 44 | path: "gemini-1-5-pro", 45 | image: { 46 | light: "/models/gemini-icon.webp", 47 | dark: "/models/gemini-icon.webp", 48 | }, 49 | apiIdentifier: "gemini-1.5-pro", 50 | fileAccept: ["image/jpeg", "image/png", "image/webp", "application/pdf", "text/plain"], 51 | formats: "PDF, PNG, JPG, WEBP, TXT", 52 | }, 53 | ] as const; 54 | 55 | export const DEFAULT_MODEL_NAME: string = "gpt-4o"; 56 | -------------------------------------------------------------------------------- /ai/prompts.ts: -------------------------------------------------------------------------------- 1 | export const systemPrompt = "You are a friendly assistant! Keep your responses concise and helpful."; 2 | 3 | // export const systemPrompt = "do not respond on markdown or lists, keep your responses brief, you can ask the user to upload images or documents if it could help you understand the problem better"; -------------------------------------------------------------------------------- /app/(auth)/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { z } from "zod"; 4 | 5 | import { createUser, getUser } from "@/db/queries"; 6 | 7 | import { signIn } from "./auth"; 8 | 9 | const authFormSchema = z.object({ 10 | email: z.string().email(), 11 | password: z.string().min(6), 12 | }); 13 | 14 | export interface RegisterActionState { 15 | email: string; 16 | username: string; 17 | password: string; 18 | } 19 | 20 | export interface LoginActionState { 21 | status: "idle" | "in_progress" | "success" | "failed" | "invalid_data"; 22 | } 23 | 24 | export const login = async ( 25 | _: LoginActionState, 26 | formData: FormData 27 | ): Promise => { 28 | try { 29 | const validatedData = authFormSchema.parse({ 30 | email: formData.get("email"), 31 | password: formData.get("password"), 32 | }); 33 | 34 | await signIn("credentials", { 35 | email: validatedData.email, 36 | password: validatedData.password, 37 | redirect: false, 38 | }); 39 | 40 | return { status: "success" }; 41 | } catch (error) { 42 | if (error instanceof z.ZodError) { 43 | return { status: "invalid_data" }; 44 | } 45 | 46 | return { status: "failed" }; 47 | } 48 | }; 49 | 50 | export const register = async ({ 51 | email, 52 | username, 53 | password, 54 | }: RegisterActionState) => { 55 | try { 56 | if (password.length < 6) { 57 | return new Response( 58 | "password length should be more than 6 characters", 59 | { status: 400 } 60 | ); 61 | } 62 | 63 | // 查询用户是否存在 64 | let [user] = await getUser(email); 65 | if (user) { 66 | return new Response("User already exists", { status: 401 }); 67 | } else { 68 | await createUser(email, password, username); 69 | await signIn("credentials", { 70 | email, 71 | password, 72 | redirect: false, 73 | }); 74 | 75 | return Response.json({ email }, { status: 200 }); 76 | } 77 | } catch (error: any) { 78 | console.error(error); 79 | return Response.json({ message: "An error occurred" }, { status: 400 }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /app/(auth)/auth.ts: -------------------------------------------------------------------------------- 1 | import { compare } from "bcrypt-ts"; 2 | import NextAuth, { User, Session } from "next-auth"; 3 | import { DrizzleAdapter } from "@auth/drizzle-adapter"; 4 | import Credentials from "next-auth/providers/credentials"; 5 | import GithubProvider from "next-auth/providers/github"; 6 | import GoogleProvider from "next-auth/providers/google"; 7 | import ResendProvider from "next-auth/providers/resend"; 8 | 9 | import { getUser, getUserById, db } from "@/db/queries"; 10 | 11 | import { accounts, sessions, user, verificationTokens } from "@/db/schema"; 12 | import { generateUUID } from "@/lib/utils"; 13 | 14 | const SESSION_MAX_AGE = 30 * 24 * 60 * 60; 15 | const adapter = DrizzleAdapter(db, { 16 | usersTable: user, 17 | accountsTable: accounts, 18 | sessionsTable: sessions, 19 | verificationTokensTable: verificationTokens, 20 | }); 21 | const sessionOptions = { 22 | strategy: "database", 23 | maxAge: SESSION_MAX_AGE, 24 | updateAge: 7 * 24 * 60 * 60, // (seconds) 25 | }; 26 | 27 | function removeExtraUserInfo(user: any) { 28 | delete user.password; 29 | delete user.paid; 30 | delete user.emailVerified; 31 | delete user.updatedAt; 32 | } 33 | 34 | export const { 35 | handlers: { GET, POST }, 36 | auth, 37 | signIn, 38 | signOut, 39 | } = NextAuth({ 40 | pages: { 41 | signIn: "/login", 42 | newUser: "/", 43 | }, 44 | adapter, 45 | providers: [ 46 | GithubProvider({ 47 | clientId: process.env.GITHUB_ID || "", 48 | clientSecret: process.env.GITHUB_SECRET || "", 49 | }), 50 | GoogleProvider({ 51 | clientId: process.env.GOOGLE_ID || "", 52 | clientSecret: process.env.GOOGLE_SECRET || "", 53 | }), 54 | ResendProvider({ 55 | apiKey: process.env.RESEND_API_KEY, 56 | from: process.env.EMAIL_FROM, 57 | // sendVerificationRequest, 58 | }), 59 | Credentials({ 60 | credentials: { 61 | email: {}, 62 | password: {}, 63 | }, 64 | async authorize({ email, password }: any) { 65 | let users: any[] = await getUser(email); 66 | if (users.length === 0) { 67 | throw new Error("Invalid credentials."); 68 | } 69 | let passwordsMatch = await compare( 70 | password, 71 | users[0].password! 72 | ); 73 | if (passwordsMatch) { 74 | removeExtraUserInfo(users[0]); 75 | return users[0] as any; 76 | } 77 | }, 78 | }), 79 | ], 80 | callbacks: { 81 | authorized({ auth, request: { nextUrl } }) { 82 | let isLoggedIn = !!auth?.user; 83 | let isOnChat = nextUrl.pathname.startsWith("/chat"); 84 | 85 | if (isLoggedIn) { 86 | return Response.redirect( 87 | new URL("/", nextUrl as unknown as URL) 88 | ); 89 | } 90 | 91 | if (isOnChat) { 92 | if (isLoggedIn) return true; 93 | return false; // Redirect unauthenticated users to login page 94 | } 95 | 96 | if (isLoggedIn) { 97 | return Response.redirect( 98 | new URL("/", nextUrl as unknown as URL) 99 | ); 100 | } 101 | 102 | return true; 103 | }, 104 | async jwt({ token, user, account }) { 105 | if ( 106 | account?.provider !== "credentials" || 107 | sessionOptions.strategy === "jwt" 108 | ) 109 | return token; 110 | 111 | const session = await adapter.createSession!({ 112 | sessionToken: generateUUID(), 113 | userId: user.id as string, 114 | expires: new Date(Date.now() + sessionOptions.maxAge * 1000), 115 | }); 116 | 117 | token.sessionId = session.sessionToken; 118 | 119 | return token; 120 | }, 121 | async session({ 122 | session, 123 | token, 124 | }: { 125 | session: any; //ExtendedSession; 126 | token: any; 127 | }) { 128 | if (session && session.user) { 129 | removeExtraUserInfo(session.user); 130 | } 131 | 132 | return session; 133 | }, 134 | }, 135 | jwt: { 136 | async encode(params) { 137 | return params.token?.sessionId as string; //?? encode(params); 138 | }, 139 | }, 140 | }); 141 | -------------------------------------------------------------------------------- /app/(chat)/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { generateText, Message } from 'ai'; 4 | import to from "await-to-js"; 5 | import { cookies } from 'next/headers'; 6 | 7 | import { customModel } from '@/ai'; 8 | 9 | export async function saveModelId(model: string) { 10 | const cookieStore = await cookies(); 11 | cookieStore.set('model-id', model); 12 | } 13 | 14 | // 为对话生成简短标题 15 | export async function generateTitleFromUserMessage({ 16 | message, 17 | }: { 18 | message: Message | any; 19 | }) { 20 | const [error, result] = await to(generateText({ 21 | model: customModel('openai/gpt-4o-mini'), 22 | system: `\n 23 | - you will generate a short title based on the first message a user begins a conversation with 24 | - ensure it is not more than 80 characters long 25 | - the title should be a summary of the user's message 26 | - do not use quotes or colons`, 27 | prompt: JSON.stringify(message), 28 | })); 29 | 30 | if (error) { 31 | console.error("generateText error:", error.message) 32 | return "An error occurred while generate text" 33 | } 34 | 35 | return result.text; 36 | } 37 | -------------------------------------------------------------------------------- /app/[locale]/chat/[model]/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { models } from "@/ai/models"; 3 | import { getChatById, getMessagesByChatId } from "@/db/queries"; 4 | import { convertToUIMessages } from "@/lib/utils"; 5 | import { Chat } from "@/components/chat/chat-ui"; 6 | import { DataStreamHandler } from "@/components/data-stream-handler"; 7 | 8 | export default async function Page(props: { params: Promise }) { 9 | const params = await props.params; 10 | const { id, model: modelPath } = params; 11 | const chat = await getChatById({ id }).catch((err) => notFound()); 12 | 13 | const selectedModelId = models.find( 14 | (model) => model.path === modelPath 15 | )?.id; 16 | 17 | if (!selectedModelId) { 18 | notFound(); 19 | } 20 | 21 | const messagesFromDb = await getMessagesByChatId({ 22 | id, 23 | }); 24 | 25 | return ( 26 | <> 27 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/[locale]/chat/[model]/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { models } from "@/ai/models"; 3 | import { generateUUID } from "@/lib/utils"; 4 | import { getTranslations } from "next-intl/server"; 5 | import { notFound } from "next/navigation"; 6 | import { Chat } from "@/components/chat/chat-ui"; 7 | import { DataStreamHandler } from '@/components/data-stream-handler'; 8 | 9 | export async function generateMetadata({ params }: any) { 10 | const { locale, model: modelPath } = await params; 11 | const currentModel = models.find((model) => model.path === modelPath) || models[0]; 12 | const t = await getTranslations(currentModel.id); 13 | 14 | return { 15 | title: t("Metadata.title"), 16 | description: t("Metadata.description"), 17 | }; 18 | } 19 | 20 | export default async function Page(props: { params: Promise }) { 21 | const params = await props.params; 22 | const { model: modelPath } = params; 23 | const id = generateUUID(); 24 | 25 | const selectedModelId = 26 | models.find((model) => model.path === modelPath)?.id 27 | 28 | if (!selectedModelId) { 29 | notFound(); 30 | } 31 | 32 | return ( 33 | <> 34 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/[locale]/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | import { AppSidebar } from "@/components/app-sidebar"; 3 | import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; 4 | 5 | export const experimental_ppr = true; 6 | 7 | export default async function Layout({ 8 | children, 9 | params, 10 | }: { 11 | children: React.ReactNode; 12 | params: any; 13 | }) { 14 | const cookieStore = await cookies() 15 | const isCollapsed = cookieStore.get("sidebar:state")?.value !== "true"; 16 | 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { ReactNode } from "react"; 3 | import { NextIntlClientProvider } from "next-intl"; 4 | import { setRequestLocale, getMessages } from "next-intl/server"; 5 | import { notFound } from "next/navigation"; 6 | import { SessionProvider } from "next-auth/react"; 7 | import { routing } from "@/i18n/routing"; 8 | import { Toaster } from "sonner"; 9 | import "@/styles/globals.css"; 10 | import { ThemeProvider } from "@/components/theme-provider"; 11 | import { auth } from "../(auth)/auth"; 12 | import { Navbar } from "@/components/layout/navbar"; 13 | import FooterSection from "@/components/layout/footer"; 14 | import ModalProvider from "@/components/modals/providers"; 15 | import { fontGeist, fontHeading, fontSans, fontUrban, fontSatoshi } from "@/assets/fonts"; 16 | import { cn } from "@/lib/utils"; 17 | 18 | type Props = { 19 | children: ReactNode; 20 | params: { locale: string }; 21 | }; 22 | 23 | export const viewport = { 24 | maximumScale: 1, // Disable auto-zoom on mobile Safari 25 | }; 26 | 27 | const LIGHT_THEME_COLOR = "hsl(0 0% 100%)"; 28 | const DARK_THEME_COLOR = "hsl(240deg 10% 3.92%)"; 29 | const THEME_COLOR_SCRIPT = `\ 30 | (function() { 31 | var html = document.documentElement; 32 | var meta = document.querySelector('meta[name="theme-color"]'); 33 | if (!meta) { 34 | meta = document.createElement('meta'); 35 | meta.setAttribute('name', 'theme-color'); 36 | document.head.appendChild(meta); 37 | } 38 | function updateThemeColor() { 39 | var isDark = html.classList.contains('dark'); 40 | meta.setAttribute('content', isDark ? '${DARK_THEME_COLOR}' : '${LIGHT_THEME_COLOR}'); 41 | } 42 | var observer = new MutationObserver(updateThemeColor); 43 | observer.observe(html, { attributes: true, attributeFilter: ['class'] }); 44 | updateThemeColor(); 45 | })();`; 46 | 47 | export default async function RootLayout({ 48 | children, 49 | params, //: { locale }, 50 | }: Props) { 51 | const { locale } = await params; 52 | // Ensure that the incoming `locale` is valid 53 | if (!routing.locales.includes(locale as any)) { 54 | notFound(); 55 | } 56 | 57 | // Enable static rendering 58 | setRequestLocale(locale); 59 | 60 | const messages = await getMessages(); 61 | const session = await auth(); 62 | 63 | return ( 64 | 65 | 66 |