├── .env.example ├── .eslintrc.json ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── components.json ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── chatsage-chat-footer-logo.svg ├── logo.webp └── videos │ ├── how-it-works-add-data.mp4 │ └── how-it-works-embedding.mp4 ├── scripts └── widget.js ├── src ├── actions │ ├── find-sites.ts │ ├── logout.ts │ └── scrape.ts ├── app │ ├── (chat) │ │ └── chatbot-embedding │ │ │ └── [id] │ │ │ ├── actions.ts │ │ │ ├── components │ │ │ ├── button-scroll-to-bottom.tsx │ │ │ ├── chat-list.tsx │ │ │ ├── chat-message-actions.tsx │ │ │ ├── chat-message.tsx │ │ │ ├── chat-panel.tsx │ │ │ ├── chat.tsx │ │ │ ├── clear-history.tsx │ │ │ ├── client-wrapper.tsx │ │ │ ├── empty-screen.tsx │ │ │ ├── external-link.tsx │ │ │ ├── footer.tsx │ │ │ ├── header.tsx │ │ │ ├── localstorage-provider.tsx │ │ │ ├── markdown.tsx │ │ │ ├── prompt-form.tsx │ │ │ ├── providers.tsx │ │ │ ├── stocks │ │ │ │ ├── index.tsx │ │ │ │ ├── message.tsx │ │ │ │ └── spinner.tsx │ │ │ └── ui │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── codeblock.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ └── tooltip.tsx │ │ │ ├── error.tsx │ │ │ ├── lib │ │ │ ├── chat │ │ │ │ └── actions.tsx │ │ │ ├── hooks │ │ │ │ ├── use-copy-to-clipboard.tsx │ │ │ │ ├── use-enter-submit.tsx │ │ │ │ ├── use-local-storage.ts │ │ │ │ ├── use-scroll-anchor.tsx │ │ │ │ └── use-streamable-text.ts │ │ │ └── types.ts │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── api │ │ ├── embedding │ │ │ └── route.ts │ │ ├── get-chatbot-status │ │ │ └── [id] │ │ │ │ └── route.ts │ │ └── protected │ │ │ └── scrape │ │ │ └── route.ts │ ├── auth │ │ └── callback │ │ │ └── route.ts │ ├── create-project │ │ ├── layout.tsx │ │ └── project-name │ │ │ ├── _components │ │ │ ├── submit-button.tsx │ │ │ └── submit-form.tsx │ │ │ ├── actions.ts │ │ │ └── page.tsx │ ├── dashboard │ │ ├── _components │ │ │ ├── chat-log-section.tsx │ │ │ ├── layout-nav.tsx │ │ │ └── skeleton-loading.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ ├── page.tsx │ │ ├── settings │ │ │ ├── _components │ │ │ │ ├── ai-settings-button.tsx │ │ │ │ ├── ai-settings.tsx │ │ │ │ ├── logout-button.tsx │ │ │ │ ├── logout-form.tsx │ │ │ │ ├── project-connect.tsx │ │ │ │ ├── project-update-button.tsx │ │ │ │ ├── project-update-form.tsx │ │ │ │ ├── project-visibility-button.tsx │ │ │ │ └── project-visibility-form.tsx │ │ │ ├── actions.ts │ │ │ └── page.tsx │ │ └── sources │ │ │ ├── _components │ │ │ ├── columns.tsx │ │ │ ├── crawl-button.tsx │ │ │ ├── crawl-form.tsx │ │ │ ├── delete-button.tsx │ │ │ ├── fetched-sources-section.tsx │ │ │ ├── sitemap-button.tsx │ │ │ ├── sitemap-form.tsx │ │ │ ├── source-list-columns.tsx │ │ │ └── status-columns.tsx │ │ │ ├── actions.ts │ │ │ └── page.tsx │ ├── error │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ ├── _components │ │ │ ├── login-button.tsx │ │ │ └── login-form.tsx │ │ ├── actions.ts │ │ └── page.tsx │ ├── opengraph-image.png │ ├── page.tsx │ └── twitter-image.png ├── components │ ├── cloud-notification.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── hero.tsx │ ├── how-it-works.tsx │ ├── icons.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── data-table.tsx │ │ ├── input.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── table.tsx │ │ └── textarea.tsx ├── lib │ ├── check-url-number.ts │ ├── consts.ts │ ├── extract-text-from-html.ts │ ├── hooks │ │ └── use-chatbot-internal-id.ts │ ├── supabase │ │ ├── client.ts │ │ ├── middleware.ts │ │ ├── server.ts │ │ └── supabaseAdminClient.ts │ ├── types.ts │ └── utils.ts ├── middleware.ts └── types │ └── supabase.ts ├── supabase ├── .gitignore ├── config.toml ├── migrations │ ├── 20240730023518_remote_schema.sql │ └── 20240807013210_remote_schema.sql └── seed.sql ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # ex) https://yourapp.vercel.app 2 | NEXT_PUBLIC_APP_URL= 3 | 4 | # Supabase 5 | NEXT_PUBLIC_SUPABASE_URL= 6 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 7 | SUPABASE_SERVICE_TOKEN= 8 | SUPABASE_PROJECT_ID= 9 | 10 | # OpenAI 11 | OPENAI_API_KEY= 12 | 13 | # ScrapingFish 14 | SCRAPER_API_KEY= 15 | 16 | # Upstash 17 | UPSTASH_REDIS_REST_URL= 18 | UPSTASH_REDIS_REST_TOKEN= 19 | 20 | # Google Analytics (Optional) 21 | NEXT_PUBLIC_GA_ID= -------------------------------------------------------------------------------- /.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 | .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*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | x.com/taishik_. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 | 5 |

