├── .devcontainer └── devcontainer.json ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── actions │ ├── bot.ts │ ├── file.ts │ └── stripe.ts ├── app │ ├── (auth) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── api │ │ ├── complete │ │ │ └── route.ts │ │ └── stripe │ │ │ └── session │ │ │ └── retrieve │ │ │ └── route.ts │ ├── checkout │ │ ├── page.tsx │ │ └── success │ │ │ └── page.tsx │ ├── dashboard │ │ ├── bots │ │ │ ├── [botId] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── ChatBox.tsx │ ├── Chatbot.tsx │ ├── ChatbotCard.tsx │ ├── CreateChatbotDialog.tsx │ ├── Heading.tsx │ ├── LoadingSpinner.tsx │ ├── Navbar.tsx │ ├── SiteNavbar.tsx │ ├── Toast.tsx │ ├── UploadFileInput.tsx │ └── ui │ │ ├── Dialog.tsx │ │ └── FormButton.tsx ├── lib │ ├── auth.ts │ ├── chatbotkit.ts │ ├── error.ts │ ├── stripe.ts │ └── utils.ts └── middleware.ts ├── tailwind.config.ts ├── tsconfig.json └── types.d.ts /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/javascript-node:1.0.7-18-bullseye", 3 | "features": { 4 | "git-lfs": "latest", 5 | "docker-in-docker": "latest" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # AUTH 2 | CLERK_SECRET_KEY= 3 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 4 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 5 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 6 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL= 7 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL= 8 | 9 | # CHATBOTKIT 10 | CHATBOTKIT_API_KEY= 11 | 12 | # STRIPE 13 | NEXT_PUBLIC_STRIPE_PUBLIC_KEY= 14 | STRIPE_SECRET_KEY= 15 | STRIPE_PRICE_ID= 16 | 17 | # GENERAL 18 | DOMAIN_URL= 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier" 6 | ], 7 | "rules": { 8 | "react/sort-prop-types": "error", 9 | "unused-imports/no-unused-imports": "error" 10 | }, 11 | "plugins": ["@typescript-eslint", "unused-imports"] 12 | } 13 | -------------------------------------------------------------------------------- /.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.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 3 | "semi": false, 4 | "trailingComma": "es5", 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "tabWidth": 2, 8 | "useTabs": false, 9 | "bracketSpacing": true, 10 | "bracketSameLine": false, 11 | "arrowParens": "always", 12 | "importOrder": [ 13 | ".css$", 14 | "^core-js-pure", 15 | "^jest", 16 | "^@/styles", 17 | "^dotenv", 18 | "^react", 19 | "^react-*", 20 | "^next/*", 21 | "^@/", 22 | "^:/", 23 | "^#/", 24 | "^[./]", 25 | "^@", 26 | "^." 27 | ], 28 | "importOrderSeparation": true, 29 | "importOrderSortSpecifiers": true 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "csstools.postcss", 6 | "bradlc.vscode-tailwindcss", 7 | "streetsidesoftware.code-spell-checker" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": false, 4 | "editor.tabSize": 2, 5 | "editor.rulers": [80], 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.formatOnSave": true, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | }, 11 | "terminal.integrated.scrollback": 100000, 12 | "githubPullRequests.remotes": ["origin"], 13 | "files.autoSave": "off", 14 | "files.associations": { 15 | "*.xml": "html", 16 | "*.svg": "html" 17 | }, 18 | "cSpell.words": [ 19 | "chatbotkit", 20 | "chatbots", 21 | "clsx", 22 | "heroicons", 23 | "nextjs", 24 | "skillset", 25 | "sonner", 26 | "whitelabel" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatBotKit White-label 2 | 3 | Welcome to the ChatBotKit White-label Solution! This powerful tool enables you to build and deploy custom conversational AI SaaS solutions quickly and efficiently. This is the ideal for entrepreneurs, agencies, and developers who want to build their own AI platforms. 4 | 5 | 6 | 7 | 8 | 9 | ## Benefits 10 | 11 | ### 🌟 **Tailored to Your Brand** 12 | 13 | **Personalize Seamlessly:** Our ChatBotKit White-label Solution boasts unparalleled customization. Transform its appearance to resonate with your brand's unique aesthetic and ethos. 14 | 15 | ### 🚀 **Scalability at Your Fingertips** 16 | 17 | **Grow Without Limits:** Built upon the robust ChatBotKit Node SDKs, our solution scales effortlessly with your business, ensuring you're always ahead of the curve. 18 | 19 | ### 🔒 **Fortified Security** 20 | 21 | **Protection You Can Trust:** Leveraging the Clerk authentication platform, the ChatBotKit White-label Solution guards your application against cyber threats, ensuring peace of mind. 22 | 23 | ### 💰 **Cost-Effective Monetization** 24 | 25 | **Profit with Ease:** Integrated with Stripe, our solution simplifies monetization, turning your application into a revenue-generating powerhouse. 26 | 27 | ### 🛠️ **Open-Source Innovation** 28 | 29 | **Freedom to Customize:** Being open-source, our solution offers unparalleled flexibility, allowing you to tailor the application to your unique business requirements. 30 | 31 | ### 🎯 **User-Friendly Design** 32 | 33 | **Effortless Experience:** With its intuitive interface, the ChatBotKit White-label Solution makes managing and customizing your application a breeze. 34 | 35 | ### 🚀 **Streamlined Deployment** 36 | 37 | **Deploy with Confidence:** Our solution simplifies the deployment process, enabling you to launch your application quickly and efficiently. 38 | 39 | ### 🛠️ **Hassle-Free Maintenance** 40 | 41 | **Maintain with Ease:** Designed for effortless upkeep, our solution ensures your application remains cutting-edge with minimal effort. 42 | 43 | ### 🔄 **Regular Updates, No Fuss** 44 | 45 | **Stay Ahead, Effortlessly:** Our easy-to-update system means your application always enjoys the latest features and improvements without any hassle. 46 | 47 | ### 🔗 **Seamless Integration** 48 | 49 | **Integrate Effortlessly:** Our solution's compatibility ensures smooth integration with your existing systems, enhancing functionality without the complexity. 50 | 51 | ## Prerequisites 52 | 53 | Before you begin, ensure you have the following installed: 54 | 55 | - A package manager (npm, yarn, pnpm, or bun) 56 | - A [ChatBotKit](https://chatbotkit.com) account 57 | - A [Clerk](https://clerk.dev/) account 58 | - A [Stripe](https://stripe.com/) account 59 | 60 | ## Getting Started 61 | 62 | ### Setting Up Your Development Environment 63 | 64 | 1. **Clone the Repository:** 65 | 66 | ```bash 67 | git clone https://github.com/chatbotkit/cbk-whitelabel 68 | cd cbk-whitelabel 69 | ``` 70 | 71 | 2. **Install Dependencies:** 72 | Choose your preferred package manager and run the following command: 73 | 74 | ```bash 75 | npm install 76 | # or 77 | yarn 78 | # or 79 | pnpm install 80 | # or 81 | bun install 82 | ``` 83 | 84 | 3. **Create a `.env.local` File:** 85 | Create a `.env.local` file in the root directory of your project. This file will contain your ChatBotKit, Clerk and Stripe credentials. 86 | 87 | ```bash 88 | cp .env.example .env.local 89 | ``` 90 | 91 | 4. **Run the Development Server:** 92 | 93 | ```bash 94 | npm run dev 95 | # or 96 | yarn dev 97 | # or 98 | pnpm dev 99 | # or 100 | bun dev 101 | ``` 102 | 103 | 5. **Access the App:** 104 | Open [http://localhost:3000](http://localhost:3000) in your browser to view the application. 105 | 106 | 6. **Begin Editing:** 107 | Start customizing your chatbot by editing `app/page.tsx`. Changes will auto-update as you modify the file. 108 | 109 | 7. **Keeping In Sync:** 110 | If you wish you can also keep the ChatBotKit SDK in sync with the latest version pulling the latest changes from the repository. 111 | 112 | ```bash 113 | # add the ChatBotKit repository as an upstream remote 114 | git remote add upstream https://github.com/chatbotkit/cbk-whitelabel.git 115 | # fetch the latest changes 116 | git fetch upstream 117 | # switch to the main branch 118 | git checkout main 119 | # merge the latest changes 120 | git merge upstream/main 121 | # install dependencies 122 | npm install 123 | ``` 124 | 125 | ## Documentation and Learning Resources 126 | 127 | - **Explore ChatBotKit:** Learn more about the capabilities and features of ChatBotKit at [ChatBotKit.com](https://chatbotkit.com). 128 | - **SDK Documentation:** Utilize the [ChatBotKit Node SDKs](https://github.com/chatbotkit/node-sdk) for advanced customization and functionality. 129 | 130 | ## Deploying Your Chatbot 131 | 132 | **Deploy with Vercel:** 133 | For a hassle-free deployment, use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js). It’s optimized for Next.js applications like this one. 134 | 135 | - **Deployment Guide:** For detailed instructions, see the [Next.js Deployment Documentation](https://nextjs.org/docs/deployment). 136 | 137 | ## Support and Contributions 138 | 139 | For support, feature requests, or contributions, please visit [our GitHub repository](https://github.com/chatbotkit/cbk-whitelabel). 140 | 141 | If you need help with ChatBotKit, please visit [ChatBotKit](https://chatbotkit.com) main website. 142 | 143 | **The ChatBotKit team may be able to provide development and commercial support for your project. Please contact us for more information.** 144 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'img.clerk.com', 8 | }, 9 | ], 10 | }, 11 | 12 | experimental: { 13 | serverActions: { 14 | allowedOrigins: ['localhost:3000'], 15 | }, 16 | }, 17 | } 18 | 19 | module.exports = nextConfig 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cbk-white-label", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "check": "tsc --noEmit", 11 | "format": "prettier -w ." 12 | }, 13 | "dependencies": { 14 | "@chatbotkit/next": "^1.9.0", 15 | "@chatbotkit/react": "^1.10.0", 16 | "@chatbotkit/sdk": "^1.10.0", 17 | "@clerk/nextjs": "^5.0.6", 18 | "@heroicons/react": "^2.0.18", 19 | "@radix-ui/react-dialog": "^1.0.5", 20 | "@radix-ui/react-select": "^2.0.0", 21 | "@radix-ui/react-slider": "^1.1.2", 22 | "@stripe/react-stripe-js": "^2.3.1", 23 | "@stripe/stripe-js": "^2.1.10", 24 | "class-variance-authority": "^0.7.0", 25 | "clsx": "^2.0.0", 26 | "next": "^14.2.3", 27 | "react": "^18", 28 | "react-dom": "^18", 29 | "react-dropzone": "^14.2.3", 30 | "sonner": "^1.0.3", 31 | "stripe": "^14.2.0", 32 | "tailwind-merge": "^1.14.0", 33 | "zod": "^3.23.6" 34 | }, 35 | "devDependencies": { 36 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 37 | "@types/node": "^20", 38 | "@types/react": "^18", 39 | "@types/react-dom": "^18", 40 | "@typescript-eslint/eslint-plugin": "^6.18.1", 41 | "autoprefixer": "^10", 42 | "eslint": "^8", 43 | "eslint-config-next": "14.0.0", 44 | "eslint-config-prettier": "^8.8.0", 45 | "eslint-plugin-unused-imports": "^3.0.0", 46 | "postcss": "^8", 47 | "prettier": "^3.1.0", 48 | "tailwindcss": "^3", 49 | "tailwindcss-animate": "^1.0.7", 50 | "typescript": "^5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/actions/bot.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath } from 'next/cache' 4 | import { redirect } from 'next/navigation' 5 | 6 | import { getChatBotKitUserClient } from '@/lib/auth' 7 | import { captureException } from '@/lib/error' 8 | 9 | import { BotListResponse } from '@chatbotkit/sdk/bot/v1' 10 | 11 | export async function listBots(): Promise { 12 | const cbk = await getChatBotKitUserClient() 13 | 14 | return await cbk.bot.list() 15 | } 16 | 17 | export async function createBot(formData: FormData) { 18 | const cbk = await getChatBotKitUserClient() 19 | 20 | const name = formData.get('name') 21 | const model = formData.get('model') 22 | const backstory = formData.get('backstory') 23 | 24 | let botId 25 | 26 | try { 27 | const bot = await cbk.bot.create({ 28 | name: name as string, 29 | model: model as string, 30 | backstory: backstory as string, 31 | }) 32 | 33 | botId = bot.id 34 | } catch (e) { 35 | await captureException(e) 36 | 37 | throw new Error(`Something went wrong. Please try again!`) 38 | } 39 | 40 | redirect(`/dashboard/bots/${botId}`) 41 | } 42 | 43 | export async function updateBot(formData: FormData, botId: string) { 44 | const cbk = await getChatBotKitUserClient() 45 | 46 | const backstory = formData.get('backstory') 47 | 48 | try { 49 | await cbk.bot.update(botId, { 50 | backstory: backstory as string, 51 | }) 52 | } catch (e) { 53 | await captureException(e) 54 | 55 | throw new Error(`Something went wrong. Please try again!`) 56 | } 57 | 58 | revalidatePath(`/dashboard/bots`) 59 | } 60 | 61 | export async function deleteBot(id: string) { 62 | const cbk = await getChatBotKitUserClient() 63 | 64 | try { 65 | const bot = await cbk.bot.fetch(id) 66 | 67 | await cbk.bot.delete(id) 68 | if (bot.datasetId) { 69 | await cbk.dataset.delete(bot.datasetId as string) 70 | } 71 | } catch (e) { 72 | await captureException(e) 73 | 74 | throw new Error(`Something went wrong. Please try again!`) 75 | } 76 | 77 | revalidatePath('/dashboard') 78 | } 79 | -------------------------------------------------------------------------------- /src/actions/file.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath } from 'next/cache' 4 | 5 | import { getChatBotKitUserClient } from '@/lib/auth' 6 | import { captureException } from '@/lib/error' 7 | 8 | export async function addFile(formData: FormData, botId: string) { 9 | const cbk = await getChatBotKitUserClient() 10 | 11 | const file = formData.get('file') as File 12 | const buffer = await file?.arrayBuffer() 13 | 14 | try { 15 | let dataset 16 | 17 | const bot = await cbk.bot.fetch(botId) 18 | 19 | if (!bot.datasetId) { 20 | dataset = await cbk.dataset.create({ 21 | name: file.name, 22 | store: 'ada-loom', 23 | }) 24 | await cbk.bot.update(botId, { 25 | datasetId: dataset.id, 26 | }) 27 | } else { 28 | dataset = await cbk.dataset.fetch(bot.datasetId) 29 | } 30 | 31 | // 1. Create a file 32 | const createdFile = await cbk.file.create({ 33 | name: file.name, 34 | }) 35 | 36 | // 2. Upload the specified file 37 | await cbk.file.upload(createdFile.id, { 38 | data: buffer, 39 | name: file.name, 40 | type: file.type, 41 | }) 42 | 43 | // 3. Attach file to the dataset 44 | await cbk.dataset.file.attach(dataset.id, createdFile.id, { 45 | type: 'source', 46 | }) 47 | 48 | // 4. Sync with the dataset 49 | await cbk.dataset.file.sync(dataset.id, createdFile.id, {}) 50 | } catch (e) { 51 | await captureException(e) 52 | 53 | return { 54 | error: { 55 | message: 'Something went wrong. Please try again!', 56 | }, 57 | } 58 | } 59 | 60 | revalidatePath('/dashboard/bots') 61 | } 62 | 63 | export async function deleteFile(id: string) { 64 | const cbk = await getChatBotKitUserClient() 65 | 66 | try { 67 | await cbk.file.delete(id) 68 | } catch (e) { 69 | await captureException(e) 70 | 71 | return { 72 | error: { 73 | message: 'Something went wrong. Please try again!', 74 | }, 75 | } 76 | } 77 | 78 | revalidatePath('/dashboard/bots') 79 | } 80 | -------------------------------------------------------------------------------- /src/actions/stripe.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import stripe from '@/lib/stripe' 4 | 5 | import { auth, clerkClient } from '@clerk/nextjs/server' 6 | 7 | import { z } from 'zod' 8 | 9 | const env = z 10 | .object({ 11 | STRIPE_PRICE_ID: z.string(), 12 | DOMAIN_URL: z.string(), 13 | }) 14 | .parse(process.env) 15 | 16 | export async function createCheckoutSession() { 17 | const { userId } = auth() 18 | 19 | const user = await clerkClient.users.getUser(userId as string) 20 | 21 | let stripeCustomer 22 | 23 | if (!user.privateMetadata.stripeCustomerId) { 24 | stripeCustomer = await stripe.customers.create({ 25 | name: user.firstName as string, 26 | email: user.emailAddresses[0].emailAddress, 27 | }) 28 | 29 | if (!stripeCustomer) { 30 | throw new Error(`Something went wrong. Please try again!`) 31 | } 32 | 33 | await clerkClient.users.updateUser(userId as string, { 34 | privateMetadata: { 35 | stripeCustomerId: stripeCustomer.id, 36 | }, 37 | }) 38 | } 39 | 40 | const session = await stripe.checkout.sessions.create({ 41 | customer: 42 | stripeCustomer?.id || (user?.privateMetadata.stripeCustomerId as string), 43 | ui_mode: 'embedded', 44 | line_items: [ 45 | { 46 | price: env.STRIPE_PRICE_ID, 47 | quantity: 1, 48 | }, 49 | ], 50 | mode: 'subscription', 51 | return_url: new URL( 52 | '/checkout/success?session_id={CHECKOUT_SESSION_ID}', 53 | env.DOMAIN_URL 54 | ).toString(), 55 | }) 56 | 57 | return session.client_secret as string 58 | } 59 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs' 2 | 3 | export default function SignInPage() { 4 | return ( 5 | <> 6 |
7 | 8 |
9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs' 2 | 3 | export default function SignUpPage() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/api/complete/route.ts: -------------------------------------------------------------------------------- 1 | import { getUserAuth } from '@/lib/auth' 2 | import { getUserClient } from '@/lib/chatbotkit' 3 | 4 | import { stream } from '@chatbotkit/next/edge' 5 | 6 | export async function POST(req: Request) { 7 | const { chatbotkitUserId } = await getUserAuth() 8 | 9 | if (!chatbotkitUserId) { 10 | return new Response('Unauthorized', { 11 | status: 401, 12 | }) 13 | } 14 | 15 | const { model, backstory, datasetId, messages } = await req.json() 16 | 17 | const cbk = getUserClient(chatbotkitUserId) 18 | 19 | const complete = cbk.conversation.complete(null, { 20 | model, 21 | backstory, 22 | datasetId, 23 | messages, 24 | }) 25 | 26 | return stream(complete) 27 | } 28 | 29 | export const runtime = 'edge' 30 | -------------------------------------------------------------------------------- /src/app/api/stripe/session/retrieve/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server' 2 | 3 | import chatbotkit from '@/lib/chatbotkit' 4 | import { captureException } from '@/lib/error' 5 | import stripe from '@/lib/stripe' 6 | 7 | import { auth, clerkClient } from '@clerk/nextjs/server' 8 | 9 | export async function GET(req: NextRequest) { 10 | try { 11 | const { userId } = auth() 12 | 13 | if (!userId) { 14 | return new Response('Unauthorized', { 15 | status: 401, 16 | }) 17 | } 18 | 19 | const user = await clerkClient.users.getUser(userId) 20 | 21 | if (!user) { 22 | return new Response('Unauthorized', { 23 | status: 401, 24 | }) 25 | } 26 | 27 | const sessionId = req.nextUrl.searchParams.get('sessionId') 28 | 29 | if (!sessionId) { 30 | return new Response('Missing session id', { 31 | status: 400, 32 | }) 33 | } 34 | 35 | const session = await stripe.checkout.sessions.retrieve(sessionId) 36 | 37 | if (!session) { 38 | return new Response('Session not found', { 39 | status: 404, 40 | }) 41 | } 42 | 43 | let chatbotkitUserId 44 | 45 | if (!user.privateMetadata.chatbotkitUserId) { 46 | const { id } = await chatbotkit.partner.user.create({ 47 | name: user?.firstName || '', 48 | }) 49 | 50 | chatbotkitUserId = id 51 | } else { 52 | chatbotkitUserId = user.privateMetadata.chatbotkitUserId as string 53 | } 54 | 55 | await clerkClient.users.updateUser(userId as string, { 56 | privateMetadata: { 57 | stripeCustomerId: user?.privateMetadata.stripeCustomerId, 58 | chatbotkitUserId, 59 | }, 60 | }) 61 | 62 | return Response.json({ 63 | status: session.status, 64 | }) 65 | } catch (e) { 66 | await captureException(e) 67 | 68 | return new Response('Something went wrong', { 69 | status: 500, 70 | }) 71 | } 72 | } 73 | 74 | export const runtime = 'edge' 75 | -------------------------------------------------------------------------------- /src/app/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | import { createCheckoutSession } from '@/actions/stripe' 6 | import SiteNavbar from '@/components/SiteNavbar' 7 | import { FormButton } from '@/components/ui/FormButton' 8 | 9 | import { 10 | EmbeddedCheckout, 11 | EmbeddedCheckoutProvider, 12 | } from '@stripe/react-stripe-js' 13 | import { loadStripe } from '@stripe/stripe-js' 14 | 15 | import { z } from 'zod' 16 | 17 | const env = z 18 | .object({ 19 | NEXT_PUBLIC_STRIPE_PUBLIC_KEY: z.string(), 20 | }) 21 | .parse({ 22 | NEXT_PUBLIC_STRIPE_PUBLIC_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!, 23 | }) 24 | 25 | const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!) 26 | 27 | export default function CheckoutPage() { 28 | const [clientSecret, setClientSecret] = useState('') 29 | 30 | return ( 31 | <> 32 | 33 |
34 |
35 |

36 | Simple, no-tricks pricing 37 |

38 |

39 | Trusted by some of the world's foremost brands to craft 40 | innovative AI solutions that benefit both their customers and 41 | employees. 42 |

43 | {!clientSecret && ( 44 |
{ 46 | const clientSecret = await createCheckoutSession() 47 | 48 | setClientSecret(clientSecret) 49 | }} 50 | > 51 |
52 |
53 |

Starter

54 |

55 | Best option for personal use & for your next project. 56 |

57 |

$29

58 | 59 | Subscribe 60 | 61 |
62 | {/* */} 63 |
64 |

Pro

65 |

Relevant for multiple users.

66 |

$69

67 | 68 | Subscribe 69 | 70 |
71 | {/* */} 72 |
73 |

Business

74 |

Best for large scale uses.

75 |

$99

76 | 77 | Subscribe 78 | 79 |
80 |
81 |
82 | )} 83 |
84 |
85 | {clientSecret && ( 86 | 90 | 91 | 92 | )} 93 |
94 |
95 | 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /src/app/checkout/success/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | 5 | import Link from 'next/link' 6 | import { useRouter } from 'next/navigation' 7 | 8 | import LoadingSpinner from '@/components/LoadingSpinner' 9 | 10 | export default function Page() { 11 | const router = useRouter() 12 | 13 | const [status, setStatus] = useState(null) 14 | 15 | useEffect(() => { 16 | const queryString = window.location.search 17 | const urlParams = new URLSearchParams(queryString) 18 | const sessionId = urlParams.get('session_id') 19 | 20 | if (sessionId === null) { 21 | router.push('/') 22 | } 23 | 24 | async function getSession() { 25 | const res = await fetch( 26 | `/api/stripe/session/retrieve?sessionId=${sessionId}` 27 | ) 28 | 29 | if (res.ok) { 30 | const data = await res.json() 31 | 32 | setStatus(data.status) 33 | } 34 | } 35 | 36 | getSession() 37 | }, [router]) 38 | 39 | if (status === 'open') { 40 | return router.push('/pricing') 41 | } 42 | 43 | if (status === 'complete') { 44 | return ( 45 |
46 |

Thank you fot subscribing!

47 | 48 | Go to dashboard 49 | 50 |
51 | ) 52 | } 53 | 54 | return ( 55 |
56 | 57 |

58 | We are setting up your account.
Please, don't refresh this 59 | page 60 |

61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/app/dashboard/bots/[botId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingSpinner from '@/components/LoadingSpinner' 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |
7 |
8 |
9 |

Chatbots

10 |

Find all you chatbots...

11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/app/dashboard/bots/[botId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | import Chatbot from '@/components/Chatbot' 4 | import { getChatBotKitUserClient } from '@/lib/auth' 5 | 6 | async function getChatbot(id: string) { 7 | const cbk = await getChatBotKitUserClient() 8 | 9 | let bot 10 | let files 11 | 12 | try { 13 | bot = await cbk.bot.fetch(id) 14 | } catch (error) { 15 | redirect('/dashboard') 16 | } 17 | 18 | try { 19 | files = await cbk.dataset.file.list(bot.datasetId as string, {}) 20 | } catch (error) { 21 | files = [] 22 | } 23 | 24 | return { bot, files: files.items } 25 | } 26 | 27 | export default async function Page({ params }: { params: { botId: string } }) { 28 | const { bot, files } = await getChatbot(params.botId) 29 | 30 | return ( 31 |
32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/app/dashboard/bots/loading.tsx: -------------------------------------------------------------------------------- 1 | import Heading from '@/components/Heading' 2 | import LoadingSpinner from '@/components/LoadingSpinner' 3 | 4 | export default function Loading() { 5 | return ( 6 |
7 | 11 |
12 |

Your chatbots

13 |
14 | 15 |
16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/app/dashboard/bots/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default function BotsPage() { 4 | return redirect('/dashboard') 5 | } 6 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | 3 | import Navbar from '@/components/Navbar' 4 | import Toast from '@/components/Toast' 5 | 6 | export const metadata: Metadata = { 7 | title: 'CBK Whitelabel', 8 | } 9 | 10 | export default function DashboardLayout({ 11 | children, 12 | }: { 13 | children: React.ReactNode 14 | }) { 15 | return ( 16 | <> 17 | 18 | 19 |
{children}
20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/app/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingSpinner from '@/components/LoadingSpinner' 2 | 3 | export default function DashboardLoadingPage() { 4 | return ( 5 |
6 |
7 |
8 |
9 |

Chatbots

10 |

11 | Find all you chatbots and create new ones... 12 |

13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { listBots } from '@/actions/bot' 4 | import ChatbotCard from '@/components/ChatbotCard' 5 | import CreateChatbotDialog from '@/components/CreateChatbotDialog' 6 | import Heading from '@/components/Heading' 7 | 8 | export default async function DashboardPage() { 9 | const bots = await listBots() 10 | 11 | return ( 12 |
13 | 17 | 18 | 19 |
20 |

Recently created

21 |
22 | {bots.items.map((item) => ( 23 | 24 | 25 | 26 | ))} 27 |
28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chatbotkit/cbk-whitelabel/357a552501c144fc1424bb3eb55bbe92a271c377/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .container { 7 | @apply max-w-7xl mx-auto px-6; 8 | } 9 | 10 | /* Button Styles */ 11 | .button { 12 | @apply inline-flex items-center justify-center rounded-lg text-xs font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 whitespace-nowrap h-10 px-4 py-2 bg-zinc-950 text-zinc-50 hover:bg-zinc-800/90 border border-zinc-200; 13 | } 14 | .button-outline { 15 | @apply bg-white border text-black border-zinc-200 shadow hover:bg-zinc-100 hover:text-zinc-900; 16 | } 17 | .button-sm { 18 | @apply h-9 rounded-lg px-3 text-xs; 19 | } 20 | .button-lg { 21 | @apply h-10 rounded-lg px-6; 22 | } 23 | .button-icon { 24 | @apply h-10 w-10; 25 | } 26 | 27 | /* Input Styles */ 28 | .input { 29 | @apply flex h-10 w-full rounded-lg border bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-400 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 border-zinc-200 shadow transition duration-150; 30 | } 31 | 32 | /* Textarea Styles */ 33 | .textarea { 34 | @apply flex w-full rounded-lg border bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-400 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 border-zinc-200 shadow transition duration-150; 35 | } 36 | 37 | /* Card */ 38 | .card { 39 | @apply rounded-xl border bg-white text-zinc-900 shadow-md overflow-hidden flex flex-col justify-start; 40 | } 41 | .card-header { 42 | @apply flex flex-col space-y-2 p-6; 43 | } 44 | .card-title { 45 | @apply text-lg font-medium leading-none tracking-tight; 46 | } 47 | .card-description { 48 | @apply text-sm text-zinc-500; 49 | } 50 | .card-content { 51 | @apply p-6 pt-0; 52 | } 53 | .card-footer { 54 | @apply flex items-center border-t border-zinc-200 px-6 py-4 bg-zinc-50 mt-auto; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | 3 | import type { Metadata } from 'next' 4 | import { Inter } from 'next/font/google' 5 | 6 | import { ClerkProvider } from '@clerk/nextjs' 7 | 8 | const inter = Inter({ subsets: ['latin'] }) 9 | 10 | export const metadata: Metadata = { 11 | title: 'ChatBotKit Whitelabel', 12 | description: 'Open-source white-label solution on top of ChatBotKit', 13 | } 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode 19 | }) { 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import SiteNavbar from '@/components/SiteNavbar' 2 | 3 | export default async function HomePage() { 4 | return ( 5 | <> 6 | 7 |
8 |

ChatBotKit Whitelabel

9 |

Open-source white-label solution on top of ChatBotKit

10 |

Extend and Customize

11 |
12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ChatBox.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useRef, useState } from 'react' 4 | 5 | import Image from 'next/image' 6 | import { useParams } from 'next/navigation' 7 | 8 | import { updateBot } from '@/actions/bot' 9 | import { FormButton } from '@/components/ui/FormButton' 10 | 11 | import { useConversationManager } from '@chatbotkit/react' 12 | import { useUser } from '@clerk/nextjs' 13 | import { PaperAirplaneIcon } from '@heroicons/react/20/solid' 14 | import { SparklesIcon } from '@heroicons/react/24/solid' 15 | 16 | import { toast } from 'sonner' 17 | 18 | export default function ChatBox({ 19 | datasetId, 20 | backstory, 21 | botName, 22 | model, 23 | }: { 24 | datasetId: string 25 | backstory: string 26 | botName: string 27 | model: string 28 | }) { 29 | const params: { botId: string } = useParams() 30 | 31 | const messagesEndRef = useRef(null) 32 | 33 | const { user } = useUser() 34 | 35 | const [updatedBackstory, setUpdatedBackstory] = useState(backstory) 36 | 37 | const { thinking, text, setText, messages, submit } = useConversationManager({ 38 | endpoint: '/api/complete', 39 | backstory, 40 | datasetId, 41 | model: { 42 | name: model, 43 | config: { 44 | temperature: 0.7, 45 | }, 46 | }, 47 | }) 48 | 49 | useEffect(() => { 50 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) 51 | }, [messages]) 52 | 53 | return ( 54 |
55 |
{ 57 | e.preventDefault() 58 | submit() 59 | }} 60 | className="border bg-white border-zinc-200 rounded-xl shadow-md col-span-2" 61 | > 62 |
63 | {messages.map(({ id, type, text }) => { 64 | switch (type) { 65 | case 'user': 66 | return ( 67 |
71 |
72 |
73 | profile picture 80 |
81 |

You

82 |
83 | 84 |

85 | {text} 86 |

87 |
88 | ) 89 | 90 | case 'bot': 91 | return ( 92 |
96 |
97 |
98 | 99 |
100 |

{botName}

101 |
102 | 103 |

104 | {text} 105 |

106 |
107 | ) 108 | } 109 | })} 110 | {thinking ? ( 111 |
115 |
116 |
117 | 118 |
119 |

{botName}

120 |
121 | 122 |

123 | Thinking... 124 |

125 |
126 | ) : null} 127 |
128 |
129 |
130 | setText(e.target.value)} 135 | /> 136 | 143 |
144 | 145 |
{ 147 | try { 148 | await updateBot(formData, params.botId) 149 | } catch (e) { 150 | const error = e as Error 151 | 152 | toast.error(error.message) 153 | } 154 | }} 155 | className="w-full" 156 | > 157 |

Backstory

158 |

159 | Adjust your model backstory 160 |

161 | 170 | 175 | Update Backstory 176 | 177 |
178 |
179 | ) 180 | } 181 | -------------------------------------------------------------------------------- /src/components/Chatbot.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | import ChatBox from '@/components/ChatBox' 6 | import UploadFileInput from '@/components/UploadFileInput' 7 | 8 | import { BotInstance } from '@chatbotkit/sdk/bot/v1' 9 | import { FileInstance } from '@chatbotkit/sdk/file/v1' 10 | 11 | const tabs = ['playground', 'sources'] 12 | 13 | export default function Chatbot({ 14 | bot, 15 | files, 16 | }: { 17 | bot: BotInstance 18 | files: FileInstance[] 19 | }) { 20 | const [currentTab, setCurrentTab] = useState(tabs[0]) 21 | 22 | return ( 23 | <> 24 |
25 |
26 |
27 |

{bot.name}

28 |

29 | {bot.backstory} 30 |

31 |
32 |
33 |
34 | {tabs.map((item) => ( 35 |
36 | 46 | {currentTab === item && ( 47 |
48 | )} 49 |
50 | ))} 51 |
52 |
53 |
54 | {currentTab === 'playground' && ( 55 | <> 56 |

Playground

57 | 63 | 64 | )} 65 | {currentTab === 'sources' && ( 66 | <> 67 |

Sources

68 | 69 | 70 | )} 71 |
72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/components/ChatbotCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { deleteBot } from '@/actions/bot' 4 | import LoadingSpinner from '@/components/LoadingSpinner' 5 | import { FormButton } from '@/components/ui/FormButton' 6 | 7 | import { TrashIcon } from '@heroicons/react/24/outline' 8 | import { ChatBubbleBottomCenterIcon } from '@heroicons/react/24/solid' 9 | 10 | import { toast } from 'sonner' 11 | 12 | type BotType = { 13 | id: string 14 | name?: string 15 | backstory?: string 16 | model?: string 17 | } 18 | 19 | export default function ChatbotCard({ bot }: { bot: BotType }) { 20 | return ( 21 |
22 |
23 |
24 | 25 |
26 |

{bot.name}

27 |

{bot.backstory}

28 |
29 |
30 |
31 |

Model:

32 |

33 | {bot.model} 34 |

35 |
36 |
{ 38 | try { 39 | await deleteBot(bot.id) 40 | 41 | toast.success('Chatbot successfully deleted!') 42 | } catch (e) { 43 | const error = e as Error 44 | 45 | toast.error(error.message) 46 | } 47 | }} 48 | > 49 | { 51 | e.stopPropagation() 52 | }} 53 | pendingState={} 54 | className="h-8 w-8 button-icon button-outline flex justify-center items-center" 55 | > 56 | 57 | 58 |
59 |
60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/components/CreateChatbotDialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | 5 | import { createBot } from '@/actions/bot' 6 | import { 7 | Dialog, 8 | DialogClose, 9 | DialogContent, 10 | DialogHeader, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from '@/components/ui/Dialog' 14 | import { FormButton } from '@/components/ui/FormButton' 15 | 16 | import { toast } from 'sonner' 17 | 18 | export default function CreateChatbotDialog() { 19 | const [open, setOpen] = useState(false) 20 | 21 | const [botState, setBotState] = useState({ 22 | name: '', 23 | backstory: '', 24 | model: 'gpt-3.5-turbo', 25 | }) 26 | 27 | return ( 28 | 29 | Create Chatbot 30 | 31 | 32 | Create a chatbot 33 | 34 |
{ 36 | try { 37 | await createBot(formData) 38 | } catch (e) { 39 | const error = e as Error 40 | 41 | toast.error(error.message) 42 | } 43 | }} 44 | className="flex flex-col space-y-4" 45 | > 46 |
47 | 50 | 56 | setBotState({ ...botState, name: e.target.value }) 57 | } 58 | /> 59 |
60 |
61 | 64 | 76 |
77 | 78 |
79 |
80 | 86 |