├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── ai.svg ├── chabot.png ├── lz-string.min.js ├── next.svg ├── readmeHero.png └── vercel.svg ├── src ├── app │ ├── api │ │ └── chat │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── hooks │ │ └── useEnsureRegeneratorRuntime.ts │ ├── layout.tsx │ └── page.tsx ├── components │ ├── chat.tsx │ ├── chat │ │ ├── bubble.tsx │ │ └── send-form.tsx │ ├── icons │ │ └── mic-icon.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts └── lib │ ├── mendable_stream.ts │ ├── strings.ts │ └── utils.ts ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | 38 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mendable Chatbot with Next.js (Starter Template) 2 | 3 | This is a sample project to demonstrate how to use Mendable API to build your own custom chatbot interface. Made with [Mendable](https://mendable.ai?ref=nextjs), [Next.js](https://vercel.com/solutions/nextjs), [Vercel AI SDK](https://vercel.com/blog/introducing-the-vercel-ai-sdk) and [shadcn/ui](https://ui.shadcn.com/). 4 | 5 | ![](/public/readmeHero.png) 6 | 7 | ## Getting Started 8 | 9 | First, create a Mendable account at [https://mendable.ai](https://mendable.ai), ingest your data and grab your API key. 10 | 11 | Add your .env file with your Mendable API key: 12 | 13 | ```bash 14 | MENDABLE_API_KEY=YOUR_MENDABLE_API_KEY 15 | ``` 16 | 17 | Then, install the dependencies: 18 | 19 | ```bash 20 | npm install 21 | ``` 22 | 23 | Run the development server: 24 | 25 | ```bash 26 | npm run dev 27 | ``` 28 | 29 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 30 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-ai", 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 | }, 11 | "browser": { 12 | "http": false, 13 | "https": false, 14 | "net": false, 15 | "path": false, 16 | "stream": false, 17 | "tls": false, 18 | "fs": false 19 | }, 20 | "dependencies": { 21 | "@microsoft/fetch-event-source": "^2.0.1", 22 | "@radix-ui/react-avatar": "^1.0.3", 23 | "@radix-ui/react-label": "^2.0.2", 24 | "@radix-ui/react-popover": "^1.0.6", 25 | "@radix-ui/react-scroll-area": "^1.0.4", 26 | "@radix-ui/react-slot": "^1.0.2", 27 | "@radix-ui/react-toast": "^1.1.5", 28 | "@types/node": "^20.11.17", 29 | "@types/react": "18.2.14", 30 | "@types/react-dom": "18.2.7", 31 | "@types/regenerator-runtime": "^0.13.5", 32 | "ai": "^2.1.19", 33 | "autoprefixer": "10.4.14", 34 | "class-variance-authority": "^0.6.1", 35 | "clsx": "^1.2.1", 36 | "eslint": "8.44.0", 37 | "eslint-config-next": "14.1.0", 38 | "event-source-polyfill": "^1.0.31", 39 | "eventsource-parser": "^1.0.0", 40 | "lucide-react": "^0.259.0", 41 | "next": "14.1.0", 42 | "openai-edge": "^1.2.0", 43 | "postcss": "8.4.25", 44 | "react": "18.2.0", 45 | "react-dom": "18.2.0", 46 | "react-icons": "^5.0.1", 47 | "react-loader-spinner": "^5.3.4", 48 | "react-speech-recognition": "^3.10.0", 49 | "react-use-clipboard": "^1.0.9", 50 | "regenerator-runtime": "^0.14.1", 51 | "tailwind-merge": "^1.13.2", 52 | "tailwindcss": "3.3.2", 53 | "tailwindcss-animate": "^1.0.6", 54 | "typescript": "5.1.6" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.23.9", 58 | "@babel/plugin-transform-runtime": "^7.23.9", 59 | "@types/eventsource": "^1.1.11", 60 | "@types/react-speech-recognition": "^3.9.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/ai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/chabot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/mendable-nextjs-chatbot/ba07b91fad2bb3bae5ce2d0b154c19dbda5d2240/public/chabot.png -------------------------------------------------------------------------------- /public/lz-string.min.js: -------------------------------------------------------------------------------- 1 | var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString}); -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/readmeHero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/mendable-nextjs-chatbot/ba07b91fad2bb3bae5ce2d0b154c19dbda5d2240/public/readmeHero.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, OpenAIApi } from "openai-edge"; 2 | import { StreamingTextResponse } from "ai"; 3 | import { MendableStream } from "@/lib/mendable_stream"; 4 | import { welcomeMessage } from "@/lib/strings"; 5 | 6 | export const runtime = "edge"; 7 | 8 | export async function POST(req: Request) { 9 | // Extract the `messages` from the body of the request 10 | const { messages } = await req.json(); 11 | 12 | // question is on the last message 13 | const question = messages[messages.length - 1].content; 14 | messages.pop(); 15 | 16 | const url = "https://api.mendable.ai/v0/newConversation"; 17 | 18 | const data = { 19 | api_key: process.env.MENDABLE_API_KEY, 20 | }; 21 | 22 | const r = await fetch(url, { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json", 26 | }, 27 | body: JSON.stringify(data), 28 | }); 29 | 30 | const conversation_id = await r.json(); 31 | 32 | 33 | const history = []; 34 | for (let i = 0; i < messages.length; i += 2) { 35 | history.push({ 36 | prompt: messages[i].content, 37 | response: messages[i + 1].content, 38 | }); 39 | } 40 | 41 | history.unshift({ 42 | prompt: "", 43 | response: welcomeMessage, 44 | }); 45 | 46 | const stream = await MendableStream({ 47 | api_key: process.env.MENDABLE_API_KEY, 48 | question: question, 49 | history: history, 50 | conversation_id: conversation_id.conversation_id, 51 | }); 52 | 53 | return new StreamingTextResponse(stream); 54 | } 55 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/mendable-nextjs-chatbot/ba07b91fad2bb3bae5ce2d0b154c19dbda5d2240/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 224 71.4% 4.1%; 9 | 10 | --muted: 220 14.3% 95.9%; 11 | --muted-foreground: 220 8.9% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 224 71.4% 4.1%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 224 71.4% 4.1%; 18 | 19 | --border: 220 13% 91%; 20 | --input: 220 13% 91%; 21 | 22 | --primary: 220.9 39.3% 11%; 23 | --primary-foreground: 210 20% 98%; 24 | 25 | --secondary: 220 14.3% 95.9%; 26 | --secondary-foreground: 220.9 39.3% 11%; 27 | 28 | --accent: 220 14.3% 95.9%; 29 | --accent-foreground: 220.9 39.3% 11%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 210 20% 98%; 33 | 34 | --ring: 217.9 10.6% 64.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 224 71.4% 4.1%; 41 | --foreground: 210 20% 98%; 42 | 43 | --muted: 215 27.9% 16.9%; 44 | --muted-foreground: 217.9 10.6% 64.9%; 45 | 46 | --popover: 224 71.4% 4.1%; 47 | --popover-foreground: 210 20% 98%; 48 | 49 | --card: 224 71.4% 4.1%; 50 | --card-foreground: 210 20% 98%; 51 | 52 | --border: 215 27.9% 16.9%; 53 | --input: 215 27.9% 16.9%; 54 | 55 | --primary: 210 20% 98%; 56 | --primary-foreground: 220.9 39.3% 11%; 57 | 58 | --secondary: 215 27.9% 16.9%; 59 | --secondary-foreground: 210 20% 98%; 60 | 61 | --accent: 215 27.9% 16.9%; 62 | --accent-foreground: 210 20% 98%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 85.7% 97.3%; 66 | 67 | --ring: 215 27.9% 16.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | 80 | .mendable-textarea::-webkit-scrollbar { 81 | width: 3px; 82 | } 83 | 84 | .mendable-textarea::-webkit-scrollbar-track { 85 | background: transparent; 86 | } 87 | 88 | .mendable-textarea::-webkit-scrollbar-thumb { 89 | background-color: black; 90 | } -------------------------------------------------------------------------------- /src/app/hooks/useEnsureRegeneratorRuntime.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import "regenerator-runtime/runtime"; 3 | 4 | export const useEnsureRegeneratorRuntime = () => { 5 | useEffect(() => {}, []); 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "@/components/ui/toaster" 2 | import type { Metadata } from 'next' 3 | import { Inter } from 'next/font/google' 4 | import './globals.css' 5 | 6 | const inter = Inter({ subsets: ['latin'] }) 7 | 8 | export const metadata: Metadata = { 9 | title: 'Create Next App', 10 | description: 'Generated by create next app', 11 | } 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 |