The open-source AI Chatbot for your website

6 | 7 |

Give us a star ⭐️

8 | 9 | # Chatsage 10 | 11 | ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/taishikato/chatsage/main) 12 | ![Vercel](https://vercelbadge.vercel.app/api/taishikato/chatsage) 13 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 14 | 15 | [Discord](https://discord.gg/reEuUQNYb3) 16 | 17 | [Chatsage](https://www.chatsage.co/) is an open source Chatbase alternative. 18 | 19 | ## About the project 20 | 21 | 22 | Demo gif image 27 | 28 | ### Built with 29 | 30 | * [Next.js](https://nextjs.org/) 31 | * [Supabase](https://supabase.com/) 32 | * [Upstash](https://upstash.com/) 33 | * [Tailwind CSS](https://tailwindcss.com/) 34 | * [ScrapingFish](https://scrapingfish.com/) 35 | 36 | ## Contact us (I mean...me) 37 | 38 | [@taishik_](https://x.com/taishik_) 39 | 40 | ## Cloud version 41 | 42 | https://www.chatsage.co 43 | 44 | ## Self hosting 45 | 46 | ### Prerequisites 47 | 48 | You need to create projects on each of the following platforms for `.env` file: 49 | * Supabase 50 | * Upstash 51 | * Vercel 52 | * ScrapingFish 53 | 54 | ### Setup 55 | 56 | 1. Clone the repo 57 | ```shell 58 | git clone https://github.com/taishikato/chatsage.git 59 | ``` 60 | 61 | 2. Go to the project folder 62 | ```shell 63 | cd chatsage 64 | ``` 65 | 66 | 3. Install packages with `pnpm` 67 | ```shell 68 | pnpm i 69 | ``` 70 | 71 | 4. Set up your `.env` file 72 | * Duplicate `.env.example` to `.env` 73 | * Set the values 74 | 75 | 5. Apply the migration to the remote database (Supabase) 76 | ```shell 77 | supabase db push 78 | ``` 79 | 80 | 6. Enable Google Sign-In on the Supabase Auth settings page. 81 | 82 | [Login with Google | Supabase Docs](https://supabase.com/docs/guides/auth/social-login/auth-google) 83 | 84 | ## Contributors 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatsage", 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 | "with-env": "dotenv -e .env -c -- ", 11 | "_genType": "npx supabase gen types typescript --project-id $SUPABASE_PROJECT_ID --schema public > src/types/supabase.ts", 12 | "genType": "npm run _with-env pnpm _genType" 13 | }, 14 | "dependencies": { 15 | "@ai-sdk/openai": "^0.0.39", 16 | "@headlessui/react": "^2.1.2", 17 | "@heroicons/react": "^2.1.5", 18 | "@langchain/community": "^0.2.19", 19 | "@langchain/openai": "^0.2.4", 20 | "@next/third-parties": "^14.2.5", 21 | "@radix-ui/react-alert-dialog": "^1.1.1", 22 | "@radix-ui/react-checkbox": "^1.1.1", 23 | "@radix-ui/react-dialog": "^1.1.1", 24 | "@radix-ui/react-dropdown-menu": "^2.1.1", 25 | "@radix-ui/react-icons": "^1.3.0", 26 | "@radix-ui/react-label": "^2.1.0", 27 | "@radix-ui/react-select": "^2.1.1", 28 | "@radix-ui/react-separator": "^1.1.0", 29 | "@radix-ui/react-slider": "^1.2.0", 30 | "@radix-ui/react-slot": "^1.1.0", 31 | "@radix-ui/react-switch": "^1.1.0", 32 | "@radix-ui/react-tooltip": "^1.1.2", 33 | "@supabase/ssr": "^0.4.0", 34 | "@supabase/supabase-js": "^2.44.4", 35 | "@tanstack/react-table": "^8.19.3", 36 | "@upstash/ratelimit": "^2.0.1", 37 | "@upstash/redis": "^1.33.0", 38 | "ai": "^3.2.32", 39 | "class-variance-authority": "^0.7.0", 40 | "clsx": "^2.1.1", 41 | "date-fns": "^3.6.0", 42 | "dotenv-cli": "^8.0.0", 43 | "html-entities": "^2.5.2", 44 | "langchain": "^0.2.10", 45 | "lucide-react": "^0.408.0", 46 | "nanoid": "^5.0.7", 47 | "next": "14.2.5", 48 | "next-themes": "^0.3.0", 49 | "node-html-parser": "^6.1.13", 50 | "react": "^18", 51 | "react-dom": "^18", 52 | "react-icons": "^5.2.1", 53 | "react-markdown": "^9.0.1", 54 | "react-syntax-highlighter": "^15.5.0", 55 | "react-textarea-autosize": "^8.5.3", 56 | "redaxios": "^0.5.1", 57 | "remark-gfm": "^4.0.0", 58 | "remark-math": "^6.0.0", 59 | "sitemapper": "^3.2.9", 60 | "sonner": "^1.5.0", 61 | "tailwind-merge": "^2.4.0", 62 | "tailwindcss-animate": "^1.0.7" 63 | }, 64 | "devDependencies": { 65 | "@types/node": "^20", 66 | "@types/react": "^18", 67 | "@types/react-dom": "^18", 68 | "@types/react-syntax-highlighter": "^15.5.13", 69 | "eslint": "^8", 70 | "eslint-config-next": "14.2.5", 71 | "postcss": "^8", 72 | "tailwindcss": "^3.4.1", 73 | "typescript": "^5" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/chatsage-chat-footer-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taishikato/chatsage/a4d5521f5e07a6900e41d8785cadfb962b7f5c6b/public/logo.webp -------------------------------------------------------------------------------- /public/videos/how-it-works-add-data.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taishikato/chatsage/a4d5521f5e07a6900e41d8785cadfb962b7f5c6b/public/videos/how-it-works-add-data.mp4 -------------------------------------------------------------------------------- /public/videos/how-it-works-embedding.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taishikato/chatsage/a4d5521f5e07a6900e41d8785cadfb962b7f5c6b/public/videos/how-it-works-embedding.mp4 -------------------------------------------------------------------------------- /src/actions/find-sites.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import Sitemapper from "sitemapper"; 4 | 5 | export const findSites = async (prevState: any, formData: FormData) => { 6 | const url = formData.get("url") as string; 7 | 8 | if (!url) { 9 | return { 10 | success: false, 11 | message: "URL is required.", 12 | sites: [], 13 | }; 14 | } 15 | 16 | const sitemapper = new Sitemapper({ 17 | url, 18 | timeout: 15000, 19 | }); 20 | 21 | const { sites } = await sitemapper.fetch(); 22 | 23 | return { 24 | sites, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/actions/logout.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { createClient } from "@/lib/supabase/server"; 4 | import { redirect } from "next/navigation"; 5 | 6 | export const logout = async () => { 7 | const supabase = createClient(); 8 | await supabase.auth.signOut(); 9 | 10 | return redirect("/login"); 11 | }; 12 | -------------------------------------------------------------------------------- /src/actions/scrape.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { type AxiosError } from "@/lib/types"; 4 | import { checkUrlNumber } from "@/lib/check-url-number"; 5 | import { APP_URL } from "@/lib/consts"; 6 | import { createClient } from "@/lib/supabase/server"; 7 | import { cookies } from "next/headers"; 8 | 9 | import axios from "redaxios"; 10 | 11 | export const scrape = async (url: string | null) => { 12 | if (!url) { 13 | return { 14 | success: false, 15 | error: "URL is required", 16 | }; 17 | } 18 | 19 | const supabase = createClient(); 20 | const { 21 | data: { user }, 22 | } = await supabase.auth.getUser(); 23 | 24 | const { data: project } = await supabase 25 | .from("chatbots") 26 | .select("internal_id") 27 | .match({ 28 | user_auth_id: user!.id, 29 | }); 30 | 31 | try { 32 | await checkUrlNumber(project![0].internal_id); 33 | 34 | const cookieStore = cookies(); 35 | await axios.post( 36 | `${APP_URL}/api/protected/scrape`, 37 | { 38 | url, 39 | chatbotInternalId: project![0].internal_id, 40 | }, 41 | { 42 | headers: { 43 | // i need this to pass supabase cookies for auth inside the scrape endpoint 44 | Cookie: cookieStore.toString(), 45 | }, 46 | } 47 | ); 48 | 49 | return { 50 | success: true, 51 | }; 52 | } catch (err) { 53 | console.error(err); 54 | 55 | const errorMessage = (err as any).data 56 | ? (err as AxiosError).data.message 57 | : (err as Error).message; 58 | 59 | return { 60 | success: false, 61 | message: errorMessage, 62 | }; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { redirect } from 'next/navigation' 4 | 5 | import { type Chat } from '@/lib/types' 6 | 7 | export async function getChats(userId?: string | null) { 8 | /** 9 | * instead of kv, i need to fetch data from dtabase or localStorage 10 | */ 11 | return [] 12 | 13 | // if (!userId) { 14 | // return [] 15 | // } 16 | 17 | // try { 18 | // const pipeline = kv.pipeline() 19 | // const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, { 20 | // rev: true 21 | // }) 22 | 23 | // for (const chat of chats) { 24 | // pipeline.hgetall(chat) 25 | // } 26 | 27 | // const results = await pipeline.exec() 28 | 29 | // return results as Chat[] 30 | // } catch (error) { 31 | // return [] 32 | // } 33 | } 34 | 35 | export async function getChat(id: string, userId: string) { 36 | /** 37 | * instead of kv, i need to fetch data from dtabase or localStorage 38 | */ 39 | return null 40 | 41 | // const chat = await kv.hgetall(`chat:${id}`) 42 | 43 | // if (!chat || (userId && chat.userId !== userId)) { 44 | // return null 45 | // } 46 | 47 | // return chat 48 | } 49 | 50 | export async function removeChat({ id, path }: { id: string; path: string }) { 51 | // const session = await auth() 52 | 53 | // if (!session) { 54 | // return { 55 | // error: 'Unauthorized' 56 | // } 57 | // } 58 | 59 | // //Convert uid to string for consistent comparison with session.user.id 60 | // const uid = String(await kv.hget(`chat:${id}`, 'userId')) 61 | 62 | // if (uid !== session?.user?.id) { 63 | // return { 64 | // error: 'Unauthorized' 65 | // } 66 | // } 67 | 68 | // await kv.del(`chat:${id}`) 69 | // await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`) 70 | 71 | // revalidatePath('/') 72 | // return revalidatePath(path) 73 | 74 | return 75 | } 76 | 77 | export async function clearChats() { 78 | /** 79 | * Instead of changing kv, i need to clear the localStorage or somewhere else. 80 | */ 81 | return redirect('/') 82 | 83 | // const session = await auth() 84 | // if (!session?.user?.id) { 85 | // return { 86 | // error: 'Unauthorized' 87 | // } 88 | // } 89 | // const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1) 90 | // if (!chats.length) { 91 | // return redirect('/') 92 | // } 93 | // const pipeline = kv.pipeline() 94 | // for (const chat of chats) { 95 | // pipeline.del(chat) 96 | // pipeline.zrem(`user:chat:${session.user.id}`, chat) 97 | // } 98 | // await pipeline.exec() 99 | // revalidatePath('/') 100 | // return redirect('/') 101 | } 102 | 103 | export async function saveChat(chat: Chat) { 104 | /** 105 | * Instead of kv, i need to store them on Supabase 106 | */ 107 | return 108 | // const session = await auth() 109 | // if (session && session.user) { 110 | // const pipeline = kv.pipeline() 111 | // pipeline.hmset(`chat:${chat.id}`, chat) 112 | // pipeline.zadd(`user:chat:${chat.userId}`, { 113 | // score: Date.now(), 114 | // member: `chat:${chat.id}` 115 | // }) 116 | // await pipeline.exec() 117 | // } else { 118 | // return 119 | // } 120 | } 121 | 122 | export async function refreshHistory(path: string) { 123 | redirect(path) 124 | } 125 | 126 | export async function getMissingKeys() { 127 | const keysRequired = ['OPENAI_API_KEY'] 128 | return keysRequired 129 | .map(key => (process.env[key] ? '' : key)) 130 | .filter(key => key !== '') 131 | } 132 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | import { Button, type ButtonProps } from "@/components/ui/button"; 7 | import { IconArrowDown } from "./ui/icons"; 8 | 9 | interface ButtonScrollToBottomProps extends ButtonProps { 10 | isAtBottom: boolean; 11 | scrollToBottom: () => void; 12 | } 13 | 14 | export function ButtonScrollToBottom({ 15 | className, 16 | isAtBottom, 17 | scrollToBottom, 18 | ...props 19 | }: ButtonScrollToBottomProps) { 20 | return ( 21 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { UIState } from "../lib/chat/actions"; 2 | 3 | export interface ChatList { 4 | messages: UIState; 5 | } 6 | 7 | export function ChatList({ messages }: ChatList) { 8 | if (!messages.length) { 9 | return null; 10 | } 11 | 12 | return ( 13 |
14 | {messages.map((message) => ( 15 |
{message.display}
16 | ))} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { type Message } from "ai"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { IconCheck, IconCopy } from "./ui/icons"; 7 | import { useCopyToClipboard } from "../lib/hooks/use-copy-to-clipboard"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<"div"> { 11 | message: Message; 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); 20 | 21 | const onCopy = () => { 22 | if (isCopied) return; 23 | copyToClipboard(message.content); 24 | }; 25 | 26 | return ( 27 |
34 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | // Inspired by Chatbot-UI and modified to fit the needs of this project 2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx 3 | 4 | import { Message } from "ai"; 5 | import remarkGfm from "remark-gfm"; 6 | import remarkMath from "remark-math"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { CodeBlock } from "./ui/codeblock"; 10 | import { MemoizedReactMarkdown } from "./markdown"; 11 | import { IconOpenAI, IconUser } from "./ui/icons"; 12 | import { ChatMessageActions } from "./chat-message-actions"; 13 | 14 | export interface ChatMessageProps { 15 | message: Message; 16 | } 17 | 18 | export function ChatMessage({ message, ...props }: ChatMessageProps) { 19 | return ( 20 |
24 |
32 | {message.role === "user" ? : } 33 |
34 |
35 | {children}

; 41 | }, 42 | // @ts-ignore 43 | code({ node, inline, className, children, ...props }) { 44 | // @ts-ignore 45 | if (children.length) { 46 | // @ts-ignore 47 | if (children[0] == "▍") { 48 | return ( 49 | 50 | ); 51 | } 52 | 53 | // @ts-ignore 54 | children[0] = (children[0] as string).replace("`▍`", "▍"); 55 | } 56 | 57 | const match = /language-(\w+)/.exec(className || ""); 58 | 59 | if (inline) { 60 | return ( 61 | 62 | {children} 63 | 64 | ); 65 | } 66 | 67 | return ( 68 | 74 | ); 75 | }, 76 | }} 77 | > 78 | {message.content} 79 |
80 | 81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { PromptForm } from "./prompt-form"; 4 | import { ButtonScrollToBottom } from "./button-scroll-to-bottom"; 5 | import { FooterText } from "./footer"; 6 | // import { useAIState, useActions, useUIState } from "ai/rsc"; 7 | // import type { AI } from "../lib/chat/actions"; 8 | // import { nanoid } from "nanoid"; 9 | // import { UserMessage } from "./stocks/message"; 10 | 11 | export type ChatPanelProps = { 12 | id?: string; 13 | title?: string; 14 | input: string; 15 | setInput: (value: string) => void; 16 | isAtBottom: boolean; 17 | scrollToBottom: () => void; 18 | chatbotId: string; 19 | temperature: number; 20 | }; 21 | 22 | export const ChatPanel = ({ 23 | id, 24 | title, 25 | input, 26 | setInput, 27 | isAtBottom, 28 | scrollToBottom, 29 | chatbotId, 30 | temperature, 31 | }: ChatPanelProps) => { 32 | // const [aiState] = useAIState(); 33 | // const [messages, setMessages] = useUIState(); 34 | // const { submitUserMessage } = useActions(); 35 | // const [shareDialogOpen, setShareDialogOpen] = React.useState(false); 36 | 37 | // const exampleMessages = [ 38 | // { 39 | // heading: "What are the", 40 | // subheading: "trending memecoins today?", 41 | // message: `What are the trending memecoins today?`, 42 | // }, 43 | // { 44 | // heading: "What is the price of", 45 | // subheading: "$DOGE right now?", 46 | // message: "What is the price of $DOGE right now?", 47 | // }, 48 | // { 49 | // heading: "I would like to buy", 50 | // subheading: "42 $DOGE", 51 | // message: `I would like to buy 42 $DOGE`, 52 | // }, 53 | // { 54 | // heading: "What are some", 55 | // subheading: `recent events about $DOGE?`, 56 | // message: `What are some recent events about $DOGE?`, 57 | // }, 58 | // ]; 59 | 60 | return ( 61 |
62 | 66 | 67 |
68 | {/*
69 | {messages.length === 0 && 70 | exampleMessages.map((example, index) => ( 71 |
1 && "hidden md:block" 75 | }`} 76 | onClick={async () => { 77 | setMessages((currentMessages) => [ 78 | ...currentMessages, 79 | { 80 | id: nanoid(), 81 | display: {example.message}, 82 | }, 83 | ]); 84 | 85 | const responseMessage = await submitUserMessage( 86 | example.message 87 | ); 88 | 89 | setMessages((currentMessages) => [ 90 | ...currentMessages, 91 | responseMessage, 92 | ]); 93 | }} 94 | > 95 |
{example.heading}
96 |
97 | {example.subheading} 98 |
99 |
100 | ))} 101 |
*/} 102 | 103 |
104 | 109 | 110 |
111 |
112 |
113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn, nanoid } from "@/lib/utils"; 4 | import { ChatList } from "./chat-list"; 5 | import { ChatPanel } from "./chat-panel"; 6 | import { EmptyScreen } from "./empty-screen"; 7 | import { useEffect, useState } from "react"; 8 | import { useUIState, useAIState, useActions } from "ai/rsc"; 9 | import { Message } from "@/lib/types"; 10 | import { useRouter } from "next/navigation"; 11 | import { useScrollAnchor } from "../lib/hooks/use-scroll-anchor"; 12 | import { toast } from "sonner"; 13 | import { BotMessage, UserMessage } from "./stocks/message"; 14 | import { AI } from "../lib/chat/actions"; 15 | import { Loader } from "lucide-react"; 16 | import { useLocalStorage } from "./localstorage-provider"; 17 | 18 | export type ChatProps = { 19 | initialMessages?: Message[]; 20 | id: string; 21 | missingKeys: string[]; 22 | chatbotId: string; 23 | temperature: number; 24 | } & React.ComponentProps<"div">; 25 | 26 | export const Chat = ({ 27 | id, 28 | className, 29 | missingKeys, 30 | chatbotId, 31 | temperature, 32 | }: ChatProps) => { 33 | const router = useRouter(); 34 | const [input, setInput] = useState(""); 35 | const [messages, setMessages] = useUIState(); 36 | const [aiState] = useAIState(); 37 | const { getChat } = useActions(); 38 | const { value: conversationId } = useLocalStorage(); 39 | 40 | const [loadingInitialChatHistory, setLoadingInitialChatHistory] = 41 | useState(true); 42 | 43 | useEffect(() => { 44 | const messagesLength = aiState.messages?.length; 45 | if (messagesLength === 2) { 46 | router.refresh(); 47 | } 48 | }, [aiState.messages, router]); 49 | 50 | useEffect(() => { 51 | scrollToBottom(); 52 | }, [messages]); 53 | 54 | useEffect(() => { 55 | missingKeys.map((key) => { 56 | toast.error(`Missing ${key} environment variable!`); 57 | }); 58 | }, [missingKeys]); 59 | 60 | const { scrollRef, visibilityRef, isAtBottom, scrollToBottom } = 61 | useScrollAnchor(); 62 | 63 | useEffect(() => { 64 | const fetchChat = async () => { 65 | setLoadingInitialChatHistory(true); 66 | 67 | try { 68 | const initialChat = await getChat(chatbotId, conversationId); 69 | 70 | if (!initialChat) return; 71 | 72 | const messagesNode = initialChat.map((chat: any) => { 73 | return { 74 | id: nanoid(), 75 | display: 76 | chat.role === "user" ? ( 77 | {chat.message} 78 | ) : ( 79 | 80 | ), 81 | }; 82 | }); 83 | 84 | setMessages(messagesNode); 85 | } catch (error) { 86 | console.error("Error fetching chat:", error); 87 | } finally { 88 | setLoadingInitialChatHistory(false); 89 | } 90 | }; 91 | 92 | fetchChat(); 93 | // eslint-disable-next-line react-hooks/exhaustive-deps 94 | }, [chatbotId, conversationId]); 95 | 96 | return ( 97 |
101 | {loadingInitialChatHistory ? ( 102 |
103 | 104 |
105 | ) : ( 106 | <> 107 |
108 | {messages.length ? ( 109 | 110 | ) : ( 111 | 112 | )} 113 |
114 |
115 | 124 | 125 | )} 126 |
127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/clear-history.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { toast } from "sonner"; 6 | 7 | import { ServerActionResult } from "@/lib/types"; 8 | import { Button } from "@/components/ui/button"; 9 | import { 10 | AlertDialog, 11 | AlertDialogAction, 12 | AlertDialogCancel, 13 | AlertDialogContent, 14 | AlertDialogDescription, 15 | AlertDialogFooter, 16 | AlertDialogHeader, 17 | AlertDialogTitle, 18 | AlertDialogTrigger, 19 | } from "./ui/alert-dialog"; 20 | import { IconSpinner } from "./ui/icons"; 21 | 22 | interface ClearHistoryProps { 23 | isEnabled: boolean; 24 | clearChats: () => ServerActionResult; 25 | } 26 | 27 | export function ClearHistory({ 28 | isEnabled = false, 29 | clearChats, 30 | }: ClearHistoryProps) { 31 | const [open, setOpen] = React.useState(false); 32 | const [isPending, startTransition] = React.useTransition(); 33 | const router = useRouter(); 34 | 35 | return ( 36 | 37 | 38 | 42 | 43 | 44 | 45 | Are you absolutely sure? 46 | 47 | This will permanently delete your chat history and remove your data 48 | from our servers. 49 | 50 | 51 | 52 | Cancel 53 | 57 | ) => { 58 | event.preventDefault(); 59 | startTransition(async () => { 60 | const result = await clearChats(); 61 | if (result && "error" in result) { 62 | toast.error(result.error); 63 | return; 64 | } 65 | 66 | setOpen(false); 67 | }); 68 | }} 69 | > 70 | {isPending && } 71 | Delete 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/client-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Fragment, type ReactNode } from "react"; 4 | import { useLocalStorage } from "./localstorage-provider"; 5 | 6 | export const ClientWrapper = ({ children }: { children: ReactNode }) => { 7 | const { value: conversationId } = useLocalStorage(); 8 | 9 | return {children}; 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | export const EmptyScreen = () => { 2 | return ( 3 |
4 |
5 |
6 | Hi! What can I help you with? 7 |
8 |
9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { APP_NAME, APP_URL } from "@/lib/consts"; 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<"p">) { 7 | return ( 8 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { RotateCw } from "lucide-react"; 5 | import { useLocalStorage } from "./localstorage-provider"; 6 | import { nanoid } from "@/lib/utils"; 7 | 8 | export const Header = ({ chatBotName }: { chatBotName: string | null }) => { 9 | const { setValue } = useLocalStorage(); 10 | 11 | return ( 12 |
13 |
14 |

15 | {chatBotName ?? "No name"} 16 |

17 | 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/localstorage-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useContext, useEffect, useState } from "react"; 4 | 5 | const conversationLocalStorageKeyPrefix = "sp_chatbodId_"; 6 | 7 | const LocalStorageContext = createContext<{ 8 | value: string; 9 | setValue: (value: string) => void; 10 | }>({ value: "", setValue: () => {} }); 11 | 12 | export const useLocalStorage = () => useContext(LocalStorageContext); 13 | 14 | export const LocalStorageProvider = ({ 15 | children, 16 | chatbotId, 17 | id, 18 | }: { 19 | children: React.ReactNode; 20 | chatbotId: string; 21 | id: string; 22 | }) => { 23 | const localStorageKeyForConversationId = `${conversationLocalStorageKeyPrefix}${chatbotId}`; 24 | const [conversationId, setConversationId] = useState(() => { 25 | if (typeof window !== "undefined") { 26 | const storedValue = localStorage.getItem( 27 | localStorageKeyForConversationId 28 | ); 29 | return storedValue ?? id; 30 | } 31 | 32 | return id; 33 | }); 34 | 35 | useEffect(() => { 36 | localStorage.setItem(localStorageKeyForConversationId, conversationId); 37 | }, [conversationId]); 38 | 39 | return ( 40 | 43 | {children} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && 8 | prevProps.className === nextProps.className 9 | ) 10 | -------------------------------------------------------------------------------- /src/app/(chat)/chatbot-embedding/[id]/components/prompt-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | 5 | import Textarea from "react-textarea-autosize"; 6 | 7 | import { useActions, useUIState } from "ai/rsc"; 8 | 9 | import { UserMessage } from "./stocks/message"; 10 | import { type AI } from "../lib/chat/actions"; 11 | import { Button } from "@/components/ui/button"; 12 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; 13 | import { useEnterSubmit } from "../lib/hooks/use-enter-submit"; 14 | import { nanoid } from "nanoid"; 15 | 16 | import { Send } from "lucide-react"; 17 | import { useLocalStorage } from "./localstorage-provider"; 18 | import { useParams } from "next/navigation"; 19 | 20 | export function PromptForm({ 21 | input, 22 | setInput, 23 | temperature, 24 | }: { 25 | input: string; 26 | setInput: (value: string) => void; 27 | temperature: number; 28 | }) { 29 | const { formRef, onKeyDown } = useEnterSubmit(); 30 | const inputRef = useRef(null); 31 | const { value: conversationId } = useLocalStorage(); 32 | const { submitUserMessage } = useActions(); 33 | const [_, setMessages] = useUIState(); 34 | const { id: chatbotId } = useParams(); 35 | 36 | useEffect(() => { 37 | if (inputRef.current) { 38 | inputRef.current.focus(); 39 | } 40 | }, []); 41 | 42 | return ( 43 |
{ 46 | e.preventDefault(); 47 | 48 | // Blur focus on mobile 49 | if (window.innerWidth < 600) { 50 | e.target["message"]?.blur(); 51 | } 52 | 53 | const value = input.trim(); 54 | setInput(""); 55 | if (!value) return; 56 | 57 | // Optimistically add user message UI 58 | setMessages((currentMessages) => [ 59 | ...currentMessages, 60 | { 61 | id: nanoid(), 62 | display: {value}, 63 | }, 64 | ]); 65 | 66 | // Submit and get response message 67 | const responseMessage = await submitUserMessage( 68 | value, 69 | chatbotId, 70 | conversationId, 71 | temperature 72 | ); 73 | 74 | setMessages((currentMessages) => [...currentMessages, responseMessage]); 75 | }} 76 | > 77 |
78 |