├── .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 |
3 |
4 |
5 | The open-source AI Chatbot for your website
6 |
7 | Give us a star ⭐️
8 |
9 | # Chatsage
10 |
11 | 
12 | 
13 | [](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 |
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 | scrollToBottom()}
30 | {...props}
31 | >
32 |
33 | Scroll to bottom
34 |
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 |
35 | {isCopied ? : }
36 | Copy message
37 |
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 |
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 |
39 | {isPending && }
40 | Clear history
41 |
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 |
22 |
26 |
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 |
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 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import { ThemeProviderProps } from "next-themes/dist/types";
5 | import { TooltipProvider } from "./ui/tooltip";
6 |
7 | export function Providers({ children, ...props }: ThemeProviderProps) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/stocks/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export { spinner } from './spinner'
4 | export { BotMessage } from './message'
5 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/stocks/message.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // import { IconOpenAI } from "../ui/icons";
4 | import { cn } from "@/lib/utils";
5 | import { CodeBlock } from "../ui/codeblock";
6 | import { MemoizedReactMarkdown } from "../markdown";
7 | import remarkGfm from "remark-gfm";
8 | import remarkMath from "remark-math";
9 | import { StreamableValue } from "ai/rsc";
10 | import { useStreamableText } from "../../lib/hooks/use-streamable-text";
11 | import { Loader } from "lucide-react";
12 |
13 | // Different types of message bubbles.
14 |
15 | export const UserMessage = ({ children }: { children: React.ReactNode }) => {
16 | return (
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export const BotMessage = ({
28 | content,
29 | className,
30 | }: {
31 | content: string | StreamableValue;
32 | className?: string;
33 | }) => {
34 | const text = useStreamableText(content);
35 |
36 | return (
37 |
38 | {/*
39 |
40 |
*/}
41 |
42 | {children};
48 | },
49 | // @ts-ignore
50 | code({ node, inline, className, children, ...props }) {
51 | // @ts-ignore
52 | if (children.length) {
53 | // @ts-ignore
54 | if (children[0] == "▍") {
55 | return (
56 | ▍
57 | );
58 | }
59 |
60 | // @ts-ignore
61 | children[0] = (children[0] as string).replace("`▍`", "▍");
62 | }
63 |
64 | const match = /language-(\w+)/.exec(className || "");
65 |
66 | if (inline) {
67 | return (
68 |
69 | {children}
70 |
71 | );
72 | }
73 |
74 | return (
75 |
81 | );
82 | },
83 | }}
84 | >
85 | {text}
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | export function SpinnerMessage() {
93 | return (
94 |
95 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/stocks/spinner.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export const spinner = (
4 |
14 |
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
16 | outline:
17 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
20 | ghost: 'hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 hover:underline'
22 | },
23 | size: {
24 | default: 'h-9 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3 text-xs',
26 | lg: 'h-10 rounded-md px-8',
27 | icon: 'size-9'
28 | }
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default'
33 | }
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button'
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = 'Button'
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/codeblock.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/Markdown/CodeBlock.tsx
3 |
4 | "use client";
5 |
6 | import { FC, memo } from "react";
7 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
8 | import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
9 |
10 | import { useCopyToClipboard } from "../../lib/hooks/use-copy-to-clipboard";
11 | import { IconCheck, IconCopy, IconDownload } from "./icons";
12 | import { Button } from "@/components/ui/button";
13 |
14 | interface Props {
15 | language: string;
16 | value: string;
17 | }
18 |
19 | interface languageMap {
20 | [key: string]: string | undefined;
21 | }
22 |
23 | export const programmingLanguages: languageMap = {
24 | javascript: ".js",
25 | python: ".py",
26 | java: ".java",
27 | c: ".c",
28 | cpp: ".cpp",
29 | "c++": ".cpp",
30 | "c#": ".cs",
31 | ruby: ".rb",
32 | php: ".php",
33 | swift: ".swift",
34 | "objective-c": ".m",
35 | kotlin: ".kt",
36 | typescript: ".ts",
37 | go: ".go",
38 | perl: ".pl",
39 | rust: ".rs",
40 | scala: ".scala",
41 | haskell: ".hs",
42 | lua: ".lua",
43 | shell: ".sh",
44 | sql: ".sql",
45 | html: ".html",
46 | css: ".css",
47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48 | };
49 |
50 | export const generateRandomString = (length: number, lowercase = false) => {
51 | const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
52 | let result = "";
53 | for (let i = 0; i < length; i++) {
54 | result += chars.charAt(Math.floor(Math.random() * chars.length));
55 | }
56 | return lowercase ? result.toLowerCase() : result;
57 | };
58 |
59 | const CodeBlock: FC = memo(({ language, value }) => {
60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
61 |
62 | const downloadAsFile = () => {
63 | if (typeof window === "undefined") {
64 | return;
65 | }
66 | const fileExtension = programmingLanguages[language] || ".file";
67 | const suggestedFileName = `file-${generateRandomString(
68 | 3,
69 | true
70 | )}${fileExtension}`;
71 | const fileName = window.prompt("Enter file name" || "", suggestedFileName);
72 |
73 | if (!fileName) {
74 | // User pressed cancel on prompt.
75 | return;
76 | }
77 |
78 | const blob = new Blob([value], { type: "text/plain" });
79 | const url = URL.createObjectURL(blob);
80 | const link = document.createElement("a");
81 | link.download = fileName;
82 | link.href = url;
83 | link.style.display = "none";
84 | document.body.appendChild(link);
85 | link.click();
86 | document.body.removeChild(link);
87 | URL.revokeObjectURL(url);
88 | };
89 |
90 | const onCopy = () => {
91 | if (isCopied) return;
92 | copyToClipboard(value);
93 | };
94 |
95 | return (
96 |
97 |
98 |
{language}
99 |
100 |
106 |
107 | Download
108 |
109 |
115 | {isCopied ? : }
116 | Copy code
117 |
118 |
119 |
120 |
141 | {value}
142 |
143 |
144 | );
145 | });
146 | CodeBlock.displayName = "CodeBlock";
147 |
148 | export { CodeBlock };
149 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { useEffect } from "react";
5 |
6 | export default function Error({
7 | error,
8 | reset,
9 | }: {
10 | error: Error & { digest?: string };
11 | reset: () => void;
12 | }) {
13 | useEffect(() => {
14 | // Log the error to an error reporting service
15 | console.error(error);
16 | }, [error]);
17 |
18 | return (
19 |
20 |
21 | Something went wrong!
22 |
23 | reset()}>Try again
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | export interface useCopyToClipboardProps {
6 | timeout?: number
7 | }
8 |
9 | export function useCopyToClipboard({
10 | timeout = 2000
11 | }: useCopyToClipboardProps) {
12 | const [isCopied, setIsCopied] = React.useState(false)
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
16 | return
17 | }
18 |
19 | if (!value) {
20 | return
21 | }
22 |
23 | navigator.clipboard.writeText(value).then(() => {
24 | setIsCopied(true)
25 |
26 | setTimeout(() => {
27 | setIsCopied(false)
28 | }, timeout)
29 | })
30 | }
31 |
32 | return { isCopied, copyToClipboard }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/lib/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject } from 'react'
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject
5 | onKeyDown: (event: React.KeyboardEvent) => void
6 | } {
7 | const formRef = useRef(null)
8 |
9 | const handleKeyDown = (
10 | event: React.KeyboardEvent
11 | ): void => {
12 | if (
13 | event.key === 'Enter' &&
14 | !event.shiftKey &&
15 | !event.nativeEvent.isComposing
16 | ) {
17 | formRef.current?.requestSubmit()
18 | event.preventDefault()
19 | }
20 | }
21 |
22 | return { formRef, onKeyDown: handleKeyDown }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/lib/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useLocalStorage = (
4 | key: string,
5 | initialValue: T
6 | ): [T, (value: T) => void] => {
7 | const [storedValue, setStoredValue] = useState(initialValue)
8 |
9 | useEffect(() => {
10 | // Retrieve from localStorage
11 | const item = window.localStorage.getItem(key)
12 | if (item) {
13 | setStoredValue(JSON.parse(item))
14 | }
15 | }, [key])
16 |
17 | const setValue = (value: T) => {
18 | // Save state
19 | setStoredValue(value)
20 | // Save to localStorage
21 | window.localStorage.setItem(key, JSON.stringify(value))
22 | }
23 | return [storedValue, setValue]
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/lib/hooks/use-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from "react";
2 |
3 | export const useScrollAnchor = () => {
4 | const messagesRef = useRef(null);
5 | const scrollRef = useRef(null);
6 | const visibilityRef = useRef(null);
7 |
8 | const [isAtBottom, setIsAtBottom] = useState(true);
9 | const [isVisible, setIsVisible] = useState(false);
10 |
11 | const scrollToBottom = useCallback(() => {
12 | if (scrollRef.current && visibilityRef.current) {
13 | scrollRef.current.scrollTop = visibilityRef.current.offsetTop;
14 | }
15 | }, []);
16 |
17 | useEffect(() => {
18 | if (scrollRef.current && visibilityRef.current) {
19 | if (isAtBottom && !isVisible) {
20 | scrollRef.current.scrollTop = visibilityRef.current.offsetTop;
21 | }
22 | }
23 | }, [isAtBottom, isVisible]);
24 |
25 | useEffect(() => {
26 | const { current } = scrollRef;
27 |
28 | if (current) {
29 | const handleScroll = (event: Event) => {
30 | const target = event.target as HTMLDivElement;
31 | const offset = 25;
32 | const isAtBottom =
33 | target.scrollTop + target.clientHeight >=
34 | target.scrollHeight - offset;
35 |
36 | setIsAtBottom(isAtBottom);
37 | };
38 |
39 | current.addEventListener("scroll", handleScroll, {
40 | passive: true,
41 | });
42 |
43 | return () => {
44 | current.removeEventListener("scroll", handleScroll);
45 | };
46 | }
47 | }, []);
48 |
49 | useEffect(() => {
50 | if (visibilityRef.current) {
51 | let observer = new IntersectionObserver(
52 | (entries) => {
53 | entries.forEach((entry) => {
54 | if (entry.isIntersecting) {
55 | setIsVisible(true);
56 | } else {
57 | setIsVisible(false);
58 | }
59 | });
60 | },
61 | {
62 | rootMargin: "0px 0px -70px 0px",
63 | }
64 | );
65 |
66 | observer.observe(visibilityRef.current);
67 |
68 | return () => {
69 | observer.disconnect();
70 | };
71 | }
72 | });
73 |
74 | return {
75 | messagesRef,
76 | scrollRef,
77 | visibilityRef,
78 | scrollToBottom,
79 | isAtBottom,
80 | isVisible,
81 | };
82 | };
83 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/lib/hooks/use-streamable-text.ts:
--------------------------------------------------------------------------------
1 | import { StreamableValue, readStreamableValue } from 'ai/rsc'
2 | import { useEffect, useState } from 'react'
3 |
4 | export const useStreamableText = (
5 | content: string | StreamableValue
6 | ) => {
7 | const [rawContent, setRawContent] = useState(
8 | typeof content === 'string' ? content : ''
9 | )
10 |
11 | useEffect(() => {
12 | ;(async () => {
13 | if (typeof content === 'object') {
14 | let value = ''
15 | for await (const delta of readStreamableValue(content)) {
16 | if (typeof delta === 'string') {
17 | setRawContent((value = value + delta))
18 | }
19 | }
20 | }
21 | })()
22 | }, [content])
23 |
24 | return rawContent
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { CoreMessage } from 'ai'
2 |
3 | export type Message = CoreMessage & {
4 | id: string
5 | }
6 |
7 | export interface Chat extends Record {
8 | id: string
9 | title: string
10 | createdAt: Date
11 | userId: string
12 | path: string
13 | messages: Message[]
14 | sharePath?: string
15 | }
16 |
17 | export type ServerActionResult = Promise<
18 | | Result
19 | | {
20 | error: string
21 | }
22 | >
23 |
24 | export interface Session {
25 | user: {
26 | id: string
27 | email: string
28 | }
29 | }
30 |
31 | export interface AuthResult {
32 | type: string
33 | message: string
34 | }
35 |
36 | export interface User extends Record {
37 | id: string
38 | email: string
39 | password: string
40 | salt: string
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from "lucide-react";
2 |
3 | export default async function Loading() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/(chat)/chatbot-embedding/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { nanoid } from "@/lib/utils";
2 | import { Chat } from "./components/chat";
3 | import { AI } from "./lib/chat/actions";
4 | import { getMissingKeys } from "./actions";
5 | import { Header } from "./components/header";
6 | import { Providers } from "./components/providers";
7 | import { LocalStorageProvider } from "./components/localstorage-provider";
8 | import { ClientWrapper } from "./components/client-wrapper";
9 | import { createAdminClient } from "@/lib/supabase/supabaseAdminClient";
10 | import type { Metadata } from "next";
11 |
12 | export const revalidate = 0;
13 |
14 | export async function generateMetadata({
15 | params,
16 | }: {
17 | params: { id: string };
18 | }): Promise {
19 | const supbabase = createAdminClient();
20 |
21 | const chatBotInternalId = params.id;
22 |
23 | const { data: chatBotData, error } = await supbabase
24 | .from("chatbots")
25 | .select("name, is_public")
26 | .match({ internal_id: chatBotInternalId })
27 | .single();
28 |
29 | return {
30 | title: chatBotData ? chatBotData.name : "No name",
31 | };
32 | }
33 |
34 | export default async function ChatbotPage({
35 | params,
36 | }: {
37 | params: { id: string };
38 | }) {
39 | const id = nanoid();
40 | const missingKeys = await getMissingKeys();
41 |
42 | const supbabase = createAdminClient();
43 |
44 | const chatBotInternalId = params.id;
45 |
46 | const { data: chatBotData, error } = await supbabase
47 | .from("chatbots")
48 | .select("name, is_public, temperature")
49 | .match({ internal_id: chatBotInternalId })
50 | .single();
51 |
52 | if (!chatBotData || !chatBotData?.is_public)
53 | return (
54 |
55 | This chatbot is unavailable.
56 |
57 | );
58 |
59 | return (
60 | <>
61 |
67 |
68 |
69 |
70 |
80 |
81 |
82 |
83 | >
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/api/embedding/route.ts:
--------------------------------------------------------------------------------
1 | import { APP_URL } from "@/lib/consts";
2 | import { type NextRequest } from "next/server";
3 |
4 | export const runtime = "edge";
5 |
6 | export async function GET(req: NextRequest) {
7 | const searchParams = req.nextUrl.searchParams;
8 | const chatbotId = searchParams.get("chatbotId");
9 |
10 | return new Response(
11 | `fetch(\`${APP_URL}/api/get-chatbot-status/${chatbotId}\`,{method:"GET",mode:"cors",cache:"no-store"}).then(_=>{if(!_.ok)throw Error("Network response was not ok");return _.json()}).then(_=>{"public"===_.visibility&&function _(){let $=document.createElement("iframe");$.style.position="fixed",$.style.bottom="80px",$.style.right="20px",$.style.width="460px",$.style.height="80dvh",$.style.border="none",$.style.borderRadius="10px",$.style.boxShadow="0 0 10px rgba(0,0,0,0.1)",$.style.zIndex="9999",$.style.backgroundColor="#ffffff";let t=document.createElement("button");t.style.position="fixed",t.style.bottom="20px",t.style.right="20px",t.style.width="50px",t.style.height="50px",t.style.padding="0",t.style.backgroundColor="#3B82F6",t.style.border="none",t.style.borderRadius="50%",t.style.cursor="pointer",t.style.zIndex="10000",t.style.display="flex",t.style.justifyContent="center",t.style.alignItems="center",t.addEventListener("mouseenter",()=>{t.style.transform="scale(1.1)",t.style.transitionProperty="transform",t.style.transitionDuration="150ms",t.style.transitionTimingFunction="cubic-bezier(0.4, 0, 0.2, 1)"}),t.addEventListener("mouseleave",()=>{t.style.transform="scale(1)"}),t.innerHTML=\`
12 |
13 |
14 |
15 |
16 | \`;let e=!1;t.addEventListener("click",function _(){"none"===$.style.display?(e||($.src=\`${APP_URL}/chatbot-embedding/${chatbotId}\`,e=!0),$.style.display="block",t.innerHTML=\`
17 |
18 |
19 | \`):($.style.display="none",t.style.backgroundColor="#3B82F6",t.innerHTML=\`
20 |
21 |
22 |
23 |
24 | \`)}),$.style.display="none",document.body.appendChild($),document.body.appendChild(t)}()}).catch(_=>{console.error("Error fetching chatbot status:",_)});
25 | `,
26 | {
27 | headers: {
28 | "Access-Control-Allow-Origin": "*",
29 | "Access-Control-Allow-Methods": "GET",
30 | "Access-Control-Allow-Headers": "Content-Type, Authorization",
31 | },
32 | }
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/api/get-chatbot-status/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { createAdminClient } from "@/lib/supabase/supabaseAdminClient";
2 |
3 | export const revalidate = 0;
4 |
5 | export async function GET(
6 | req: Request,
7 | { params }: { params: { id: string } }
8 | ) {
9 | const chatbotId = params.id;
10 | const supabaseAdmin = createAdminClient();
11 |
12 | const headers = {
13 | "Access-Control-Allow-Origin": "*",
14 | "Access-Control-Allow-Methods": "GET, OPTIONS",
15 | "Access-Control-Allow-Headers": "Content-Type, Authorization",
16 | };
17 |
18 | try {
19 | const { data, error } = await supabaseAdmin
20 | .from("chatbots")
21 | .select("is_public")
22 | .match({ internal_id: chatbotId })
23 | .single();
24 |
25 | if (error) {
26 | throw new Error(error.message);
27 | }
28 |
29 | if (!data)
30 | return Response.json(
31 | {
32 | success: true,
33 | },
34 | {
35 | headers,
36 | status: 404,
37 | }
38 | );
39 |
40 | if (!data.is_public)
41 | return Response.json(
42 | {
43 | success: true,
44 | visibility: "private",
45 | },
46 | {
47 | headers,
48 | status: 403,
49 | }
50 | );
51 |
52 | return Response.json({ success: true, visibility: "public" }, { headers });
53 | } catch (err) {
54 | const errorMessage = (err as Error).message;
55 |
56 | return Response.json(
57 | {
58 | success: false,
59 | message: errorMessage,
60 | },
61 | {
62 | headers,
63 | status: 500,
64 | }
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/api/protected/scrape/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import axios from "redaxios";
3 | import { extractTextFromHtml } from "@/lib/extract-text-from-html";
4 | import { parse } from "node-html-parser";
5 | import { OpenAIEmbeddings } from "@langchain/openai";
6 | import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase";
7 | import { createAdminClient } from "@/lib/supabase/supabaseAdminClient";
8 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
9 |
10 | export async function POST(req: Request): Promise {
11 | const supabaseAdmin = createAdminClient();
12 |
13 | const jsonReq = await req.json();
14 |
15 | const url = jsonReq.url;
16 | const chatbotId = jsonReq.chatbotInternalId as string;
17 |
18 | const payload = {
19 | api_key: process.env.SCRAPER_API_KEY,
20 | url,
21 | render_js: "true",
22 | };
23 | try {
24 | const { data } = await axios.get("https://scraping.narf.ai/api/v1/", {
25 | params: payload,
26 | });
27 |
28 | const parsed = parse(data);
29 |
30 | const extractedText = extractTextFromHtml(parsed.toString());
31 |
32 | const splitter = new RecursiveCharacterTextSplitter({
33 | chunkSize: 1500,
34 | chunkOverlap: 100,
35 | });
36 |
37 | const docs = await splitter.createDocuments(
38 | [extractedText],
39 | [
40 | {
41 | chatbot_internal_id: chatbotId,
42 | url,
43 | },
44 | ]
45 | );
46 |
47 | const store = new SupabaseVectorStore(new OpenAIEmbeddings(), {
48 | client: supabaseAdmin,
49 | tableName: "vectors",
50 | });
51 |
52 | await store.addDocuments(docs);
53 |
54 | // save the url on Supabase
55 | const { error } = await supabaseAdmin.from("urls").insert({
56 | url,
57 | chatbot_internal_id: chatbotId,
58 | });
59 |
60 | return NextResponse.json({
61 | success: true,
62 | });
63 | } catch (err) {
64 | console.error(err);
65 | return NextResponse.json(
66 | {
67 | success: false,
68 | },
69 | {
70 | status: 500,
71 | }
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { createClient } from "@/lib/supabase/server";
3 |
4 | export async function GET(request: Request) {
5 | // The `/auth/callback` route is required for the server-side auth flow implemented
6 | // by the Auth Helpers package. It exchanges an auth code for the user's session.
7 | // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
8 | const requestUrl = new URL(request.url);
9 | const code = requestUrl.searchParams.get("code");
10 |
11 | const supabase = createClient();
12 |
13 | if (code) {
14 | await supabase.auth.exchangeCodeForSession(code);
15 | }
16 |
17 | const {
18 | data: { user },
19 | } = await supabase.auth.getUser();
20 |
21 | if (!user) return NextResponse.redirect(`${requestUrl.origin}/login`);
22 |
23 | await supabase.from("users").insert({
24 | email: user.email,
25 | });
26 |
27 | return NextResponse.redirect(`${requestUrl.origin}/dashboard`);
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/create-project/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function CreateProjectLayout({
2 | children,
3 | }: Readonly<{
4 | children: React.ReactNode;
5 | }>) {
6 | return (
7 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/create-project/project-name/_components/submit-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader, MoveRight } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const SubmitButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | Next
13 | {pending ? (
14 |
15 | ) : (
16 |
17 | )}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/app/create-project/project-name/_components/submit-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | CardContent,
5 | CardDescription,
6 | CardFooter,
7 | CardHeader,
8 | CardTitle,
9 | } from "@/components/ui/card";
10 | import { Input } from "@/components/ui/input";
11 | import { SubmitButton } from "./submit-button";
12 | import { saveProjectName } from "../actions";
13 |
14 | export const SubmitForm = () => {
15 | return (
16 |
17 |
18 | Chatbot Name
19 | Used to identify your chatbot.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/app/create-project/project-name/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { createClient } from "@/lib/supabase/server";
4 | import { redirect } from "next/navigation";
5 |
6 | export const saveProjectName = async (formData: FormData) => {
7 | const name = formData.get("project-name") as string;
8 |
9 | const supabase = createClient();
10 | const {
11 | data: { user },
12 | } = await supabase.auth.getUser();
13 |
14 | if (!user) return;
15 |
16 | const { count } = await supabase
17 | .from("chatbots")
18 | .select("id", { count: "exact" })
19 | .match({ user_auth_id: user.id });
20 |
21 | if ((count as number) > 0) redirect("/dashboard/sources");
22 |
23 | await supabase.from("chatbots").insert({
24 | name,
25 | user_auth_id: user.id,
26 | });
27 |
28 | redirect("/dashboard/sources");
29 | };
30 |
--------------------------------------------------------------------------------
/src/app/create-project/project-name/page.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@/components/ui/card";
2 | import { SubmitForm } from "./_components/submit-form";
3 |
4 | export default function CreateProjectPage() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/dashboard/_components/chat-log-section.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { Database, Tables } from "@/types/supabase";
4 | import { Fragment } from "react";
5 | import { createClient } from "@/lib/supabase/client";
6 | import { useEffect, useState } from "react";
7 | import { SkeletonLoading } from "./skeleton-loading";
8 | import { useChatbotInternalId } from "@/lib/hooks/use-chatbot-internal-id";
9 | import { formatDistanceToNow } from "date-fns";
10 | import { cn } from "@/lib/utils";
11 | import { BotMessage } from "@/app/(chat)/chatbot-embedding/[id]/components/stocks";
12 | import { UserMessage } from "@/app/(chat)/chatbot-embedding/[id]/components/stocks/message";
13 |
14 | type GetChatLogsFunctionReturnType = Omit<
15 | Database["public"]["Functions"]["get_chat_logs_by_chatbot"]["Returns"],
16 | "messages"
17 | > & { messages: Tables<"chat_logs">[] };
18 | [];
19 |
20 | export const ChatLogSection = () => {
21 | const supabase = createClient();
22 | const chatbotInternalId = useChatbotInternalId();
23 | const [conversations, setConversations] = useState<
24 | GetChatLogsFunctionReturnType | []
25 | >([]);
26 |
27 | const [selectedConversationId, setSelectedConversationId] = useState<
28 | GetChatLogsFunctionReturnType[number]["conversation_id"] | null
29 | >(null);
30 | const [selectedConversationMessages, setSelectedConversationMessages] =
31 | useState<
32 | {
33 | id: string;
34 | internal_id: string;
35 | message: string;
36 | role: string;
37 | created_at: string;
38 | }[]
39 | >([]);
40 | const [loading, setLoading] = useState(false);
41 |
42 | useEffect(() => {
43 | const fetchChatLogs = async (chatbotInternalId: string) => {
44 | setLoading(true);
45 |
46 | const { data, error } = await supabase.rpc("get_chat_logs_by_chatbot", {
47 | chatbot_id: chatbotInternalId,
48 | });
49 |
50 | if (error) {
51 | console.error("Error fetching chat logs:", error);
52 | return null;
53 | }
54 |
55 | setConversations(data as GetChatLogsFunctionReturnType);
56 | setSelectedConversationId(
57 | data.length > 0 ? data[0].conversation_id : null
58 | );
59 |
60 | setLoading(false);
61 | };
62 |
63 | if (chatbotInternalId) fetchChatLogs(chatbotInternalId);
64 | }, [chatbotInternalId]);
65 |
66 | useEffect(() => {
67 | const selectedConversation = conversations.find(
68 | (conversation) => conversation.conversation_id === selectedConversationId
69 | );
70 | setSelectedConversationMessages(
71 | selectedConversation
72 | ? (selectedConversation.messages as {
73 | id: string;
74 | internal_id: string;
75 | message: string;
76 | role: string;
77 | created_at: string;
78 | }[])
79 | : []
80 | );
81 | }, [selectedConversationId]);
82 |
83 | if (loading) return ;
84 |
85 | if (conversations.length === 0)
86 | return (
87 | No chats yet
88 | );
89 |
90 | return (
91 | <>
92 |
93 |
94 | {conversations.map((conversation) => {
95 | const messages = conversation.messages as {
96 | id: string;
97 | internal_id: string;
98 | message: string;
99 | role: string;
100 | created_at: string;
101 | }[];
102 |
103 | const userMessage =
104 | messages[messages.length - 2]?.message ||
105 | "No second latest message";
106 |
107 | const messageCreated = formatDistanceToNow(
108 | new Date(messages[messages.length - 2].created_at),
109 | { addSuffix: true }
110 | );
111 |
112 | const lastMessage =
113 | messages[messages.length - 1]?.message ||
114 | "No second latest message";
115 |
116 | return (
117 |
120 | setSelectedConversationId(conversation.conversation_id)
121 | }
122 | className={cn(
123 | "w-full min-w-full p-4 space-y-2 text-sm hover:cursor-pointer hover:bg-secondary",
124 | conversation.conversation_id === selectedConversationId
125 | ? "bg-secondary"
126 | : null
127 | )}
128 | >
129 |
130 |
{userMessage}
131 |
135 | {messageCreated}
136 |
137 |
138 |
139 | Bot: {lastMessage}
140 |
141 |
142 | );
143 | })}
144 |
145 |
146 |
147 | {selectedConversationMessages?.map((messageData) => {
148 | return (
149 |
150 | {messageData.role === "user" ? (
151 | {messageData.message}
152 | ) : (
153 |
154 | )}
155 |
156 | );
157 | })}
158 |
159 |
160 | >
161 | );
162 | };
163 |
--------------------------------------------------------------------------------
/src/app/dashboard/_components/layout-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 |
7 | const links = [
8 | {
9 | name: "Chat logs",
10 | path: "/dashboard",
11 | },
12 | {
13 | name: "Sources",
14 | path: "/dashboard/sources",
15 | },
16 | {
17 | name: "Settings",
18 | path: "/dashboard/settings",
19 | },
20 | ];
21 |
22 | export const LayoutNav = () => {
23 | const path = usePathname();
24 |
25 | return (
26 |
27 | {links.map((link) => {
28 | return (
29 |
36 | {link.name}
37 |
38 | );
39 | })}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/app/dashboard/_components/skeleton-loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton";
2 |
3 | export const SkeletonLoading = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster } from "sonner";
2 | import { LayoutNav } from "./_components/layout-nav";
3 | import { Header } from "@/components/header";
4 | import { type Metadata } from "next";
5 | import { APP_NAME } from "@/lib/consts";
6 |
7 | export const metadata: Metadata = {
8 | title: `Dashboard | ${APP_NAME}`,
9 | };
10 |
11 | export default async function DashboardLayout({
12 | children,
13 | }: Readonly<{
14 | children: React.ReactNode;
15 | }>) {
16 | return (
17 | <>
18 |
19 |
20 |
21 |
22 |
23 |
24 |
Dashboard
25 |
26 |
27 |
28 | {children}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | >
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from "lucide-react";
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2 | import { ChatLogSection } from "./_components/chat-log-section";
3 |
4 | export default function DashboardPage() {
5 | return (
6 |
7 |
8 |
9 | Chat logs
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/ai-settings-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const AiSettingsButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | {pending && }
13 | Update
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/ai-settings.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CardContent, CardFooter } from "@/components/ui/card";
4 | import { Slider } from "@/components/ui/slider";
5 | import { updateAISettings } from "../actions";
6 | import { AiSettingsButton } from "./ai-settings-button";
7 | import { toast } from "sonner";
8 | import { useState } from "react";
9 |
10 | export const AiSettings = ({ temperature }: { temperature: number }) => {
11 | const [newTemperature, setNewtemperature] = useState(temperature);
12 |
13 | return (
14 | {
16 | const result = await updateAISettings(formData);
17 |
18 | if (result.success) {
19 | toast.success("The temperature value is successfully updated.");
20 | } else if (result.success === false && result.message) {
21 | toast.error(result.message);
22 | }
23 | }}
24 | >
25 |
26 |
27 |
28 | Temperature
29 |
30 |
{newTemperature}
31 |
32 | {
38 | setNewtemperature(values[0] ?? 0);
39 | }}
40 | />
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/logout-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const LogoutButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | {pending && }
13 | Logout
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/logout-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { logout } from "@/actions/logout";
4 | import { LogoutButton } from "./logout-button";
5 |
6 | export const LogoutForm = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/project-connect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { CardContent, CardFooter } from "@/components/ui/card";
5 | import { APP_URL } from "@/lib/consts";
6 | import { Copy, Check } from "lucide-react";
7 | import { useState } from "react";
8 |
9 | export const ProjectConnect = ({ chatbotId }: { chatbotId: string }) => {
10 | const [isCopied, setIsCopied] = useState(false);
11 |
12 | const scriptText = ``;
17 |
18 | const handleCopy = async () => {
19 | await navigator.clipboard.writeText(scriptText);
20 | setIsCopied(true);
21 | setTimeout(() => setIsCopied(false), 3000);
22 | };
23 |
24 | return (
25 | <>
26 |
27 |
28 | {scriptText}
29 |
30 |
31 |
32 |
33 | {isCopied ? (
34 |
35 | ) : (
36 |
37 | )}
38 | {isCopied ? "Copied!" : "Copy"}
39 |
40 |
41 | >
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/project-update-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const ProjectUpdateButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | {pending && }
13 | Update
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/project-update-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CardContent, CardFooter } from "@/components/ui/card";
4 | import { Input } from "@/components/ui/input";
5 | import { useState } from "react";
6 | import { toast } from "sonner";
7 | import { updateChatbotName } from "../actions";
8 | import { ProjectUpdateButton } from "./project-update-button";
9 | import { useRouter } from "next/navigation";
10 |
11 | export const ProjectUpdateForm = ({ projectName }: { projectName: string }) => {
12 | const router = useRouter();
13 |
14 | const [name, setName] = useState(projectName);
15 |
16 | return (
17 | {
19 | const projectName = formData.get("project-name") as string | null;
20 |
21 | if (!projectName || !projectName.replace(/\s+/g, "")) {
22 | return toast.error("Project name can't be empty.");
23 | }
24 |
25 | const result = await updateChatbotName(projectName);
26 |
27 | if (result.success)
28 | return toast.success("Your project name has been updated!");
29 |
30 | if (!result.success) {
31 | toast.error(result.message);
32 |
33 | if (result.needLogin) router.push("/login");
34 | }
35 | }}
36 | >
37 |
38 | setName(e.target.value)}
42 | name="project-name"
43 | />
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/project-visibility-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const ProjectVisibilityButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | {pending && }
13 | Update
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/_components/project-visibility-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CardContent, CardFooter } from "@/components/ui/card";
4 | import { useState } from "react";
5 | import { toast } from "sonner";
6 | import { updateChatbotVisibility } from "../actions";
7 | import { useRouter } from "next/navigation";
8 | import {
9 | Select,
10 | SelectContent,
11 | SelectItem,
12 | SelectTrigger,
13 | SelectValue,
14 | } from "@/components/ui/select";
15 | import { ProjectVisibilityButton } from "./project-visibility-button";
16 |
17 | export const ProjectVisibilityForm = ({
18 | chatbotVisibility,
19 | }: {
20 | chatbotVisibility: string;
21 | }) => {
22 | const router = useRouter();
23 |
24 | const [visibility, setVisibility] = useState(chatbotVisibility);
25 |
26 | return (
27 | {
29 | const chatbotVisibility = formData.get("chatbot-visibility") as string;
30 |
31 | const result = await updateChatbotVisibility(chatbotVisibility);
32 |
33 | if (result.success)
34 | return toast.success("Your chatbot visibility has been updated!");
35 |
36 | if (!result.success) {
37 | toast.error(result.message);
38 |
39 | if (result.needLogin) router.push("/login");
40 | }
41 | }}
42 | >
43 |
44 | {
48 | setVisibility(e);
49 | }}
50 | >
51 |
52 |
53 |
54 |
55 | Private
56 | Public
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { createClient } from "@/lib/supabase/server";
4 | import { revalidatePath } from "next/cache";
5 |
6 | export const updateChatbotName = async (projectName: string) => {
7 | const supabase = createClient();
8 | const {
9 | data: { user },
10 | error: userFetchError,
11 | } = await supabase.auth.getUser();
12 |
13 | if (!user || userFetchError) {
14 | return {
15 | success: false,
16 | needLogin: true,
17 | message: "Failed to fetch user data. Please login again.",
18 | };
19 | }
20 |
21 | const { error } = await supabase
22 | .from("chatbots")
23 | .update({ name: projectName })
24 | .match({ user_auth_id: user.id });
25 |
26 | if (error) {
27 | return {
28 | success: false,
29 | needLogin: false,
30 | message: error.message,
31 | };
32 | }
33 |
34 | return {
35 | success: true,
36 | };
37 | };
38 |
39 | export const updateChatbotVisibility = async (chatbotVisibility: string) => {
40 | const supabase = createClient();
41 | const {
42 | data: { user },
43 | error: userFetchError,
44 | } = await supabase.auth.getUser();
45 |
46 | if (!user || userFetchError) {
47 | return {
48 | success: false,
49 | needLogin: true,
50 | message: "Failed to fetch user data. Please login again.",
51 | };
52 | }
53 |
54 | const { error } = await supabase
55 | .from("chatbots")
56 | .update({ is_public: chatbotVisibility === "public" })
57 | .match({ user_auth_id: user.id });
58 |
59 | if (error) {
60 | return {
61 | success: false,
62 | needLogin: false,
63 | message: error.message,
64 | };
65 | }
66 |
67 | return {
68 | success: true,
69 | };
70 | };
71 |
72 | export const updateAISettings = async (formData: FormData) => {
73 | const temperature = formData.get("temperature") as number | null;
74 |
75 | if (!temperature) {
76 | return {
77 | success: false,
78 | message: "temperature can't be empty",
79 | };
80 | }
81 |
82 | const numberedTemperature = Number(temperature);
83 |
84 | const supabase = createClient();
85 | const {
86 | data: { user },
87 | error: userFetchError,
88 | } = await supabase.auth.getUser();
89 |
90 | if (!user || userFetchError) {
91 | return {
92 | success: false,
93 | needLogin: true,
94 | message: "Failed to fetch user data. Please login again.",
95 | };
96 | }
97 |
98 | const { error } = await supabase
99 | .from("chatbots")
100 | .update({ temperature: numberedTemperature })
101 | .match({ user_auth_id: user.id });
102 |
103 | if (error) {
104 | return {
105 | success: false,
106 | needLogin: false,
107 | message: error.message,
108 | };
109 | }
110 |
111 | revalidatePath("/dashboard/settings");
112 |
113 | return {
114 | success: true,
115 | };
116 | };
117 |
--------------------------------------------------------------------------------
/src/app/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardFooter,
5 | CardHeader,
6 | CardTitle,
7 | } from "@/components/ui/card";
8 | import { LogoutForm } from "./_components/logout-form";
9 | import { createClient } from "@/lib/supabase/server";
10 | import { redirect } from "next/navigation";
11 | import { ProjectUpdateForm } from "./_components/project-update-form";
12 | import { ProjectVisibilityForm } from "./_components/project-visibility-form";
13 | import { ProjectConnect } from "./_components/project-connect";
14 | import { Slider } from "@/components/ui/slider";
15 | import { AiSettings } from "./_components/ai-settings";
16 |
17 | export default async function SettingsPage() {
18 | const supabase = createClient();
19 | const {
20 | data: { user },
21 | } = await supabase.auth.getUser();
22 | if (!user) redirect("/login");
23 |
24 | const { data, error } = await supabase
25 | .from("chatbots")
26 | .select("name, is_public, internal_id, temperature")
27 | .match({
28 | user_auth_id: user.id,
29 | })
30 | .single();
31 |
32 | if (!data || error) redirect("/login");
33 |
34 | return (
35 |
36 |
37 |
38 | Chatbot name
39 |
40 |
41 |
42 |
43 |
44 |
45 | Chatbot visibility
46 |
47 |
50 |
51 |
52 |
53 |
54 | Chatbot connect
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | AI
63 |
64 |
65 |
66 |
67 |
68 |
69 | Logout
70 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/columns.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ColumnDef } from "@tanstack/react-table";
4 | import { cn } from "@/lib/utils";
5 | import { DeleteButton } from "./delete-button";
6 | import { deleteSource } from "../actions";
7 | import { toast } from "sonner";
8 |
9 | export type Scraping = {
10 | id: number;
11 | status: string | null;
12 | url: string;
13 | };
14 |
15 | export const columns: ColumnDef[] = [
16 | {
17 | accessorKey: "id",
18 | header: "Id",
19 | enableHiding: false,
20 | },
21 | {
22 | accessorKey: "status",
23 | header: "Status",
24 | cell: ({ row }) => {
25 | const status = row.original.status;
26 |
27 | return (
28 |
36 | {status}
37 |
38 | );
39 | },
40 | },
41 | {
42 | accessorKey: "url",
43 | header: "URL",
44 | },
45 | {
46 | id: "actions",
47 | enableHiding: false,
48 | cell: ({ row }) => {
49 | const id = row.getValue("id") as number;
50 |
51 | return (
52 | {
54 | const confirmDelete = confirm(
55 | "Are you sure you want to delete this source?"
56 | );
57 | if (!confirmDelete) {
58 | return;
59 | }
60 |
61 | const result = await deleteSource(id);
62 |
63 | if (result.success) {
64 | toast.success("Successfully deleted.");
65 | } else {
66 | toast.error(`Error: ${result.message}`);
67 | }
68 | }}
69 | >
70 |
71 |
72 | );
73 | },
74 | },
75 | ];
76 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/crawl-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const CrawlButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | {pending && }
13 | Fetch a link
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/crawl-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "@/components/ui/input";
4 | import { CrawlButton } from "./crawl-button";
5 | import { scrape } from "@/actions/scrape";
6 | import { toast } from "sonner";
7 | import { createClient } from "@/lib/supabase/client";
8 | import { useRouter } from "next/navigation";
9 | import { useChatbotInternalId } from "@/lib/hooks/use-chatbot-internal-id";
10 |
11 | export const CrawlForm = () => {
12 | const router = useRouter();
13 | const supabase = createClient();
14 |
15 | const chatbotInternalId = useChatbotInternalId();
16 |
17 | return (
18 | {
21 | const url = formData.get("url") as string;
22 |
23 | if (!url || !url.replace(/\s+/g, "")) {
24 | return toast.error("URL can't be empty.");
25 | }
26 |
27 | toast.success("We're adding a new source...hang tight!");
28 |
29 | const result = await scrape(url);
30 | if (!result.success) {
31 | toast.error(result.message, { duration: 7000 });
32 | return;
33 | }
34 |
35 | await supabase
36 | .from("urls")
37 | .update({
38 | status: "done",
39 | })
40 | .match({ url, chatbot_internal_id: chatbotInternalId });
41 |
42 | router.refresh();
43 |
44 | toast.success("A new source has been added!");
45 | }}
46 | >
47 |
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/delete-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader, Trash } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const DeleteButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | {pending ? (
13 |
14 | ) : (
15 |
16 | )}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/fetched-sources-section.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DataTable } from "@/components/ui/data-table";
4 | import { columns } from "./columns";
5 | import { Tables } from "@/types/supabase";
6 |
7 | export type Source = Tables<"urls">;
8 |
9 | export const FetchedSourcesSection = ({
10 | sources,
11 | }: {
12 | sources: Source[] | null;
13 | }) => {
14 | const handleSelectionChange = (newSelectedRows: string[]) => {
15 | // setSelectedRows(newSelectedRows);
16 | };
17 | return (
18 | <>
19 | Fetched sources
20 |
21 | {sources ? (
22 | {
25 | return {
26 | id: source.id,
27 | url: source.url,
28 | status: source.status ?? "waiting",
29 | };
30 | })}
31 | state={{
32 | columnVisibility: { id: false },
33 | }}
34 | onSelectionChange={handleSelectionChange}
35 | />
36 | ) : (
37 | No sources yet
38 | )}
39 | >
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/sitemap-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export const SitemapButton = () => {
8 | const { pending } = useFormStatus();
9 |
10 | return (
11 |
12 | {pending && }Find sources
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/sitemap-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "@/components/ui/input";
4 | import { SitemapButton } from "./sitemap-button";
5 | import { useFormState } from "react-dom";
6 | import { findSites } from "@/actions/find-sites";
7 | import { DataTable } from "@/components/ui/data-table";
8 | import { useEffect, useState } from "react";
9 | import { scrape } from "@/actions/scrape";
10 | import { Button } from "@/components/ui/button";
11 | import { createClient } from "@/lib/supabase/client";
12 | import { statusColumns } from "./status-columns";
13 | import { sourceListColumns } from "./source-list-columns";
14 | import { useRouter } from "next/navigation";
15 | import { toast } from "sonner";
16 | import { useChatbotInternalId } from "@/lib/hooks/use-chatbot-internal-id";
17 |
18 | const initialState = {
19 | sites: [],
20 | };
21 |
22 | export const SitemapForm = () => {
23 | const supabase = createClient();
24 | const router = useRouter();
25 | const chatbotInternalId = useChatbotInternalId();
26 |
27 | const [scraping, setScraping] = useState(false);
28 | const [selectedRows, setSelectedRows] = useState([]);
29 | const [scrapingInitiated, setScrapingInitiated] = useState(false);
30 | const [state, formAction] = useFormState(findSites, initialState);
31 | const [scrapingStatus, setScrapingStatus] = useState<
32 | { url: string; status: string }[]
33 | >([]);
34 | const [showTable, setShowTable] = useState(true);
35 |
36 | const handleSelectionChange = (newSelectedRows: string[]) => {
37 | setSelectedRows(newSelectedRows);
38 | };
39 |
40 | useEffect(() => {
41 | const cloned = [...selectedRows];
42 |
43 | setScrapingStatus(
44 | cloned.map((c) => {
45 | return {
46 | url: c,
47 | status: "Waiting",
48 | };
49 | })
50 | );
51 | }, [selectedRows]);
52 |
53 | if (state.success === false) {
54 | toast.error("URL is required");
55 | }
56 |
57 | return (
58 | <>
59 |
60 |
67 |
68 |
69 | {showTable && state.sites.length > 0 && (
70 |
71 |
72 |
Found sources
73 |
{
75 | e.preventDefault();
76 |
77 | setScrapingInitiated(true);
78 |
79 | for (const site of selectedRows) {
80 | console.log(`scraping ${site}...`);
81 |
82 | setScrapingStatus((prev) => {
83 | const cloned = [...prev];
84 |
85 | const index = cloned.findIndex((item) => item.url === site);
86 | if (index !== -1) {
87 | cloned[index].status = "Processing...";
88 | }
89 |
90 | return cloned;
91 | });
92 |
93 | const result = await scrape(site);
94 | if (!result.success) {
95 | toast.error(result.message, { duration: 7000 });
96 | setShowTable(false);
97 | router.refresh();
98 |
99 | return;
100 | }
101 |
102 | setScrapingStatus((prev) => {
103 | const cloned = [...prev];
104 |
105 | const index = cloned.findIndex((item) => item.url === site);
106 | if (index !== -1) {
107 | cloned[index].status = "Done";
108 | }
109 |
110 | return cloned;
111 | });
112 |
113 | await supabase
114 | .from("urls")
115 | .update({
116 | status: "done",
117 | })
118 | .match({
119 | url: site,
120 | chatbot_internal_id: chatbotInternalId,
121 | });
122 | }
123 |
124 | router.refresh();
125 | }}
126 | >
127 |
134 | Scrape the selected sources
135 |
136 |
137 |
138 | {scrapingInitiated ? (
139 |
144 | ) : (
145 |
({ url: site }))}
148 | onSelectionChange={handleSelectionChange}
149 | />
150 | )}
151 |
152 | )}
153 | >
154 | );
155 | };
156 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/source-list-columns.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Checkbox } from "@/components/ui/checkbox";
4 | import { ColumnDef } from "@tanstack/react-table";
5 |
6 | // This type is used to define the shape of our data.
7 | // You can use a Zod schema here if you want.
8 | export type Payment = {
9 | url: string;
10 | };
11 |
12 | export const sourceListColumns: ColumnDef[] = [
13 | {
14 | id: "select",
15 | header: ({ table }) => (
16 | table.toggleAllPageRowsSelected(!!value)}
22 | aria-label="Select all"
23 | />
24 | ),
25 | cell: ({ row }) => (
26 | row.toggleSelected(!!value)}
29 | aria-label="Select row"
30 | />
31 | ),
32 | enableSorting: false,
33 | enableHiding: false,
34 | },
35 | {
36 | accessorKey: "url",
37 | header: "URL",
38 | },
39 | ];
40 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/_components/status-columns.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ColumnDef } from "@tanstack/react-table";
4 |
5 | // This type is used to define the shape of our data.
6 | // You can use a Zod schema here if you want.
7 | export type Payment = {
8 | url: string;
9 | status: string;
10 | };
11 |
12 | export const statusColumns: ColumnDef[] = [
13 | {
14 | accessorKey: "url",
15 | header: "URL",
16 | },
17 | {
18 | accessorKey: "status",
19 | header: "Status",
20 | },
21 | ];
22 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { createClient } from "@/lib/supabase/server";
4 | import { revalidatePath } from "next/cache";
5 |
6 | export const deleteSource = async (sourceId: number) => {
7 | const supabase = createClient();
8 |
9 | try {
10 | const { data } = await supabase
11 | .from("urls")
12 | .select("url, chatbot_internal_id")
13 | .match({
14 | id: sourceId,
15 | });
16 |
17 | // First, we need to delete vector data
18 | const { error: vectorDeletionError } = await supabase
19 | .from("vectors")
20 | .delete()
21 | .filter(
22 | "metadata->>chatbot_internal_id",
23 | "eq",
24 | data?.[0].chatbot_internal_id
25 | )
26 | .filter("metadata->>url::text", "eq", data?.[0].url);
27 |
28 | if (vectorDeletionError) {
29 | throw new Error(vectorDeletionError.message);
30 | }
31 |
32 | // then, we delete the data on urls table
33 | await supabase.from("urls").delete().match({
34 | id: sourceId,
35 | });
36 |
37 | revalidatePath("/dashboard/sources");
38 |
39 | return {
40 | success: true,
41 | };
42 | } catch (err) {
43 | return {
44 | success: false,
45 | message: (err as Error).message,
46 | };
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/app/dashboard/sources/page.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2 | import { Separator } from "@/components/ui/separator";
3 | import {
4 | type Source,
5 | FetchedSourcesSection,
6 | } from "./_components/fetched-sources-section";
7 | import { createClient } from "@/lib/supabase/server";
8 | import { CrawlForm } from "./_components/crawl-form";
9 | import { SitemapForm } from "./_components/sitemap-form";
10 |
11 | // to handle scraping server actions
12 | export const maxDuration = 30;
13 |
14 | export default async function SourcesPage() {
15 | const supabase = createClient();
16 |
17 | const {
18 | data: { user },
19 | } = await supabase.auth.getUser();
20 | const { data: projectAndUrls } = await supabase
21 | .from("chatbots")
22 | .select("internal_id, urls(id, url, status)")
23 | .match({
24 | user_auth_id: user!.id,
25 | });
26 |
27 | return (
28 |
29 |
30 |
31 | Sources
32 |
33 |
34 |
38 |
39 |
40 | OR
41 |
42 |
43 |
47 |
48 |
49 |
50 |
55 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/error/page.tsx:
--------------------------------------------------------------------------------
1 | export default function ErrorPage() {
2 | return Sorry, something went wrong
;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taishikato/chatsage/a4d5521f5e07a6900e41d8785cadfb962b7f5c6b/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: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 10% 3.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 240 10% 3.9%;
36 | --foreground: 0 0% 98%;
37 | --card: 240 10% 3.9%;
38 | --card-foreground: 0 0% 98%;
39 | --popover: 240 10% 3.9%;
40 | --popover-foreground: 0 0% 98%;
41 | --primary: 0 0% 98%;
42 | --primary-foreground: 240 5.9% 10%;
43 | --secondary: 240 3.7% 15.9%;
44 | --secondary-foreground: 0 0% 98%;
45 | --muted: 240 3.7% 15.9%;
46 | --muted-foreground: 240 5% 64.9%;
47 | --accent: 240 3.7% 15.9%;
48 | --accent-foreground: 0 0% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 0 0% 98%;
51 | --border: 240 3.7% 15.9%;
52 | --input: 240 3.7% 15.9%;
53 | --ring: 240 4.9% 83.9%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { cn } from "@/lib/utils";
5 | import { APP_NAME } from "@/lib/consts";
6 | import { GoogleAnalytics } from "@next/third-parties/google";
7 |
8 | const inter = Inter({ subsets: ["latin"] });
9 |
10 | export const metadata: Metadata = {
11 | title: `${APP_NAME} | 24/7 Customer Service for your website`,
12 | description:
13 | "Chatsage is an OSS AI Chatbot application. Deploy AI-driven chatbots to handle customer inquiries instantly and efficiently, improving response times.",
14 | };
15 |
16 | export default async function RootLayout({
17 | children,
18 | }: Readonly<{
19 | children: React.ReactNode;
20 | }>) {
21 | return (
22 |
23 |
24 | {children}
25 |
26 | {process.env.NEXT_PUBLIC_GA_ID && (
27 |
28 | )}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/login/_components/login-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Icons } from "@/components/icons";
4 | import { Button } from "@/components/ui/button";
5 | import { Loader } from "lucide-react";
6 | import { useFormStatus } from "react-dom";
7 |
8 | export const LoginButton = () => {
9 | const { pending } = useFormStatus();
10 |
11 | return (
12 |
18 | {pending ? (
19 |
20 | ) : (
21 |
22 | )}{" "}
23 | Login in with Google
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/app/login/_components/login-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { LoginButton } from "./login-button";
4 | import { login } from "../actions";
5 |
6 | export const LoginForm = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/app/login/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { createClient } from "@/lib/supabase/server";
4 | import { redirect } from "next/navigation";
5 | import { headers } from "next/headers";
6 |
7 | export async function login(formData: FormData) {
8 | const supabase = createClient();
9 | const origin = headers().get("origin");
10 |
11 | const { data, error } = await supabase.auth.signInWithOAuth({
12 | provider: "google",
13 | options: {
14 | redirectTo: `${origin}/auth/callback`,
15 | },
16 | });
17 |
18 | if (error) {
19 | redirect("/error");
20 | }
21 |
22 | redirect(data.url);
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardDescription,
5 | CardHeader,
6 | CardTitle,
7 | } from "@/components/ui/card";
8 | import { LoginForm } from "./_components/login-form";
9 | import { Header } from "@/components/header";
10 |
11 | export default function LoginPage() {
12 | return (
13 | <>
14 |
15 |
16 |
17 |
18 | Login
19 |
20 | Start by signing in with your Google account
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | >
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taishikato/chatsage/a4d5521f5e07a6900e41d8785cadfb962b7f5c6b/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Header } from "@/components/header";
2 | import Script from "next/script";
3 | import { APP_URL } from "../lib/consts";
4 | import { Hero } from "@/components/hero";
5 | import { Footer } from "@/components/footer";
6 | import { HowItWorks } from "@/components/how-it-works";
7 | import { CloudNotification } from "@/components/cloud-notification";
8 |
9 | export default async function Home() {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 | >
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taishikato/chatsage/a4d5521f5e07a6900e41d8785cadfb962b7f5c6b/src/app/twitter-image.png
--------------------------------------------------------------------------------
/src/components/cloud-notification.tsx:
--------------------------------------------------------------------------------
1 | import { AlertCircle } from "lucide-react";
2 | import Link from "next/link";
3 |
4 | export function CloudNotification() {
5 | return (
6 |
7 |
8 |
11 |
12 |
13 | ChatSage Cloud is currently inactive. You can still use the
14 | self-hosted version by{" "}
15 |
19 | following our installation guide
20 |
21 | .
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import { APP_NAME } from "@/lib/consts";
2 | import { FaDiscord } from "react-icons/fa6";
3 |
4 | const navigation = [
5 | {
6 | name: "GitHub",
7 | href: "https://github.com/taishikato/chatsage",
8 | icon: (props: any) => (
9 |
10 |
15 |
16 | ),
17 | },
18 | {
19 | name: "Discord",
20 | href: "https://discord.gg/reEuUQNYb3",
21 | icon: (props: any) => ,
22 | },
23 | {
24 | name: "X",
25 | href: "https://x.com/taishik_",
26 | icon: (props: any) => (
27 |
28 |
29 |
30 | ),
31 | },
32 | ] as const;
33 |
34 | export const Footer = () => {
35 | return (
36 |
37 |
38 |
51 |
52 |
53 | © {new Date().getFullYear()} {APP_NAME}. All rights reserved.
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { Dialog, DialogPanel } from "@headlessui/react";
5 | import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
6 | import { APP_NAME } from "@/lib/consts";
7 | import Link from "next/link";
8 | import { createClient } from "@/lib/supabase/client";
9 | import { Button, buttonVariants } from "./ui/button";
10 | import { MoveRight, Star } from "lucide-react";
11 | import { cn } from "@/lib/utils";
12 | import { IconGitHub } from "@/app/(chat)/chatbot-embedding/[id]/components/ui/icons";
13 |
14 | export const Header = () => {
15 | const supabase = createClient();
16 |
17 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
18 | const [isLoggedIn, setIsLoggedIn] = useState(false);
19 |
20 | useEffect(() => {
21 | const checkUserStatus = async () => {
22 | const { data } = await supabase.auth.getUser();
23 |
24 | if (data?.user) setIsLoggedIn(true);
25 | };
26 |
27 | checkUserStatus();
28 | }, []);
29 |
30 | return (
31 |
141 | );
142 | };
143 |
--------------------------------------------------------------------------------
/src/components/hero.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { buttonVariants } from "./ui/button";
3 | import { IconGitHub } from "@/app/(chat)/chatbot-embedding/[id]/components/ui/icons";
4 | import { APP_NAME, APP_URL } from "@/lib/consts";
5 | import Link from "next/link";
6 |
7 | export const Hero = () => {
8 | return (
9 |
10 |
14 |
15 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
36 | 24/7 Customer Service for your website
37 |
38 |
39 |
40 | {APP_NAME} is an open source AI chatbot for your website.
41 |
42 |
43 | Deploy AI-driven chatbots to handle customer inquiries instantly
44 | and efficiently, improving response times.
45 |
46 |
47 |
63 |
64 |
65 |
69 |
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/components/how-it-works.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { buttonVariants } from "./ui/button";
3 | import { cn } from "@/lib/utils";
4 |
5 | const steps = [
6 | {
7 | title: "1. Add your data",
8 | subtitle: "Enter the URL of your website or the URL of your sitemap.",
9 | video: "/videos/how-it-works-add-data.mp4",
10 | },
11 | {
12 | title: "2. Embed your bot.",
13 | subtitle:
14 | "Make your chatbot public and embed your own custom bot on your website.",
15 | video: "/videos/how-it-works-embedding.mp4",
16 | },
17 | ];
18 | export const HowItWorks = () => {
19 | return (
20 |
21 |
22 | How It Works — Supa easy!
23 |
24 |
25 | Fetch your data, train your bot, and embed it on your website.
26 |
27 |
28 |
29 | {steps.map((step) => {
30 | return (
31 |
35 |
36 |
37 | {step.title}
38 |
39 |
{step.subtitle}
40 |
41 |
42 |
43 |
50 |
51 |
52 | );
53 | })}
54 |
55 |
56 |
57 |
64 | Get started for free
65 |
66 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-xl px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/src/components/ui/data-table.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | type ColumnDef,
5 | type TableState,
6 | flexRender,
7 | getCoreRowModel,
8 | useReactTable,
9 | } from "@tanstack/react-table";
10 |
11 | import {
12 | Table,
13 | TableBody,
14 | TableCell,
15 | TableHead,
16 | TableHeader,
17 | TableRow,
18 | } from "@/components/ui/table";
19 | import { useEffect } from "react";
20 |
21 | interface DataTableProps {
22 | columns: ColumnDef[];
23 | data: TData[];
24 | state?: Partial;
25 | onSelectionChange: (selectedRows: string[]) => void;
26 | }
27 |
28 | export function DataTable({
29 | columns,
30 | data,
31 | state,
32 | onSelectionChange,
33 | }: DataTableProps) {
34 | const table = useReactTable({
35 | data,
36 | columns,
37 | state,
38 | getCoreRowModel: getCoreRowModel(),
39 | });
40 |
41 | useEffect(() => {
42 | const selectedRows = table
43 | .getFilteredSelectedRowModel()
44 | .rows.map((row) => row.getValue("url") as string);
45 | onSelectionChange(selectedRows);
46 | }, [table.getState().rowSelection]);
47 |
48 | return (
49 | <>
50 |
51 |
52 |
53 | {table.getHeaderGroups().map((headerGroup) => (
54 |
55 | {headerGroup.headers.map((header) => {
56 | return (
57 |
58 | {header.isPlaceholder
59 | ? null
60 | : flexRender(
61 | header.column.columnDef.header,
62 | header.getContext()
63 | )}
64 |
65 | );
66 | })}
67 |
68 | ))}
69 |
70 |
71 | {table.getRowModel().rows?.length ? (
72 | table.getRowModel().rows.map((row) => (
73 |
77 | {row.getVisibleCells().map((cell) => (
78 |
79 | {flexRender(
80 | cell.column.columnDef.cell,
81 | cell.getContext()
82 | )}
83 |
84 | ))}
85 |
86 | ))
87 | ) : (
88 |
89 |
93 | No results.
94 |
95 |
96 | )}
97 |
98 |
99 |
100 | >
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/lib/check-url-number.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "./supabase/server";
2 |
3 | const sourceLimit = 10;
4 |
5 | export const checkUrlNumber = async (chatbotInternalId: string) => {
6 | const supabase = createClient();
7 |
8 | const { count } = await supabase
9 | .from("urls")
10 | .select("id", { count: "exact" })
11 | .match({
12 | chatbot_internal_id: chatbotInternalId,
13 | });
14 |
15 | if (count && count >= sourceLimit) {
16 | throw new Error(
17 | `You have reached the maximum limit of ${sourceLimit} sources. Sorry Chatsage is still in beta and we're working on it!`
18 | );
19 | }
20 |
21 | return;
22 | };
23 |
--------------------------------------------------------------------------------
/src/lib/consts.ts:
--------------------------------------------------------------------------------
1 | const isProd = process.env.NODE_ENV === "production";
2 |
3 | export const APP_URL = isProd
4 | ? process.env.NEXT_PUBLIC_APP_URL
5 | : "http://localhost:3000";
6 |
7 | export const APP_NAME = "Chatsage";
8 |
--------------------------------------------------------------------------------
/src/lib/extract-text-from-html.ts:
--------------------------------------------------------------------------------
1 | import { decode } from "html-entities";
2 |
3 | export function extractTextFromHtml(htmlString: string): string {
4 | // Remove script and style elements
5 | htmlString = htmlString.replace(/<(script|style)[\s\S]*?<\/\1>/gi, "");
6 |
7 | // Remove HTML comments
8 | htmlString = htmlString.replace(//g, "");
9 |
10 | // Remove all remaining HTML tags
11 | htmlString = htmlString.replace(/<[^>]+>/g, "");
12 |
13 | // Decode HTML entities
14 | htmlString = decode(htmlString);
15 |
16 | // Remove extra whitespace
17 | htmlString = htmlString.replace(/\s+/g, " ").trim();
18 |
19 | return htmlString;
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/hooks/use-chatbot-internal-id.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { createClient } from "@/lib/supabase/client";
3 |
4 | export const useChatbotInternalId = () => {
5 | const supabase = createClient();
6 |
7 | const [chatbotInternalId, setChatbotInternalId] = useState(
8 | null
9 | );
10 |
11 | useEffect(() => {
12 | const fetchProjectId = async () => {
13 | const {
14 | data: { user },
15 | error,
16 | } = await supabase.auth.getUser();
17 |
18 | if (user) {
19 | const { data: chatbot, error: chatbotFetchError } = await supabase
20 | .from("chatbots")
21 | .select("internal_id")
22 | .match({ user_auth_id: user.id })
23 | .single();
24 |
25 | if (!chatbot || chatbotFetchError) {
26 | console.error(
27 | "Error fetching chatbot ID:",
28 | chatbotFetchError.message ?? "No detail"
29 | );
30 | } else {
31 | const chatbotInternalId = chatbot.internal_id;
32 | setChatbotInternalId(chatbotInternalId);
33 | }
34 | }
35 | };
36 |
37 | fetchProjectId();
38 | }, [supabase]);
39 |
40 | return chatbotInternalId;
41 | };
42 |
--------------------------------------------------------------------------------
/src/lib/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { type Database } from "@/types/supabase";
2 | import { createBrowserClient } from "@supabase/ssr";
3 |
4 | export const createClient = () =>
5 | createBrowserClient(
6 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
8 | );
9 |
--------------------------------------------------------------------------------
/src/lib/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { Database } from "@/types/supabase";
2 | import { createServerClient } from "@supabase/ssr";
3 | import { NextResponse, type NextRequest } from "next/server";
4 | import { Ratelimit } from "@upstash/ratelimit";
5 | import { Redis } from "@upstash/redis";
6 |
7 | const ratelimit = new Ratelimit({
8 | redis: Redis.fromEnv(),
9 | limiter: Ratelimit.slidingWindow(10, "10 s"),
10 | });
11 |
12 | export async function updateSession(request: NextRequest) {
13 | /**
14 | * Rate limiting
15 | */
16 | if (
17 | request.method === "POST" &&
18 | request.nextUrl.pathname.startsWith("/chatbot-embedding")
19 | ) {
20 | // apply your logic here
21 | const ip = request.ip ?? "127.0.0.1";
22 | const { success } = await ratelimit.limit(ip);
23 |
24 | if (!success) {
25 | return NextResponse.json(
26 | {
27 | message: "too many requests",
28 | },
29 | { status: 429 }
30 | );
31 | }
32 | }
33 |
34 | /**
35 | * SUpabase stuff
36 | */
37 | let supabaseResponse = NextResponse.next({
38 | request,
39 | });
40 |
41 | const supabase = createServerClient(
42 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
43 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
44 | {
45 | cookies: {
46 | getAll() {
47 | return request.cookies.getAll();
48 | },
49 | setAll(cookiesToSet) {
50 | cookiesToSet.forEach(({ name, value, options }) =>
51 | request.cookies.set(name, value)
52 | );
53 | supabaseResponse = NextResponse.next({
54 | request,
55 | });
56 | cookiesToSet.forEach(({ name, value, options }) =>
57 | supabaseResponse.cookies.set(name, value, options)
58 | );
59 | },
60 | },
61 | }
62 | );
63 |
64 | // IMPORTANT: Avoid writing any logic between createServerClient and
65 | // supabase.auth.getUser(). A simple mistake could make it very hard to debug
66 | // issues with users being randomly logged out.
67 |
68 | const {
69 | data: { user },
70 | } = await supabase.auth.getUser();
71 |
72 | // For protected routes
73 | if (request.nextUrl.pathname.startsWith("/api/protected")) {
74 | if (!user) {
75 | return NextResponse.json(
76 | {
77 | success: false,
78 | message: "You need to login",
79 | },
80 | { status: 403 }
81 | );
82 | }
83 | }
84 |
85 | if (request.nextUrl.pathname.startsWith("/dashboard")) {
86 | if (!user) {
87 | return NextResponse.redirect(new URL("/login", request.url));
88 | }
89 |
90 | const { count } = await supabase
91 | .from("chatbots")
92 | .select("id", { count: "exact" })
93 | .match({ user_auth_id: user.id });
94 |
95 | if (!count) {
96 | return NextResponse.redirect(
97 | new URL("/create-project/project-name", request.url)
98 | );
99 | }
100 | }
101 |
102 | // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
103 | // creating a new response object with NextResponse.next() make sure to:
104 | // 1. Pass the request in it, like so:
105 | // const myNewResponse = NextResponse.next({ request })
106 | // 2. Copy over the cookies, like so:
107 | // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
108 | // 3. Change the myNewResponse object to fit your needs, but avoid changing
109 | // the cookies!
110 | // 4. Finally:
111 | // return myNewResponse
112 | // If this is not done, you may be causing the browser and server to go out
113 | // of sync and terminate the user's session prematurely!
114 | return supabaseResponse;
115 | }
116 |
--------------------------------------------------------------------------------
/src/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import type { Database } from "@/types/supabase";
2 | import { createServerClient } from "@supabase/ssr";
3 | import { cookies } from "next/headers";
4 |
5 | export function createClient() {
6 | const cookieStore = cookies();
7 |
8 | return createServerClient(
9 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11 | {
12 | cookies: {
13 | getAll() {
14 | return cookieStore.getAll();
15 | },
16 | setAll(cookiesToSet) {
17 | try {
18 | cookiesToSet.forEach(({ name, value, options }) =>
19 | cookieStore.set(name, value, options)
20 | );
21 | } catch {
22 | // The `setAll` method was called from a Server Component.
23 | // This can be ignored if you have middleware refreshing
24 | // user sessions.
25 | }
26 | },
27 | },
28 | }
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/lib/supabase/supabaseAdminClient.ts:
--------------------------------------------------------------------------------
1 | import { type Database } from "@/types/supabase";
2 | import { createClient } from "@supabase/supabase-js";
3 |
4 | export const createAdminClient = () => {
5 | return createClient(
6 | process.env.NEXT_PUBLIC_SUPABASE_URL as string,
7 | process.env.SUPABASE_SERVICE_TOKEN as string
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { CoreMessage } from "ai";
2 |
3 | export type Message = CoreMessage & {
4 | id: string;
5 | };
6 |
7 | export interface Chat extends Record {
8 | id: string;
9 | title: string;
10 | createdAt: Date;
11 | userId: string;
12 | path: string;
13 | messages: Message[];
14 | sharePath?: string;
15 | }
16 |
17 | export type ServerActionResult = Promise<
18 | | Result
19 | | {
20 | error: string;
21 | }
22 | >;
23 |
24 | export interface Session {
25 | user: {
26 | id: string;
27 | email: string;
28 | };
29 | }
30 |
31 | export interface AuthResult {
32 | type: string;
33 | message: string;
34 | }
35 |
36 | export interface User extends Record {
37 | id: string;
38 | email: string;
39 | password: string;
40 | salt: string;
41 | }
42 |
43 | export type AxiosError = { data: { message: string } };
44 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 | import { customAlphabet } from "nanoid";
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | export const nanoid = customAlphabet(
10 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
11 | 7
12 | ); // 7-character random string
13 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from "next/server";
2 | import { updateSession } from "@/lib/supabase/middleware";
3 |
4 | export async function middleware(request: NextRequest) {
5 | return await updateSession(request);
6 | }
7 |
8 | export const config = {
9 | matcher: [
10 | /*
11 | * Match all request paths except for the ones starting with:
12 | * - _next/static (static files)
13 | * - _next/image (image optimization files)
14 | * - favicon.ico (favicon file)
15 | */
16 | "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/supabase/migrations/20240807013210_remote_schema.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."chatbots" add column "temperature" numeric;
2 |
3 | alter table "public"."vectors" enable row level security;
4 |
5 | set check_function_bodies = off;
6 |
7 | CREATE OR REPLACE FUNCTION public.get_chat_logs_by_chatbot(chatbot_id uuid)
8 | RETURNS TABLE(conversation_id text, messages jsonb)
9 | LANGUAGE sql
10 | AS $function$
11 | SELECT
12 | conversation_id,
13 | json_agg(
14 | json_build_object(
15 | 'id', id,
16 | 'created_at', created_at,
17 | 'message', message,
18 | 'role', role,
19 | 'internal_id', internal_id
20 | ) ORDER BY created_at ASC
21 | ) AS messages
22 | FROM
23 | public.chat_logs
24 | WHERE
25 | chatbot_internal_id = chatbot_id
26 | GROUP BY
27 | conversation_id
28 | ORDER BY
29 | MAX(created_at) DESC;
30 | $function$
31 | ;
32 |
33 |
34 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taishikato/chatsage/a4d5521f5e07a6900e41d8785cadfb962b7f5c6b/supabase/seed.sql
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules", "scripts"]
26 | }
27 |
--------------------------------------------------------------------------------