├── .env.example
├── .eslintrc.json
├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierignore
├── LICENSE
├── README.md
├── app
├── (auth)
│ ├── auth-code-error
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── signin
│ │ └── page.tsx
│ └── signup
│ │ └── page.tsx
├── api
│ ├── auth
│ │ ├── callback
│ │ │ └── route.ts
│ │ └── logout
│ │ │ └── route.ts
│ └── chat
│ │ └── route.ts
├── apps
│ ├── chat
│ │ ├── [id]
│ │ │ └── page.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── docs
│ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── page.tsx
├── profile
│ ├── layout.tsx
│ └── page.tsx
└── styles
│ └── custom.css
├── components.json
├── components
├── modules
│ ├── apps
│ │ ├── app-side-bar
│ │ │ ├── AppSideBar.tsx
│ │ │ ├── AppSideBarItem.tsx
│ │ │ ├── AppSideBarList.tsx
│ │ │ ├── AppSidebarSection.tsx
│ │ │ └── index.ts
│ │ └── chat
│ │ │ ├── ChatForm.tsx
│ │ │ ├── ChatHistory.tsx
│ │ │ ├── ChatHistoryDrawer.tsx
│ │ │ ├── ChatHistoryItem.tsx
│ │ │ ├── ChatLayout.tsx
│ │ │ ├── ChatPanel.tsx
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── DeleteChatAction.tsx
│ │ │ ├── EditChatAction.tsx
│ │ │ ├── Header.tsx
│ │ │ ├── MobileDrawerControls.tsx
│ │ │ ├── NewChatButton.tsx
│ │ │ ├── SystemPromptControl.tsx
│ │ │ ├── action.ts
│ │ │ ├── chat-members
│ │ │ ├── AddMembersForm.tsx
│ │ │ ├── ChatMemberItem.tsx
│ │ │ ├── ChatMembers.tsx
│ │ │ ├── DeleteMemberAction.tsx
│ │ │ ├── action.ts
│ │ │ ├── index.ts
│ │ │ └── schema.ts
│ │ │ ├── control-side-bar
│ │ │ ├── ControlSidebar.tsx
│ │ │ ├── ControlSidebarSheet.tsx
│ │ │ ├── FrequencyPenaltySelector.tsx
│ │ │ ├── MaxLengthSelector.tsx
│ │ │ ├── ModelSelector.tsx
│ │ │ ├── PresencePenaltySelector.tsx
│ │ │ ├── TemperatureSelector.tsx
│ │ │ ├── TopPSelector.tsx
│ │ │ ├── action.ts
│ │ │ ├── data
│ │ │ │ └── models.ts
│ │ │ └── index.ts
│ │ │ ├── schema.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ ├── auth
│ │ ├── LogoutButton.tsx
│ │ ├── SocialLoginButton.tsx
│ │ ├── SocialLoginOptions.tsx
│ │ ├── UserAuthForm.tsx
│ │ ├── UserSignupForm.tsx
│ │ └── schema.ts
│ ├── home
│ │ ├── DescriptionHeadingText.tsx
│ │ ├── FeatureItems.tsx
│ │ └── HeroBannerImage.tsx
│ └── profile
│ │ ├── AccountDropdownMenu.tsx
│ │ ├── Header.tsx
│ │ ├── ProfileForm.tsx
│ │ ├── action.ts
│ │ ├── schema.ts
│ │ └── type.ts
├── navigation
│ ├── NavigationBar.tsx
│ ├── NavigationMainMenu.tsx
│ └── SideBar.tsx
├── theme
│ ├── ThemeToggle.tsx
│ └── index.ts
└── ui
│ ├── Accordion.tsx
│ ├── AlertDialog.tsx
│ ├── Avatar.tsx
│ ├── Badge.tsx
│ ├── Button.tsx
│ ├── Card.tsx
│ ├── Command.tsx
│ ├── CustomIcon.tsx
│ ├── Dialog.tsx
│ ├── DropdownMenu.tsx
│ ├── Flex.tsx
│ ├── HoverCard.tsx
│ ├── Input.tsx
│ ├── Label.tsx
│ ├── NavigationMenu.tsx
│ ├── Popover.tsx
│ ├── Resizable.tsx
│ ├── ScrollArea.tsx
│ ├── Section.tsx
│ ├── Select.tsx
│ ├── Separator.tsx
│ ├── Sheet.tsx
│ ├── Skeleton.tsx
│ ├── Slider.tsx
│ ├── Switch.tsx
│ ├── Tabs.tsx
│ ├── TextArea.tsx
│ ├── Toast.tsx
│ ├── Toaster.tsx
│ ├── Tooltip.tsx
│ ├── chat
│ ├── ChatBubble.tsx
│ ├── ChatInput.tsx
│ ├── ChatList.tsx
│ ├── ChatProfileHoverCard.tsx
│ ├── Markdown.tsx
│ ├── index.ts
│ └── mention-input-default-style.ts
│ ├── common
│ ├── AppLogo.tsx
│ ├── ChatScrollAnchor.tsx
│ ├── MainLayout.tsx
│ └── UserAvatar.tsx
│ ├── form
│ └── form-fields
│ │ ├── InputField
│ │ ├── InputField.tsx
│ │ └── index.ts
│ │ ├── SliderField
│ │ ├── SliderField.tsx
│ │ └── index.ts
│ │ ├── TextAreaField
│ │ ├── TextAreaField.tsx
│ │ └── index.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── typography
│ ├── Blockquote.tsx
│ ├── Heading1.tsx
│ ├── Heading2.tsx
│ ├── Heading3.tsx
│ ├── Heading4.tsx
│ ├── Heading5.tsx
│ ├── Paragraph.tsx
│ ├── Subtle.tsx
│ ├── index.ts
│ └── types.ts
│ └── use-toast.ts
├── config
└── site.ts
├── env.mjs
├── hooks
├── useActiveTheme.tsx
├── useAtBottom.tsx
├── useChatIdFromPathName.tsx
├── useCopyToClipboard.tsx
├── useEnterSubmit.tsx
├── useMutationObserver.ts
├── usePrevious.tsx
└── useSubscribeChatMessages.ts
├── lib
├── cache.ts
├── chat-input.ts
├── contants.ts
├── db
│ ├── apps.ts
│ ├── chat-members.ts
│ ├── chats.ts
│ ├── database.types.ts
│ ├── index.ts
│ ├── message.ts
│ └── profile.ts
├── session.ts
├── stores
│ └── profile.ts
├── supabase
│ ├── client.ts
│ ├── middleware.ts
│ └── server.ts
└── utils.ts
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── prettier.config.js
├── public
├── avatar.png
├── chat-gpt.jpeg
├── featured-dark.jpg
├── featured-mobile.png
├── featured.jpg
├── logo.png
├── next.svg
├── screenshot.png
└── vercel.svg
├── supabase
├── .gitignore
├── config.toml
├── migrations
│ ├── 20240402103717_init_schema.sql
│ ├── 20240403013936_rls.sql
│ ├── 20240405151156_default_profile_id.sql
│ ├── 20240420162835_chat_members.sql
│ ├── 20240504083818_chat_members.sql
│ ├── 20240609070425_handle_new_user_update.sql
│ ├── 20240626065103_migrate_username_from_email.sql
│ └── 20240626065226_update_handle_new_user.sql
└── seed.sql
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_APP_URL=YOUR_VALUE
2 | OPENAI_API_KEY=YOUR_VALUE
3 | NEXT_PUBLIC_SUPABASE_URL=YOUR_VALUE
4 | NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_VALUE
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "plugin:tailwindcss/recommended",
7 | "plugin:@typescript-eslint/recommended",
8 | "prettier"
9 | ],
10 | "plugins": ["tailwindcss", "prettier"],
11 | "rules": {
12 | "@next/next/no-html-link-for-pages": "off",
13 | "react/jsx-key": "off",
14 | "tailwindcss/no-custom-classname": "off",
15 | "@typescript-eslint/no-unused-vars": "error",
16 | "@typescript-eslint/ban-ts-comment": "off",
17 | "@typescript-eslint/no-empty-function": "off",
18 | "prettier/prettier": "error"
19 | },
20 | "settings": {
21 | "tailwindcss": {
22 | "callees": ["cn"],
23 | "config": "tailwind.config.js"
24 | },
25 | "next": {
26 | "rootDir": ["./"]
27 | }
28 | },
29 | "overrides": [
30 | {
31 | "files": ["*.ts", "*.tsx"],
32 | "parser": "@typescript-eslint/parser"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [nphivu414]
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.*
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | .env
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | cache
2 | .cache
3 | package.json
4 | package-lock.json
5 | public
6 | CHANGELOG.md
7 | .yarn
8 | dist
9 | node_modules
10 | .next
11 | build
12 | .contentlayer
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Vu Nguyen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | AI Fusion Kit
4 |
5 |
6 |
7 | A feature-rich, highly customizable AI Web App Template, empowered by Next.js.
8 |
9 |
10 |
11 | Tech stacks ·
12 | Installation ·
13 | Run Locally ·
14 | Authors
15 |
16 |
17 |
18 | ## Tech stacks
19 | - [Typescript](https://www.typescriptlang.org/)
20 | - [ReactJS](https://reactjs.org/)
21 | - [NextJS](https://nextjs.org/)
22 | - [Supabase](https://supabase.com/)
23 | - [Open AI API](https://platform.openai.com/docs/api-reference)
24 | - [Vercel AI SDK](https://github.com/vercel/ai)
25 | - [TailwindCSS](https://tailwindcss.com/)
26 | - [Shadcn UI](https://ui.shadcn.com/)
27 | - [Aceternity UI](https://ui.aceternity.com/)
28 | - [Next.js AI Chatbot](https://github.com/vercel-labs/ai-chatbot)
29 |
30 |
31 | ## Installation
32 |
33 | 1. Clone the repo
34 | ```sh
35 | git clone https://github.com/nphivu414/ai-fusion-kit
36 | ```
37 | 2. Install dependencies
38 | ```sh
39 | yarn install
40 | ```
41 | 3. Setup Supabase local development
42 | - Install [Docker](https://www.docker.com/get-started/)
43 | - The start command uses Docker to start the Supabase services. This command may take a while to run if this is the first time using the CLI.
44 | ```sh
45 | supabase start
46 | ```
47 | - Once all of the Supabase services are running, you'll see output containing your local Supabase credentials. It should look like this, with urls and keys that you'll use in your local project:
48 | ```sh
49 | Started supabase local development setup.
50 |
51 | API URL: http://localhost:54321
52 | DB URL: postgresql://postgres:postgres@localhost:54322/postgres
53 | Studio URL: http://localhost:54323
54 | Inbucket URL: http://localhost:54324
55 | anon key: eyJh......
56 | service_role key: eyJh......
57 | ```
58 | - The API URL will be used as the `NEXT_PUBLIC_SUPABASE_URL` in `.env.local`
59 | - For more information about how to use Supabase on your local development machine: https://supabase.com/docs/guides/cli/local-development
60 |
61 | 4. Get an account from OpenAI and generate your own API key
62 |
63 | 5. Rename `.env.example` to `.env.local` and populate with your values
64 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts.
65 |
66 | ## Run Locally
67 |
68 | 1. Go to the project directory
69 |
70 | ```bash
71 | cd ai-fusion-kit
72 | ```
73 |
74 | 2. Start the web app
75 |
76 | ```bash
77 | yarn dev
78 | ```
79 |
80 | ## Authors
81 | - [@nphivu414](https://github.com/nphivu414)
82 | - [@toproad1407](https://github.com/toproad1407)
83 |
--------------------------------------------------------------------------------
/app/(auth)/auth-code-error/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 |
3 | import { Heading3 } from "@/components/ui/typography/Heading3";
4 |
5 | export const metadata: Metadata = {
6 | title: "Error",
7 | description: "Failed to sign in",
8 | };
9 |
10 | export default async function AuthCodeError() {
11 | return (
12 | <>
13 |
14 |
Error
15 |
Failed to sign in
16 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { AppLogo } from "@/components/ui/common/AppLogo";
2 |
3 | type AuthLayoutProps = {
4 | children: React.ReactNode;
5 | };
6 |
7 | export default function AuthLayout({ children }: AuthLayoutProps) {
8 | return (
9 |
10 |
11 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/(auth)/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { cookies } from "next/headers";
3 | import { redirect } from "next/navigation";
4 |
5 | import { siteConfig } from "@/config/site";
6 | import { getCurrentUser } from "@/lib/session";
7 | import { createClient } from "@/lib/supabase/server";
8 | import { Heading3 } from "@/components/ui/typography";
9 | import { UserAuthForm } from "@/components/modules/auth/UserAuthForm";
10 |
11 | export const metadata: Metadata = {
12 | title: "Sigin",
13 | description: "Sigin to your account",
14 | };
15 |
16 | export const runtime = "edge";
17 | export const dynamic = "force-dynamic";
18 |
19 | export default async function LoginPage() {
20 | const cookieStore = cookies();
21 | const supabase = createClient(cookieStore);
22 | const user = await getCurrentUser(supabase);
23 |
24 | if (user) {
25 | redirect(`/apps/chat`);
26 | }
27 |
28 | return (
29 | <>
30 |
31 |
{siteConfig.name}
32 |
33 | Empowering Your Imagination with AI Services
34 |
35 |
36 |
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/app/(auth)/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import { cookies } from "next/headers";
3 | import { redirect } from "next/navigation";
4 |
5 | import { siteConfig } from "@/config/site";
6 | import { getCurrentUser } from "@/lib/session";
7 | import { createClient } from "@/lib/supabase/server";
8 | import { Heading3 } from "@/components/ui/typography";
9 | import { UserSignupForm } from "@/components/modules/auth/UserSignupForm";
10 |
11 | export const runtime = "edge";
12 | export const metadata: Metadata = {
13 | title: "Signup",
14 | description: "Signup a new account",
15 | };
16 |
17 | export const dynamic = "force-dynamic";
18 |
19 | export default async function LoginPage() {
20 | const cookieStore = cookies();
21 | const supabase = createClient(cookieStore);
22 | const user = await getCurrentUser(supabase);
23 |
24 | if (user) {
25 | redirect(`/apps/chat`);
26 | }
27 |
28 | return (
29 | <>
30 |
31 |
{siteConfig.name}
32 |
33 | Empowering Your Imagination with AI Services
34 |
35 |
36 |
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/app/api/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 | import { NextResponse } from "next/server";
3 |
4 | import { createClient } from "@/lib/supabase/server";
5 |
6 | export async function GET(request: Request) {
7 | const { searchParams, origin } = new URL(request.url);
8 | const code = searchParams.get("code");
9 | // if "next" is in param, use it as the redirect URL
10 | const next = searchParams.get("next") ?? "/";
11 |
12 | if (code) {
13 | const cookieStore = cookies();
14 | const supabase = createClient(cookieStore);
15 |
16 | const { error } = await supabase.auth.exchangeCodeForSession(code);
17 | if (!error) {
18 | return NextResponse.redirect(`${origin}${next}`);
19 | }
20 | }
21 |
22 | // return the user to an error page with instructions
23 | return NextResponse.redirect(`${origin}/auth-code-error`);
24 | }
25 |
--------------------------------------------------------------------------------
/app/api/auth/logout/route.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 | import { NextResponse, type NextRequest } from "next/server";
3 |
4 | import { createClient } from "@/lib/supabase/server";
5 |
6 | export const dynamic = "force-dynamic";
7 |
8 | export async function POST(req: NextRequest) {
9 | const cookieStore = cookies();
10 | const supabase = createClient(cookieStore);
11 |
12 | // Check if we have a session
13 | const {
14 | data: { user },
15 | } = await supabase.auth.getUser();
16 |
17 | if (user) {
18 | await supabase.auth.signOut();
19 | }
20 |
21 | return NextResponse.redirect(new URL("/signin", req.url), {
22 | status: 302,
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 | import { env } from "@/env.mjs";
3 | import { createOpenAI } from "@ai-sdk/openai";
4 | import { Message, streamText } from "ai";
5 | import { pick } from "lodash";
6 | import { AxiomRequest, withAxiom } from "next-axiom";
7 |
8 | import { getAppBySlug } from "@/lib/db/apps";
9 | import { createNewChatMember } from "@/lib/db/chat-members";
10 | import { createNewChat } from "@/lib/db/chats";
11 | import {
12 | createNewMessage,
13 | deleteMessagesFrom,
14 | getMessageById,
15 | } from "@/lib/db/message";
16 | import { getCurrentUser } from "@/lib/session";
17 | import { createClient } from "@/lib/supabase/server";
18 |
19 | export const dynamic = "force-dynamic";
20 | export const runtime = "edge";
21 | export const preferredRegion = "home";
22 |
23 | const openai = createOpenAI({
24 | apiKey: env.OPENAI_API_KEY,
25 | });
26 |
27 | export const POST = withAxiom(async (req: AxiomRequest) => {
28 | const log = req.log.with({
29 | route: "api/chat",
30 | });
31 |
32 | const cookieStore = cookies();
33 | const supabase = createClient(cookieStore);
34 | const params = await req.json();
35 | const {
36 | messages,
37 | temperature,
38 | model,
39 | maxTokens,
40 | topP,
41 | frequencyPenalty,
42 | presencePenalty,
43 | chatId,
44 | isRegenerate,
45 | regenerateMessageId,
46 | isNewChat,
47 | enableChatAssistant = true,
48 | } = params;
49 |
50 | const user = await getCurrentUser(supabase);
51 | const currentApp = await getAppBySlug(supabase, "/apps/chat");
52 |
53 | if (!user) {
54 | return new Response("Unauthorized", { status: 401 });
55 | }
56 |
57 | const lastMessage = messages[messages.length - 1];
58 |
59 | if (!isRegenerate) {
60 | if (isNewChat && currentApp) {
61 | await createNewChat(supabase, {
62 | id: chatId,
63 | app_id: currentApp.id,
64 | name: lastMessage.content,
65 | });
66 | await createNewChatMember(supabase, {
67 | chat_id: chatId,
68 | member_id: user.id,
69 | });
70 | }
71 | await createNewMessage(supabase, {
72 | chat_id: chatId,
73 | content: lastMessage.content,
74 | role: "user",
75 | id: lastMessage.id,
76 | });
77 | } else if (regenerateMessageId) {
78 | const fromMessage = await getMessageById(supabase, regenerateMessageId);
79 | if (fromMessage?.created_at) {
80 | await deleteMessagesFrom(supabase, chatId, fromMessage.created_at);
81 | }
82 | }
83 |
84 | if (!enableChatAssistant) {
85 | return new Response(null, {
86 | status: 200,
87 | headers: {
88 | "Content-Type": "application/json",
89 | "should-redirect-to-new-chat": "true",
90 | },
91 | });
92 | }
93 |
94 | log.debug("Start stream text");
95 | const response = await streamText({
96 | model: openai(model),
97 | temperature,
98 | messages: messages.map((message: Message) =>
99 | pick(message, "content", "role")
100 | ),
101 | maxTokens,
102 | topP,
103 | frequencyPenalty,
104 | presencePenalty,
105 | onFinish: async ({ text }) => {
106 | await createNewMessage(supabase, {
107 | chat_id: chatId,
108 | content: text,
109 | role: "assistant",
110 | });
111 | },
112 | });
113 | log.debug("End stream text");
114 |
115 | return response.toAIStreamResponse();
116 | });
117 |
--------------------------------------------------------------------------------
/app/apps/chat/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Metadata } from "next";
3 | import { cookies } from "next/headers";
4 | import { redirect } from "next/navigation";
5 | import { Message } from "ai";
6 |
7 | import {
8 | CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE,
9 | DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT,
10 | } from "@/lib/contants";
11 | import { getAppBySlug } from "@/lib/db/apps";
12 | import { getChatMembers } from "@/lib/db/chat-members";
13 | import { getChatById, getChats } from "@/lib/db/chats";
14 | import { getMessages } from "@/lib/db/message";
15 | import { getCurrentUser } from "@/lib/session";
16 | import { createClient } from "@/lib/supabase/server";
17 | import { ChatPanel } from "@/components/modules/apps/chat/ChatPanel";
18 | import { ChatParams } from "@/components/modules/apps/chat/types";
19 |
20 | export const runtime = "edge";
21 | export const preferredRegion = "home";
22 |
23 | export const metadata: Metadata = {
24 | title: "Chat",
25 | description:
26 | "Chat with your AI assistant to generate new ideas and get inspired.",
27 | };
28 |
29 | export default async function ChatPage({ params }: { params: { id: string } }) {
30 | const chatId = params.id;
31 |
32 | const cookieStore = cookies();
33 | const supabase = createClient(cookieStore);
34 | const user = await getCurrentUser(supabase);
35 | const currentApp = await getAppBySlug(supabase, "/apps/chat");
36 |
37 | if (!currentApp || !user) {
38 | return No app found
;
39 | }
40 |
41 | const chats = await getChats(supabase, currentApp.id);
42 |
43 | const dbMessages = await getMessages(supabase, chatId);
44 |
45 | const chatDetails = await getChatById(supabase, chatId);
46 | if (!chatDetails) {
47 | redirect("/apps/chat");
48 | }
49 | const chatParams = chatDetails?.settings as ChatParams | undefined;
50 | const isChatHost = chatDetails?.profile_id === user.id;
51 |
52 | const initialChatMessages: Message[] = dbMessages?.length
53 | ? dbMessages.map((message) => {
54 | return {
55 | id: message.id,
56 | role: message.role || "system",
57 | content: message.content || "",
58 | data: {
59 | profile_id: message.profile_id,
60 | chat_id: message.chat_id,
61 | chatBubleDirection:
62 | message.role === "user" && message.profile_id === user.id
63 | ? "end"
64 | : "start",
65 | },
66 | };
67 | })
68 | : [];
69 |
70 | if (chatParams?.description) {
71 | initialChatMessages.unshift({
72 | id: "description",
73 | role: "system",
74 | content: chatParams.description,
75 | });
76 | }
77 |
78 | const chatMembers = await getChatMembers(supabase, chatId);
79 |
80 | const memberSidebarLayout = cookies().get(CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE);
81 |
82 | let defaultMemberSidebarLayout = DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT;
83 | if (memberSidebarLayout) {
84 | defaultMemberSidebarLayout = JSON.parse(memberSidebarLayout.value);
85 | }
86 |
87 | return (
88 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/app/apps/chat/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from "lucide-react";
2 |
3 | import { Separator } from "@/components/ui/Separator";
4 | import { Skeleton } from "@/components/ui/Skeleton";
5 | import { Heading2 } from "@/components/ui/typography";
6 |
7 | export default function Page() {
8 | return (
9 |
10 |
11 |
12 |
13 | GPT AI Assistant
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/app/apps/chat/page.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Metadata } from "next";
3 | import { cookies } from "next/headers";
4 | import { v4 as uuidv4 } from "uuid";
5 |
6 | import { getAppBySlug } from "@/lib/db/apps";
7 | import { getChats } from "@/lib/db/chats";
8 | import { getCurrentUser } from "@/lib/session";
9 | import { createClient } from "@/lib/supabase/server";
10 | import { ChatPanel } from "@/components/modules/apps/chat/ChatPanel";
11 |
12 | export const metadata: Metadata = {
13 | title: "Create a New Chat",
14 | };
15 |
16 | export default async function NewChatPage() {
17 | const chatId = uuidv4();
18 |
19 | const cookieStore = cookies();
20 | const supabase = createClient(cookieStore);
21 | const user = await getCurrentUser(supabase);
22 | const currentApp = await getAppBySlug(supabase, "/apps/chat");
23 |
24 | if (!currentApp || !user) {
25 | return No app found
;
26 | }
27 |
28 | const chats = await getChats(supabase, currentApp.id);
29 |
30 | return (
31 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/apps/layout.tsx:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 |
3 | import { getAppBySlug } from "@/lib/db/apps";
4 | import { getChats } from "@/lib/db/chats";
5 | import { getCurrentUser } from "@/lib/session";
6 | import { createClient } from "@/lib/supabase/server";
7 | import { MainLayout } from "@/components/ui/common/MainLayout";
8 | import { ChatHistory } from "@/components/modules/apps/chat/ChatHistory";
9 |
10 | interface AppLayoutProps {
11 | children: React.ReactNode;
12 | }
13 |
14 | export default async function AppLayout({ children }: AppLayoutProps) {
15 | const cookieStore = cookies();
16 | const supabase = createClient(cookieStore);
17 | const user = await getCurrentUser(supabase);
18 | const currentApp = await getAppBySlug(supabase, "/apps/chat");
19 |
20 | if (!currentApp || !user) {
21 | return No app found
;
22 | }
23 |
24 | const chats = await getChats(supabase, currentApp.id);
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {children}
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/app/apps/page.tsx:
--------------------------------------------------------------------------------
1 | import { Heading1 } from "@/components/ui/typography";
2 |
3 | export const runtime = "edge";
4 |
5 | export default function Apps() {
6 | return Apps ;
7 | }
8 |
--------------------------------------------------------------------------------
/app/docs/page.tsx:
--------------------------------------------------------------------------------
1 | export default async function Docs() {
2 | return docs
;
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import url('./styles/custom.css');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | .drawer-toggle:checked ~ .drawer-side {
8 | backdrop-filter: blur(5px);
9 | }
10 |
11 | @media screen and (-webkit-min-device-pixel-ratio:0) {
12 | select,
13 | textarea,
14 | input {
15 | font-size: 16px !important;
16 | }
17 | }
18 |
19 | @layer base {
20 | :root {
21 | --background: 0 0% 100%;
22 | --foreground: 240 10% 3.9%;
23 | --card: 0 0% 100%;
24 | --card-foreground: 240 10% 3.9%;
25 | --popover: 0 0% 100%;
26 | --popover-foreground: 240 10% 3.9%;
27 | --primary: 262.1 83.3% 57.8%;
28 | --primary-foreground: 210 20% 98%;
29 | --secondary: 240 4.8% 95.9%;
30 | --secondary-foreground: 240 5.9% 10%;
31 | --muted: 240 4.8% 95.9%;
32 | --muted-foreground: 240 3.8% 46.1%;
33 | --accent: 240 4.8% 95.9%;
34 | --accent-foreground: 240 5.9% 10%;
35 | --destructive: 0 84.2% 60.2%;
36 | --destructive-foreground: 0 0% 98%;
37 | --border: 240 5.9% 90%;
38 | --input: 240 5.9% 90%;
39 | --ring: 262.1 83.3% 57.8%;
40 | --radius: 0.5rem;
41 | }
42 |
43 | .dark {
44 | --background: 20 14.3% 4.1%;
45 | --foreground: 0 0% 95%;
46 | --card: 24 9.8% 10%;
47 | --card-foreground: 0 0% 95%;
48 | --popover: 0 0% 9%;
49 | --popover-foreground: 0 0% 95%;
50 | --primary: 263.4 70% 50.4%;
51 | --primary-foreground: 210 20% 98%;
52 | --secondary: 240 3.7% 15.9%;
53 | --secondary-foreground: 0 0% 98%;
54 | --muted: 0 0% 15%;
55 | --muted-foreground: 240 5% 64.9%;
56 | --accent: 12 6.5% 15.1%;
57 | --accent-foreground: 0 0% 98%;
58 | --destructive: 0 62.8% 30.6%;
59 | --destructive-foreground: 0 85.7% 97.3%;
60 | --border: 240 3.7% 15.9%;
61 | --input: 240 3.7% 15.9%;
62 | --ring: 263.4 70% 50.4%;
63 | }
64 | }
65 |
66 | @layer base {
67 | * {
68 | @apply border-border;
69 | }
70 | body {
71 | @apply bg-background text-foreground;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { AxiomWebVitals } from "next-axiom";
2 |
3 | import "./globals.css";
4 |
5 | import { Metadata, Viewport } from "next";
6 | import { Analytics } from "@vercel/analytics/react";
7 | import { GeistMono } from "geist/font/mono";
8 | import { GeistSans } from "geist/font/sans";
9 | import { ThemeProvider } from "next-themes";
10 |
11 | import { siteConfig } from "@/config/site";
12 | import { Toaster } from "@/components/ui/Toaster";
13 |
14 | export const metadata: Metadata = {
15 | title: {
16 | default: siteConfig.name,
17 | template: `%s - ${siteConfig.name}`,
18 | },
19 | description: siteConfig.description,
20 | icons: {
21 | icon: "/favicon.ico",
22 | shortcut: "/favicon-16x16.png",
23 | apple: "/apple-touch-icon.png",
24 | },
25 | };
26 |
27 | export const viewport: Viewport = {
28 | themeColor: [
29 | { media: "(prefers-color-scheme: light)", color: "white" },
30 | { media: "(prefers-color-scheme: dark)", color: "black" },
31 | ],
32 | };
33 |
34 | interface RootLayoutProps {
35 | children: React.ReactNode;
36 | }
37 |
38 | export default function RootLayout({ children }: RootLayoutProps) {
39 | return (
40 | <>
41 |
46 |
50 |
52 |
53 | {children}
54 |
55 |
56 |
57 |
58 |
59 |
60 | >
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next";
2 | import Link from "next/link";
3 | import { Play } from "lucide-react";
4 |
5 | import { siteConfig } from "@/config/site";
6 | import { cn } from "@/lib/utils";
7 | import { buttonVariants } from "@/components/ui/Button";
8 | import { MainLayout } from "@/components/ui/common/MainLayout";
9 | import { Heading1 } from "@/components/ui/typography";
10 | import { DescriptionHeadingText } from "@/components/modules/home/DescriptionHeadingText";
11 | import { FeatureItems } from "@/components/modules/home/FeatureItems";
12 | import { HeroBannerImage } from "@/components/modules/home/HeroBannerImage";
13 |
14 | export const metadata: Metadata = {
15 | title: siteConfig.name,
16 | description: siteConfig.description,
17 | };
18 |
19 | export const runtime = "edge";
20 |
21 | export default async function Home() {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {siteConfig.name}
31 |
32 |
33 |
34 |
43 |
Demo
44 |
45 |
46 | Get Started
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/app/profile/layout.tsx:
--------------------------------------------------------------------------------
1 | import { MainLayout } from "@/components/ui/common/MainLayout";
2 |
3 | interface AppLayoutProps {
4 | children: React.ReactNode;
5 | }
6 |
7 | export default function AppLayout({ children }: AppLayoutProps) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/profile/page.tsx:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 |
3 | import { getCurrentProfile } from "@/lib/db/profile";
4 | import { getCurrentUser } from "@/lib/session";
5 | import { createClient } from "@/lib/supabase/server";
6 | import { Header } from "@/components/modules/profile/Header";
7 | import { ProfileForm } from "@/components/modules/profile/ProfileForm";
8 | import { ProfileFormValues } from "@/components/modules/profile/type";
9 |
10 | export const dynamic = "force-dynamic";
11 |
12 | export const runtime = "edge";
13 |
14 | export default async function Profile() {
15 | const cookieStore = cookies();
16 | const supabase = createClient(cookieStore);
17 | const profile = await getCurrentProfile(supabase);
18 | const user = await getCurrentUser(supabase);
19 |
20 | if (!profile) {
21 | return null;
22 | }
23 |
24 | const { avatar_url, full_name, username, website } = profile;
25 | const profileFormValues: ProfileFormValues = {
26 | fullName: full_name || undefined,
27 | username: username || undefined,
28 | website: website || undefined,
29 | };
30 |
31 | return (
32 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/modules/apps/app-side-bar/AppSideBar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { cookies } from "next/headers";
3 |
4 | import { getApps } from "@/lib/db/apps";
5 | import { createClient } from "@/lib/supabase/server";
6 |
7 | import { AppSideBarList } from "./AppSideBarList";
8 |
9 | export const AppSideBar = async () => {
10 | const cookieStore = cookies();
11 | const supabase = await createClient(cookieStore);
12 | const apps = await getApps(supabase);
13 | return (
14 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/components/modules/apps/app-side-bar/AppSideBarItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 |
4 | import { App } from "@/lib/db";
5 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar";
6 |
7 | type AppSideBarItemProps = Pick<
8 | App,
9 | "name" | "slug" | "description" | "logo_url"
10 | >;
11 |
12 | export const AppSideBarItem = ({
13 | name,
14 | slug,
15 | description,
16 | logo_url: logoUrl,
17 | }: AppSideBarItemProps) => {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | {logoUrl ? (
25 |
26 | ) : null}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
{name}
34 | {description ? (
35 |
{description}
36 | ) : null}
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/components/modules/apps/app-side-bar/AppSideBarList.tsx:
--------------------------------------------------------------------------------
1 | import { App } from "@/lib/db";
2 |
3 | import { AppSideBarItem } from "./AppSideBarItem";
4 |
5 | type AppSideBarListProps = {
6 | apps: App[] | null;
7 | };
8 |
9 | export const AppSideBarList = ({ apps }: AppSideBarListProps) => {
10 | if (!apps?.length) {
11 | return No apps found
;
12 | }
13 |
14 | return (
15 |
16 | {apps.map((app) => {
17 | const { id, name, description, slug, logo_url } = app;
18 | return (
19 |
26 | );
27 | })}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/components/modules/apps/app-side-bar/AppSidebarSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Heading5 } from "@/components/ui/typography";
4 |
5 | type AppSidebarSectionProps = {
6 | title: string;
7 | children: React.ReactNode;
8 | };
9 |
10 | export const AppSidebarSection = ({
11 | title,
12 | children,
13 | }: AppSidebarSectionProps) => {
14 | return (
15 |
16 | {title}
17 | {children}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/components/modules/apps/app-side-bar/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./AppSideBar";
2 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/ChatForm.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { SendHorizonal } from "lucide-react";
3 | import { MentionsInputProps, SuggestionDataItem } from "react-mentions";
4 |
5 | import { Chat, ChatMemberProfile } from "@/lib/db";
6 | import { useProfileStore } from "@/lib/stores/profile";
7 | import { useEnterSubmit } from "@/hooks/useEnterSubmit";
8 | import { Button } from "@/components/ui/Button";
9 | import { ChatInput } from "@/components/ui/chat";
10 |
11 | import { MobileDrawerControl } from "./MobileDrawerControls";
12 |
13 | type ChatFormProps = {
14 | chatInput: string;
15 | chats: Chat[] | null;
16 | isChatStreamming: boolean;
17 | chatMembers: ChatMemberProfile[] | null;
18 | onSubmit: (e: React.FormEvent) => void;
19 | onInputChange: (
20 | e:
21 | | React.ChangeEvent
22 | | React.ChangeEvent
23 | ) => void;
24 | };
25 |
26 | export const ChatForm = ({
27 | chats,
28 | isChatStreamming,
29 | chatMembers,
30 | chatInput,
31 | onSubmit,
32 | onInputChange,
33 | }: ChatFormProps) => {
34 | const { formRef, onKeyDown } = useEnterSubmit();
35 | const currentProfile = useProfileStore((state) => state.profile);
36 |
37 | const mentionData: SuggestionDataItem[] = React.useMemo(() => {
38 | const mentionData = [{ id: "assistant", display: "Assistant" }];
39 |
40 | if (!chatMembers) return mentionData;
41 |
42 | chatMembers.forEach((member) => {
43 | if (!member.profiles) return;
44 | mentionData.push({
45 | id: member.profiles.id,
46 | display: member.profiles.username || "",
47 | });
48 | });
49 |
50 | return mentionData.filter((mention) => mention.id !== currentProfile?.id);
51 | }, [chatMembers, currentProfile?.id]);
52 |
53 | const handleOnChange: MentionsInputProps["onChange"] = (e) => {
54 | onInputChange({
55 | target: { value: e.target.value },
56 | } as React.ChangeEvent);
57 | };
58 |
59 | return (
60 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/ChatHistory.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | import { Chat } from "@/lib/db";
6 | import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
7 | import { Separator } from "@/components/ui/Separator";
8 | import { Paragraph } from "@/components/ui/typography";
9 |
10 | import { ChatHistoryItem } from "./ChatHistoryItem";
11 | import { NewChatButton } from "./NewChatButton";
12 |
13 | type ChatHistoryProps = {
14 | data: Chat[] | null;
15 | closeDrawer?: () => void;
16 | };
17 |
18 | export const ChatHistory = ({ data, closeDrawer }: ChatHistoryProps) => {
19 | const chatId = useChatIdFromPathName();
20 |
21 | return (
22 |
23 |
24 |
25 | Chat history
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {data?.length ? null : (
34 |
35 | No chats found
36 |
37 | )}
38 | {data?.map((chat) => {
39 | return (
40 |
46 | );
47 | })}
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/ChatHistoryDrawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { History } from "lucide-react";
5 | import { Drawer } from "vaul";
6 |
7 | import { Chat } from "@/lib/db";
8 | import { cn } from "@/lib/utils";
9 | import { Button } from "@/components/ui/Button";
10 |
11 | import { ChatHistory } from "./ChatHistory";
12 |
13 | type ChatHistoryDrawerProps = {
14 | data: Chat[] | null;
15 | };
16 |
17 | export const ChatHistoryDrawer = ({ data }: ChatHistoryDrawerProps) => {
18 | const [drawerOpen, setDrawerOpen] = React.useState(false);
19 |
20 | const onHistoryButtonClick = () => {
21 | setDrawerOpen(true);
22 | };
23 |
24 | const closeDrawer = React.useCallback(() => {
25 | setDrawerOpen(false);
26 | }, []);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/ChatHistoryItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { MessageCircle } from "lucide-react";
4 |
5 | import { Chat } from "@/lib/db";
6 | import { cn } from "@/lib/utils";
7 |
8 | import { DeleteChatAction } from "./DeleteChatAction";
9 | import { EditChatAction } from "./EditChatAction";
10 |
11 | type ChatHistoryItemProps = {
12 | chat: Chat;
13 | isActive: boolean;
14 | closeDrawer?: () => void;
15 | };
16 |
17 | export const ChatHistoryItem = ({
18 | chat,
19 | isActive,
20 | closeDrawer,
21 | }: ChatHistoryItemProps) => {
22 | const renderActionButtons = () => {
23 | return (
24 |
38 | );
39 | };
40 |
41 | return (
42 |
43 |
49 |
50 |
51 |
61 |
62 |
63 |
64 |
65 | {chat.name}
66 |
67 |
68 | {renderActionButtons()}
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/ChatLayout.tsx:
--------------------------------------------------------------------------------
1 | import { MainLayout } from "@/components/ui/common/MainLayout";
2 |
3 | interface ChatLayoutProps {
4 | children: React.ReactNode;
5 | leftSidebarElement: React.ReactNode;
6 | }
7 |
8 | export const ChatLayout = ({
9 | children,
10 | leftSidebarElement,
11 | }: ChatLayoutProps) => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 | {leftSidebarElement}
20 |
21 | {children}
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/DeleteChatAction.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useRouter } from "next/navigation";
3 | import { Loader, Trash2 } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
7 | import {
8 | AlertDialog,
9 | AlertDialogAction,
10 | AlertDialogCancel,
11 | AlertDialogContent,
12 | AlertDialogDescription,
13 | AlertDialogFooter,
14 | AlertDialogHeader,
15 | AlertDialogTitle,
16 | AlertDialogTrigger,
17 | } from "@/components/ui/AlertDialog";
18 | import { Button, buttonVariants } from "@/components/ui/Button";
19 | import { toast } from "@/components/ui/use-toast";
20 |
21 | import { deleteChat } from "./action";
22 | import { ChatActionProps } from "./types";
23 |
24 | export const DeleteChatAction = ({ chat, ...rest }: ChatActionProps) => {
25 | const [isAlertOpen, setIsAlertOpen] = React.useState(false);
26 | const [pendingDeleteChat, startDeleteChat] = React.useTransition();
27 | const { replace } = useRouter();
28 | const chatIdFromPathName = useChatIdFromPathName();
29 |
30 | const onDelete = (e: React.MouseEvent) => {
31 | e.preventDefault();
32 | startDeleteChat(async () => {
33 | try {
34 | await deleteChat(chat.id);
35 | toast({
36 | title: "Success",
37 | description: "Your chat has been deleted.",
38 | });
39 | if (chatIdFromPathName === chat.id) {
40 | replace("/apps/chat");
41 | }
42 | setIsAlertOpen(false);
43 | } catch (error) {
44 | toast({
45 | title: "Error",
46 | description: "Failed to delete chat. Please try again.",
47 | variant: "destructive",
48 | });
49 | }
50 | });
51 | };
52 |
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Are you sure you want to delete this chat?
64 |
65 |
66 | This action cannot be undone. This will permanently delete your
67 | chat.
68 |
69 |
70 |
71 | Cancel
72 |
80 | {pendingDeleteChat ? (
81 |
82 | ) : (
83 | "Delete"
84 | )}
85 |
86 |
87 |
88 |
89 | );
90 | };
91 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/EditChatAction.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Edit, Loader } from "lucide-react";
3 |
4 | import {
5 | AlertDialog,
6 | AlertDialogAction,
7 | AlertDialogCancel,
8 | AlertDialogContent,
9 | AlertDialogDescription,
10 | AlertDialogFooter,
11 | AlertDialogHeader,
12 | AlertDialogTitle,
13 | AlertDialogTrigger,
14 | } from "@/components/ui/AlertDialog";
15 | import { Button } from "@/components/ui/Button";
16 | import { Input } from "@/components/ui/Input";
17 | import { toast } from "@/components/ui/use-toast";
18 |
19 | import { updateChat } from "./action";
20 | import { ChatActionProps } from "./types";
21 |
22 | export const EditChatAction = ({ chat, ...rest }: ChatActionProps) => {
23 | const [isAlertOpen, setIsAlertOpen] = React.useState(false);
24 | const [pendingUpdateChat, startUpdateChat] = React.useTransition();
25 | const [inputValue, setInputValue] = React.useState(chat.name || "");
26 |
27 | const handleDelete = () => {
28 | startUpdateChat(async () => {
29 | try {
30 | await updateChat({
31 | id: chat.id,
32 | name: inputValue,
33 | });
34 | toast({
35 | title: "Success",
36 | description: "Your chat has been updated.",
37 | });
38 | setIsAlertOpen(false);
39 | } catch (error) {
40 | toast({
41 | title: "Error",
42 | description: "Failed to update chat. Please try again.",
43 | variant: "destructive",
44 | });
45 | }
46 | });
47 | };
48 |
49 | const onChange = (e: React.ChangeEvent) => {
50 | setInputValue(e.target.value);
51 | };
52 |
53 | const onKeyDown = (e: React.KeyboardEvent) => {
54 | if (e.key === "Enter") {
55 | e.preventDefault();
56 | handleDelete();
57 | }
58 | };
59 |
60 | const onEdit = (e: React.MouseEvent) => {
61 | e.stopPropagation();
62 | };
63 |
64 | const onDelete = (e: React.MouseEvent) => {
65 | e.preventDefault();
66 | handleDelete();
67 | };
68 |
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | Edit your chat title
79 |
80 |
81 |
82 |
89 |
90 |
91 | Cancel
92 |
93 | {pendingUpdateChat ? (
94 |
95 | ) : (
96 | "Update"
97 | )}
98 |
99 |
100 |
101 |
102 | );
103 | };
104 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Heading2 } from "@/components/ui/typography";
4 |
5 | export const Header = () => {
6 | return (
7 |
8 | GPT AI Assistant
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/MobileDrawerControls.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { PanelRight } from "lucide-react";
3 |
4 | import { Chat } from "@/lib/db";
5 | import { Button } from "@/components/ui/Button";
6 | import { SheetTrigger } from "@/components/ui/Sheet";
7 |
8 | import { ChatHistoryDrawer } from "./ChatHistoryDrawer";
9 |
10 | type MobileDrawerControlProps = {
11 | chats: Chat[] | null;
12 | };
13 |
14 | export const MobileDrawerControl = React.memo(function MobileDrawerControl({
15 | chats,
16 | }: MobileDrawerControlProps) {
17 | return (
18 | <>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | >
30 | );
31 | });
32 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/NewChatButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Link from "next/link";
5 | import { Plus } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 | import { buttonVariants } from "@/components/ui/Button";
9 |
10 | type NewChatButtonProps = {
11 | closeDrawer?: () => void;
12 | };
13 |
14 | export const NewChatButton = ({ closeDrawer }: NewChatButtonProps) => {
15 | return (
16 |
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/SystemPromptControl.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Message, UseChatHelpers } from "ai/react";
3 | import { useFormContext } from "react-hook-form";
4 |
5 | import { Button } from "@/components/ui/Button";
6 | import { Label } from "@/components/ui/Label";
7 | import {
8 | Popover,
9 | PopoverContent,
10 | PopoverTrigger,
11 | } from "@/components/ui/Popover";
12 | import { TextArea } from "@/components/ui/TextArea";
13 | import { Subtle } from "@/components/ui/typography";
14 |
15 | import { ChatParams } from "./types";
16 |
17 | type SystemPromptControlProps = Pick<
18 | UseChatHelpers,
19 | "setMessages" | "messages"
20 | >;
21 |
22 | export const SystemPromptControl = ({
23 | setMessages,
24 | messages,
25 | }: SystemPromptControlProps) => {
26 | const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
27 | const { getValues, setValue } = useFormContext();
28 | const formValues = getValues();
29 | const { description } = formValues;
30 | const [systemPromptInputValue, setSystemPromptInputValue] = React.useState<
31 | string | undefined
32 | >(description);
33 |
34 | const handlePopoverOpenChange = (isOpen: boolean) => {
35 | if (isOpen) {
36 | if (description !== systemPromptInputValue) {
37 | setSystemPromptInputValue(description);
38 | }
39 | }
40 |
41 | setIsPopoverOpen(isOpen);
42 | };
43 |
44 | const handleSystemPromptInputChange = (
45 | e: React.ChangeEvent
46 | ) => {
47 | setSystemPromptInputValue(e.target.value);
48 | };
49 |
50 | const handleSave = () => {
51 | if (!systemPromptInputValue) {
52 | return;
53 | }
54 |
55 | const systemMessage: Message = {
56 | role: "system",
57 | content: systemPromptInputValue,
58 | id: "system-prompt",
59 | };
60 |
61 | setMessages([systemMessage, ...messages]);
62 | setValue("description", systemPromptInputValue);
63 |
64 | setIsPopoverOpen(false);
65 | };
66 |
67 | return (
68 |
69 |
70 |
71 |
Description
72 |
73 |
74 | Edit
75 |
76 |
77 |
78 |
{description}
79 |
80 |
81 |
82 |
83 |
Set system prompt
84 |
85 | {`Set a custom system prompt to be prepended to the user's input. This is useful for giving the AI some context about the conversation.`}
86 |
87 |
88 |
89 |
95 |
96 | Done
97 |
98 |
99 |
100 |
101 |
102 | );
103 | };
104 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { cookies } from "next/headers";
5 |
6 | import { TablesUpdate } from "@/lib/db";
7 | import {
8 | deleteChat as deleteChatDb,
9 | updateChat as updateChatDb,
10 | } from "@/lib/db/chats";
11 | import { createClient } from "@/lib/supabase/server";
12 |
13 | export const deleteChat = async (id: string) => {
14 | const cookieStore = cookies();
15 | const supabase = createClient(cookieStore);
16 |
17 | try {
18 | await deleteChatDb(supabase, id);
19 | revalidatePath(`/apps`, "layout");
20 | } catch (error) {
21 | throw new Error("Failed to delete chat");
22 | }
23 | };
24 |
25 | export const updateChat = async (params: TablesUpdate<"chats">) => {
26 | const cookieStore = cookies();
27 | const supabase = createClient(cookieStore);
28 | const { id, ...rest } = params;
29 | if (!id) {
30 | throw new Error("Missing ID");
31 | }
32 |
33 | try {
34 | await updateChatDb(supabase, {
35 | id,
36 | ...rest,
37 | });
38 | revalidatePath(`/apps`, "layout");
39 | } catch (error) {
40 | throw new Error("Failed to update chat");
41 | }
42 | };
43 |
44 | export const revalidateChatLayout = async () => {
45 | revalidatePath("/apps", "layout");
46 | };
47 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/chat-members/AddMembersForm.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { zodResolver } from "@hookform/resolvers/zod";
3 | import { SubmitHandler, useForm } from "react-hook-form";
4 | import z from "zod";
5 |
6 | import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
7 | import { Button } from "@/components/ui/Button";
8 | import { InputField } from "@/components/ui/form/form-fields";
9 | import { useToast } from "@/components/ui/use-toast";
10 |
11 | import { addNewMember } from "./action";
12 | import { addMemberSchema } from "./schema";
13 |
14 | type AddMembersFormProps = {
15 | onCloseAddMemberPopover: () => void;
16 | };
17 |
18 | type AddMembersFormParams = z.infer;
19 |
20 | const defaultValues: AddMembersFormParams = {
21 | username: "",
22 | };
23 |
24 | export const AddMembersForm = ({
25 | onCloseAddMemberPopover,
26 | }: AddMembersFormProps) => {
27 | const { toast } = useToast();
28 | const chatId = useChatIdFromPathName();
29 | const [isPending, startTransition] = React.useTransition();
30 |
31 | const { handleSubmit, formState, register } = useForm({
32 | defaultValues: defaultValues,
33 | mode: "onChange",
34 | resolver: zodResolver(addMemberSchema),
35 | });
36 |
37 | const fieldProps = { register, formState };
38 |
39 | const onSubmit: SubmitHandler = (data) => {
40 | handleAddMember(data.username);
41 | };
42 |
43 | const handleAddMember = async (username: string) => {
44 | startTransition(async () => {
45 | try {
46 | await addNewMember(username, chatId);
47 | toast({
48 | title: "Success",
49 | description: `${username} has been added to this chat.`,
50 | });
51 | } catch (error) {
52 | toast({
53 | title: "Error",
54 | description:
55 | "Failed to add the member to this chat. Please try again.",
56 | variant: "destructive",
57 | });
58 | } finally {
59 | onCloseAddMemberPopover();
60 | }
61 | });
62 | };
63 |
64 | return (
65 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/chat-members/ChatMemberItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
4 | import { UserAvatar } from "@/components/ui/common/UserAvatar";
5 |
6 | import { DeleteMemberAction } from "./DeleteMemberAction";
7 |
8 | type ChatMemberItemProps = {
9 | id: string;
10 | username: string;
11 | avatarUrl: string | null;
12 | fullname?: string;
13 | removeable?: boolean;
14 | isOnline?: boolean;
15 | };
16 |
17 | export const ChatMemberItem = ({
18 | id,
19 | fullname,
20 | username,
21 | avatarUrl,
22 | removeable = false,
23 | isOnline,
24 | }: ChatMemberItemProps) => {
25 | const chatId = useChatIdFromPathName();
26 |
27 | return (
28 |
29 |
36 |
37 |
{fullname || username}
38 |
39 | {removeable && (
40 |
45 | )}
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/chat-members/ChatMembers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Plus } from "lucide-react";
5 |
6 | import { ChatMemberProfile } from "@/lib/db";
7 | import { useProfileStore } from "@/lib/stores/profile";
8 | import { Button } from "@/components/ui/Button";
9 | import {
10 | Popover,
11 | PopoverContent,
12 | PopoverTrigger,
13 | } from "@/components/ui/Popover";
14 | import { Separator } from "@/components/ui/Separator";
15 | import { Paragraph } from "@/components/ui/typography";
16 |
17 | import { ChatPanelProps } from "../ChatPanel";
18 | import { AddMembersForm } from "./AddMembersForm";
19 | import { ChatMemberItem } from "./ChatMemberItem";
20 |
21 | type ChatMembersProps = {
22 | data: ChatMemberProfile[] | null;
23 | isChatHost: ChatPanelProps["isChatHost"];
24 | closeDrawer?: () => void;
25 | };
26 |
27 | export const ChatMembers = ({ data, isChatHost }: ChatMembersProps) => {
28 | const [addMemberPopoverOpen, setAddMemberPopoverOpen] = React.useState(false);
29 | const currentProfile = useProfileStore((state) => state.profile);
30 |
31 | const handleAddMemberPopoverOpen = (isOpen: boolean) => {
32 | setAddMemberPopoverOpen(isOpen);
33 | };
34 |
35 | const closeAddMemberPopover = () => {
36 | setAddMemberPopoverOpen(false);
37 | };
38 |
39 | return (
40 |
41 |
42 |
Chat members
43 |
44 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {data?.length ? null : (
62 |
63 | No members in this chat.
64 |
65 | )}
66 | {data?.map((member) => {
67 | const { profiles, id } = member;
68 | if (!profiles) return null;
69 | let removeable = false;
70 | const isMemberChat =
71 | currentProfile && currentProfile.id !== profiles.id ? true : false;
72 |
73 | if (isChatHost && isMemberChat) {
74 | removeable = true;
75 | }
76 | return (
77 |
78 |
85 |
86 |
87 | );
88 | })}
89 |
90 |
91 | );
92 | };
93 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/chat-members/DeleteMemberAction.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AlertDialogProps } from "@radix-ui/react-alert-dialog";
3 | import { Loader, Trash2 } from "lucide-react";
4 |
5 | import { Chat, Profile } from "@/lib/db";
6 | import { cn } from "@/lib/utils";
7 | import {
8 | AlertDialog,
9 | AlertDialogAction,
10 | AlertDialogCancel,
11 | AlertDialogContent,
12 | AlertDialogDescription,
13 | AlertDialogFooter,
14 | AlertDialogHeader,
15 | AlertDialogTitle,
16 | AlertDialogTrigger,
17 | } from "@/components/ui/AlertDialog";
18 | import { Button, buttonVariants } from "@/components/ui/Button";
19 | import { useToast } from "@/components/ui/use-toast";
20 |
21 | import { deleteMember } from "./action";
22 |
23 | type ChatActionProps = {
24 | memberId: Profile["id"];
25 | memberUsername: Profile["username"];
26 | chatId: Chat["id"];
27 | } & AlertDialogProps;
28 |
29 | export const DeleteMemberAction = ({
30 | memberId,
31 | memberUsername,
32 | chatId,
33 | ...rest
34 | }: ChatActionProps) => {
35 | const { toast } = useToast();
36 | const [isAlertOpen, setIsAlertOpen] = React.useState(false);
37 | const [pendingDeleteMember, startDeleteMember] = React.useTransition();
38 |
39 | const handleRemoveMember = (
40 | e: React.MouseEvent
41 | ) => {
42 | e.preventDefault();
43 | startDeleteMember(async () => {
44 | try {
45 | await deleteMember(memberId, chatId);
46 | toast({
47 | title: "Success",
48 | description: `${memberUsername || "The member"} has been removed to this chat.`,
49 | });
50 | } catch (error) {
51 | toast({
52 | title: "Error",
53 | description:
54 | "Failed to remove the member to this chat. Please try again.",
55 | variant: "destructive",
56 | });
57 | } finally {
58 | setIsAlertOpen(false);
59 | }
60 | });
61 | };
62 |
63 | return (
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | Are you sure you want to remove{" "}
74 | "{memberUsername}"
75 | from this chat?
76 |
77 |
78 | This action cannot be undone.
79 |
80 |
81 |
82 | Cancel
83 |
91 | {pendingDeleteMember ? (
92 |
93 | ) : (
94 | "Delete"
95 | )}
96 |
97 |
98 |
99 |
100 | );
101 | };
102 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/chat-members/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { cookies } from "next/headers";
5 |
6 | import { createNewChatMember, deleteChatMember } from "@/lib/db/chat-members";
7 | import { getProfileByUsername } from "@/lib/db/profile";
8 | import { getCurrentUser } from "@/lib/session";
9 | import { createClient } from "@/lib/supabase/server";
10 |
11 | export const addNewMember = async (username: string, chatId: string) => {
12 | const cookieStore = cookies();
13 | const supabase = createClient(cookieStore);
14 | const user = await getCurrentUser(supabase);
15 |
16 | if (!user) {
17 | throw new Error("You must be logged in to create a chat");
18 | }
19 |
20 | const profile = await getProfileByUsername(supabase, username);
21 |
22 | if (!profile) {
23 | throw new Error("Profile not found");
24 | }
25 |
26 | const newMember = await createNewChatMember(supabase, {
27 | chat_id: chatId,
28 | member_id: profile.id,
29 | });
30 |
31 | if (!newMember) {
32 | throw new Error("Failed to add the member to this chat");
33 | }
34 |
35 | revalidatePath(`/apps/chat/${chatId}`);
36 |
37 | return newMember;
38 | };
39 |
40 | export const deleteMember = async (memberId: string, chatId: string) => {
41 | const cookieStore = cookies();
42 | const supabase = createClient(cookieStore);
43 | const user = await getCurrentUser(supabase);
44 |
45 | if (!user) {
46 | throw new Error("You must be logged in to delete a chat member");
47 | }
48 |
49 | try {
50 | await deleteChatMember(supabase, memberId);
51 | revalidatePath(`/apps/chat/${chatId}`);
52 | } catch (error) {
53 | throw new Error("Failed to remove the member from this chat");
54 | }
55 |
56 | return null;
57 | };
58 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/chat-members/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ChatMembers";
2 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/chat-members/schema.ts:
--------------------------------------------------------------------------------
1 | import z from "zod";
2 |
3 | export const addMemberSchema = z.object({
4 | username: z.string(),
5 | });
6 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/ControlSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { UseChatHelpers } from "ai/react";
3 | import { Loader } from "lucide-react";
4 | import { useFormContext } from "react-hook-form";
5 |
6 | import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
7 | import { Button } from "@/components/ui/Button";
8 | import { Separator } from "@/components/ui/Separator";
9 | import {
10 | SheetDescription,
11 | SheetHeader,
12 | SheetTitle,
13 | } from "@/components/ui/Sheet";
14 | import { toast } from "@/components/ui/use-toast";
15 |
16 | import { SystemPromptControl } from "../SystemPromptControl";
17 | import { ChatParams } from "../types";
18 | import { updateChatSettings } from "./action";
19 | import { models, types } from "./data/models";
20 | import { FrequencyPenaltySelector } from "./FrequencyPenaltySelector";
21 | import { MaxLengthSelector } from "./MaxLengthSelector";
22 | import { ModelSelector } from "./ModelSelector";
23 | import { PresencePenaltySelector } from "./PresencePenaltySelector";
24 | import { TemperatureSelector } from "./TemperatureSelector";
25 | import { TopPSelector } from "./TopPSelector";
26 |
27 | type ControlSidebarProps = Pick & {
28 | closeSidebarSheet?: () => void;
29 | isNewChat?: boolean;
30 | };
31 |
32 | export const ControlSidebar = ({
33 | setMessages,
34 | messages,
35 | closeSidebarSheet,
36 | isNewChat,
37 | }: ControlSidebarProps) => {
38 | const [pendingUpdateSettings, startUpdateSettings] = React.useTransition();
39 | const currentChatId = useChatIdFromPathName();
40 | const { getValues } = useFormContext();
41 |
42 | const onSave = () => {
43 | if (!currentChatId) {
44 | return;
45 | }
46 |
47 | const formValues = getValues();
48 | startUpdateSettings(async () => {
49 | try {
50 | updateChatSettings(currentChatId, formValues);
51 | toast({
52 | title: "Success",
53 | description: "Your chat settings have been saved.",
54 | });
55 | closeSidebarSheet?.();
56 | } catch (error) {
57 | toast({
58 | title: "Error",
59 | description: "Failed to save chat settings. Please try again.",
60 | variant: "destructive",
61 | });
62 | }
63 | });
64 | };
65 |
66 | return (
67 | <>
68 |
69 | Settings
70 |
71 | {`Combining these parameters allows you to fine-tune the AI's output to suit different use cases.`}
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | {!isNewChat && (
85 |
86 |
87 | {pendingUpdateSettings ? (
88 |
89 | ) : (
90 | "Save"
91 | )}
92 |
93 |
94 | )}
95 | >
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/ControlSidebarSheet.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Message, UseChatHelpers } from "ai/react";
3 | import { FormProvider, FormProviderProps } from "react-hook-form";
4 |
5 | import {
6 | CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE,
7 | MAX_CHAT_MEMBER_SIDEBAR_SIZE,
8 | MIN_CHAT_MEMBER_SIDEBAR_SIZE,
9 | } from "@/lib/contants";
10 | import { ChatMemberProfile } from "@/lib/db";
11 | import {
12 | ResizableHandle,
13 | ResizablePanel,
14 | ResizablePanelGroup,
15 | } from "@/components/ui/Resizable";
16 | import { SheetContent } from "@/components/ui/Sheet";
17 |
18 | import { ChatMembers } from "../chat-members";
19 | import { ChatPanelProps } from "../ChatPanel";
20 | import { ChatParams } from "../types";
21 | import { ControlSidebar } from "./ControlSidebar";
22 |
23 | type ControlSidebarSheetProps = {
24 | setMessages: UseChatHelpers["setMessages"];
25 | messages: Message[];
26 | chatMembers: ChatMemberProfile[] | null;
27 | closeSidebarSheet: () => void;
28 | isNewChat: ChatPanelProps["isNewChat"];
29 | isChatHost: ChatPanelProps["isChatHost"];
30 | formReturn: Omit, "children">;
31 | defaultMemberSidebarLayout: number[];
32 | };
33 |
34 | export const ControlSidebarSheet = React.memo(function ControlSidebarSheet({
35 | setMessages,
36 | messages,
37 | chatMembers,
38 | closeSidebarSheet,
39 | isNewChat,
40 | isChatHost,
41 | formReturn,
42 | defaultMemberSidebarLayout,
43 | }: ControlSidebarSheetProps) {
44 | const onLayout = (sizes: number[]) => {
45 | document.cookie = `${CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE}=${JSON.stringify(sizes)}`;
46 | };
47 |
48 | const renderControlSidebar = () => {
49 | return (
50 |
56 | );
57 | };
58 |
59 | return (
60 |
61 |
62 |
63 |
{renderControlSidebar()}
64 | {!isNewChat && (
65 |
66 |
67 |
68 | )}
69 |
70 |
71 |
72 | {isNewChat ? (
73 | renderControlSidebar()
74 | ) : (
75 |
76 |
77 |
78 | {renderControlSidebar()}
79 |
80 |
81 |
82 |
87 |
88 |
89 |
90 | )}
91 |
92 |
93 | );
94 | });
95 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/FrequencyPenaltySelector.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useFormContext } from "react-hook-form";
5 |
6 | import { SliderField } from "@/components/ui/form/form-fields";
7 | import {
8 | HoverCard,
9 | HoverCardContent,
10 | HoverCardTrigger,
11 | } from "@/components/ui/HoverCard";
12 |
13 | import { ChatParams } from "../types";
14 |
15 | export function FrequencyPenaltySelector() {
16 | const { control, formState, getValues } = useFormContext();
17 |
18 | return (
19 |
20 |
21 |
22 |
34 |
35 |
40 |
41 | Adjust this to encourage the AI to use less common words. A higher
42 | value like 1.2 makes it prefer unique words, while a lower value
43 | like 0.8 lets it use common words more often.
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/MaxLengthSelector.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useFormContext } from "react-hook-form";
5 |
6 | import { SliderField } from "@/components/ui/form/form-fields";
7 | import {
8 | HoverCard,
9 | HoverCardContent,
10 | HoverCardTrigger,
11 | } from "@/components/ui/HoverCard";
12 |
13 | import { ChatParams } from "../types";
14 |
15 | export function MaxLengthSelector() {
16 | const { control, formState, getValues } = useFormContext();
17 |
18 | return (
19 |
20 |
21 |
22 |
34 |
35 |
40 | {`This sets the maximum length of the AI's reply. Use it to limit how long the AI's response should be, helpful to keep responses concise.`}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/PresencePenaltySelector.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useFormContext } from "react-hook-form";
5 |
6 | import { SliderField } from "@/components/ui/form/form-fields";
7 | import {
8 | HoverCard,
9 | HoverCardContent,
10 | HoverCardTrigger,
11 | } from "@/components/ui/HoverCard";
12 |
13 | import { ChatParams } from "../types";
14 |
15 | export function PresencePenaltySelector() {
16 | const { control, formState, getValues } = useFormContext();
17 |
18 | return (
19 |
20 |
21 |
22 |
34 |
35 |
40 |
41 | This nudges the AI to include specific topics or words in its
42 | responses. Use a higher value like 1.2 to make sure it talks about
43 | certain things, or a lower value like 0.8 for more freedom in topic
44 | choice.
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/TemperatureSelector.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useFormContext } from "react-hook-form";
5 |
6 | import { SliderField } from "@/components/ui/form/form-fields/SliderField";
7 | import {
8 | HoverCard,
9 | HoverCardContent,
10 | HoverCardTrigger,
11 | } from "@/components/ui/HoverCard";
12 |
13 | import { ChatParams } from "../types";
14 |
15 | export function TemperatureSelector() {
16 | const { control, formState, getValues } = useFormContext();
17 |
18 | return (
19 |
20 |
21 |
22 |
34 |
35 |
40 | {`Think of it as the AI's "creativity knob." A higher value like 0.8 makes responses more creative and unpredictable, while a lower value like 0.2 makes responses more focused and consistent. Adjust it to control how imaginative or precise the AI's answers are.`}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/TopPSelector.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useFormContext } from "react-hook-form";
5 |
6 | import { SliderField } from "@/components/ui/form/form-fields";
7 | import {
8 | HoverCard,
9 | HoverCardContent,
10 | HoverCardTrigger,
11 | } from "@/components/ui/HoverCard";
12 |
13 | import { ChatParams } from "../types";
14 |
15 | export function TopPSelector() {
16 | const { control, formState, getValues } = useFormContext();
17 |
18 | return (
19 |
20 |
21 |
22 |
34 |
35 |
40 |
41 | It determines the randomness in AI responses. Higher values like 0.8
42 | mean more randomness, while lower values like 0.2 make responses
43 | more focused on a single idea or word.
44 |
45 |
46 | We generally recommend altering this or Creativity but not both.
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { cookies } from "next/headers";
4 |
5 | import { Chat, TablesUpdate } from "@/lib/db";
6 | import { getAppBySlug } from "@/lib/db/apps";
7 | import { updateChat } from "@/lib/db/chats";
8 | import { getCurrentUser } from "@/lib/session";
9 | import { createClient } from "@/lib/supabase/server";
10 |
11 | export const updateChatSettings = async (
12 | id: Chat["id"],
13 | params: TablesUpdate<"chats">["settings"]
14 | ) => {
15 | const cookieStore = cookies();
16 | const supabase = createClient(cookieStore);
17 | const user = await getCurrentUser(supabase);
18 | const currentApp = await getAppBySlug(supabase, "/apps/chat");
19 |
20 | if (!currentApp || !user) {
21 | throw new Error("You must be logged in to create a chat");
22 | }
23 |
24 | const currentProfileId = user.id;
25 |
26 | try {
27 | await updateChat(supabase, {
28 | id: id,
29 | settings: params,
30 | profile_id: currentProfileId,
31 | app_id: currentApp.id,
32 | });
33 | } catch (error) {
34 | throw new Error("Failed to save chat settings");
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/data/models.ts:
--------------------------------------------------------------------------------
1 | export const defaultSystemPrompt =
2 | "You are an AI assistant. You can answer questions, generate code snippets, and more.";
3 |
4 | export const types = ["GPT-3.5", "GPT-4"] as const;
5 |
6 | export type ModelType = (typeof types)[number];
7 |
8 | export interface Model {
9 | id: string;
10 | name: string;
11 | description: string;
12 | type: Type;
13 | }
14 |
15 | export const models: Model[] = [
16 | {
17 | id: "1",
18 | name: "gpt-3.5-turbo",
19 | description:
20 | "Most capable GPT-3.5 model and optimized for chat at 1/10th the cost of text-davinci-003. Will be updated with our latest model iteration 2 weeks after it is released.",
21 | type: "GPT-3.5",
22 | },
23 | {
24 | id: "2",
25 | name: "gpt-3.5-turbo-16k",
26 | description:
27 | " Same capabilities as the standard gpt-3.5-turbo model but with 4 times the context.",
28 | type: "GPT-3.5",
29 | },
30 | {
31 | id: "3",
32 | name: "gpt-3.5-turbo-0613",
33 | description:
34 | "Snapshot of gpt-3.5-turbo from June 13th 2023 with function calling data. Unlike gpt-3.5-turbo, this model will not receive updates, and will be deprecated 3 months after a new version is released.",
35 | type: "GPT-3.5",
36 | },
37 | {
38 | id: "4",
39 | name: "gpt-4",
40 | description:
41 | "More capable than any GPT-3.5 model, able to do more complex tasks, and optimized for chat. Will be updated with our latest model iteration 2 weeks after it is released.",
42 | type: "GPT-4",
43 | },
44 | {
45 | id: "5",
46 | name: "gpt-4-0613",
47 | description:
48 | "Snapshot of gpt-4 from June 13th 2023 with function calling data. Unlike gpt-4, this model will not receive updates, and will be deprecated 3 months after a new version is released.",
49 | type: "GPT-4",
50 | },
51 | {
52 | id: "6",
53 | name: "gpt-4-32k",
54 | description:
55 | "Same capabilities as the standard gpt-4 mode but with 4x the context length. Will be updated with our latest model iteration.",
56 | type: "GPT-4",
57 | },
58 | ];
59 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/control-side-bar/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ControlSidebar";
2 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | // create schema based on CompletionCreateParams from openai
4 | export const ChatParamSchema = z
5 | .object({
6 | model: z.string().optional(),
7 | description: z.string().optional(),
8 | temperature: z.array(z.number()).optional(),
9 | maxTokens: z.array(z.number()).optional(),
10 | topP: z.array(z.number()).optional(),
11 | presencePenalty: z.array(z.number()).optional(),
12 | frequencyPenalty: z.array(z.number()).optional(),
13 | })
14 | .strict();
15 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/types.ts:
--------------------------------------------------------------------------------
1 | import { AlertDialogProps } from "@radix-ui/react-alert-dialog";
2 | import { z } from "zod";
3 |
4 | import { Chat } from "@/lib/db";
5 |
6 | import { ChatParamSchema } from "./schema";
7 |
8 | export type ChatParams = z.infer;
9 |
10 | export type ChatActionProps = {
11 | chat: Chat;
12 | } & Omit;
13 |
--------------------------------------------------------------------------------
/components/modules/apps/chat/utils.ts:
--------------------------------------------------------------------------------
1 | import { ChatParams } from "./types";
2 |
3 | export const buildChatRequestParams = (formValues: ChatParams) => {
4 | const {
5 | model,
6 | temperature,
7 | topP,
8 | maxTokens,
9 | frequencyPenalty,
10 | presencePenalty,
11 | description,
12 | } = formValues;
13 | return {
14 | model,
15 | description,
16 | temperature: temperature?.[0],
17 | topP: topP?.[0],
18 | maxTokens: maxTokens?.[0],
19 | frequencyPenalty: frequencyPenalty?.[0],
20 | presencePenalty: presencePenalty?.[0],
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/components/modules/auth/LogoutButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { LogOut } from "lucide-react";
5 |
6 | import { Button } from "@/components/ui/Button";
7 |
8 | export default function LogoutButton() {
9 | return (
10 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/modules/auth/SocialLoginButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Provider } from "@supabase/supabase-js";
3 | import { Loader } from "lucide-react";
4 |
5 | import { getURL } from "@/config/site";
6 | import { createClient } from "@/lib/supabase/client";
7 | import { Button, ButtonProps } from "@/components/ui/Button";
8 | import { useToast } from "@/components/ui/use-toast";
9 |
10 | type SocialLoginButtonProps = ButtonProps & {
11 | provider: Provider;
12 | };
13 |
14 | export const SocialLoginButton = ({
15 | provider,
16 | children,
17 | ...rest
18 | }: SocialLoginButtonProps) => {
19 | const supabase = createClient();
20 | const [isLoading, setIsLoading] = React.useState(false);
21 | const { toast } = useToast();
22 |
23 | const signIn = async () => {
24 | setIsLoading(true);
25 | const { error } = await supabase.auth.signInWithOAuth({
26 | provider: provider,
27 | options: {
28 | redirectTo: `${getURL()}api/auth/callback`,
29 | },
30 | });
31 |
32 | if (error) {
33 | setIsLoading(false);
34 | return toast({
35 | title: "Error",
36 | description: "Your email or password is incorrect. Please try again.",
37 | variant: "destructive",
38 | });
39 | }
40 | };
41 |
42 | return (
43 |
50 | {isLoading ? : children}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/components/modules/auth/SocialLoginOptions.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Github } from "lucide-react";
3 |
4 | import { CustomIcon } from "@/components/ui/CustomIcon";
5 |
6 | import { SocialLoginButton } from "./SocialLoginButton";
7 |
8 | export const SocialLoginOptions = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/modules/auth/schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | const credentialAuthObject = {
4 | email: z.string().email(),
5 | password: z.string().min(6, "Password must be at least 6 characters long."),
6 | };
7 |
8 | export const credentialAuthSchema = z.object({
9 | ...credentialAuthObject,
10 | });
11 |
12 | export const registerProfileSchema = z
13 | .object({
14 | ...credentialAuthObject,
15 | fullName: z.string().optional(),
16 | confirmPassword: z.string(),
17 | })
18 | .refine((data) => data.password === data.confirmPassword, {
19 | message: "Passwords do not match.",
20 | path: ["confirmPassword"],
21 | });
22 |
--------------------------------------------------------------------------------
/components/modules/home/DescriptionHeadingText.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { motion } from "framer-motion";
5 |
6 | export function DescriptionHeadingText() {
7 | const text =
8 | "A feature-rich, highly customizable AI Chatbot Template, powered by Next.js and Supabase.";
9 | const [displayedText, setDisplayedText] = React.useState("");
10 | const [i, setI] = React.useState(0);
11 |
12 | React.useEffect(() => {
13 | const typingEffect = setInterval(() => {
14 | if (i < text.length) {
15 | setDisplayedText((prevState) => prevState + text.charAt(i));
16 | setI(i + 1);
17 | } else {
18 | clearInterval(typingEffect);
19 | }
20 | }, 50);
21 |
22 | return () => {
23 | clearInterval(typingEffect);
24 | };
25 | }, [i]);
26 |
27 | return (
28 |
29 |
35 | {displayedText
36 | ? displayedText
37 | : "A feature-rich, highly customizable AI Chatbot Template, empowered by Next.js x Supabase."}
38 |
39 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/modules/home/HeroBannerImage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Image from "next/image";
5 | import { motion } from "framer-motion";
6 |
7 | import { useActiveThemeColor } from "@/hooks/useActiveTheme";
8 |
9 | export const HeroBannerImage = () => {
10 | const [isMounted, setIsMounted] = React.useState(false);
11 | const theme = useActiveThemeColor();
12 | React.useEffect(() => {
13 | setIsMounted(true);
14 | }, []);
15 |
16 | if (!isMounted) return
;
17 |
18 | const getImageSrc = () => {
19 | if (theme === "dark") {
20 | return "/featured-dark.jpg";
21 | }
22 | return "/featured.jpg";
23 | };
24 |
25 | return (
26 |
33 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/components/modules/profile/AccountDropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Link from "next/link";
5 | import { Loader, User } from "lucide-react";
6 |
7 | import { getCurrentProfile } from "@/lib/db/profile";
8 | import { useProfileStore } from "@/lib/stores/profile";
9 | import { createClient } from "@/lib/supabase/client";
10 | import { cn } from "@/lib/utils";
11 | import { Button, buttonVariants } from "@/components/ui/Button";
12 | import { UserAvatar } from "@/components/ui/common/UserAvatar";
13 | import {
14 | DropdownMenu,
15 | DropdownMenuContent,
16 | DropdownMenuGroup,
17 | DropdownMenuItem,
18 | DropdownMenuSeparator,
19 | DropdownMenuTrigger,
20 | } from "@/components/ui/DropdownMenu";
21 |
22 | import LogoutButton from "../auth/LogoutButton";
23 |
24 | type AccountDropdownMenuProps = {
25 | userEmail?: string;
26 | };
27 |
28 | export const AccountDropdownMenu = ({
29 | userEmail,
30 | }: AccountDropdownMenuProps) => {
31 | const supabase = createClient();
32 | const [isLoading, setIsLoading] = React.useState();
33 | const [isMounted, setIsMounted] = React.useState(false);
34 |
35 | React.useEffect(() => {
36 | setIsMounted(true);
37 | }, []);
38 |
39 | const { profile, setProfile } = useProfileStore((state) => state);
40 |
41 | React.useEffect(() => {
42 | const fetchProfile = async () => {
43 | setIsLoading(true);
44 | const profile = await getCurrentProfile(supabase);
45 | setIsLoading(false);
46 | if (!profile) {
47 | return;
48 | }
49 | setProfile(profile);
50 | };
51 | fetchProfile();
52 | }, [setProfile, supabase]);
53 |
54 | if (!isMounted) {
55 | return null;
56 | }
57 |
58 | if (isLoading) {
59 | return (
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | if (!profile) {
67 | return (
68 |
77 | Signin
78 |
79 | );
80 | }
81 |
82 | const { username, avatar_url } = profile;
83 | const nameLabel = username || userEmail || "";
84 |
85 | return (
86 |
87 |
88 |
89 |
90 |
95 |
{nameLabel}
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Profile
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/components/modules/profile/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { User } from "@supabase/supabase-js";
3 |
4 | import { Profile } from "@/lib/db";
5 | import { UserAvatar } from "@/components/ui/common/UserAvatar";
6 | import { Subtle } from "@/components/ui/typography";
7 |
8 | type HeaderProps = {
9 | email: User["email"];
10 | fullName: Profile["full_name"];
11 | username: Profile["username"];
12 | avatarUrl: Profile["avatar_url"];
13 | website: Profile["website"];
14 | };
15 |
16 | export const Header = ({
17 | username,
18 | avatarUrl,
19 | email,
20 | fullName,
21 | }: HeaderProps) => {
22 | return (
23 |
24 |
30 |
31 |
32 | {fullName ?
{fullName}
: null}
33 | {username ? (
34 |
(@{username})
35 | ) : null}
36 |
37 | {email ?
{email} : null}
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/components/modules/profile/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { cookies } from "next/headers";
5 |
6 | import { getCurrentUser } from "@/lib/session";
7 | import { createClient } from "@/lib/supabase/server";
8 |
9 | import { ProfileFormValues } from "./type";
10 |
11 | export async function updateProfile({
12 | fullName,
13 | username,
14 | website,
15 | }: ProfileFormValues) {
16 | const cookieStore = cookies();
17 | const supabase = createClient(cookieStore);
18 | const user = await getCurrentUser(supabase);
19 |
20 | if (!user) {
21 | throw new Error("You must be logged in to update your profile");
22 | }
23 |
24 | try {
25 | const { error } = await supabase.from("profiles").upsert({
26 | id: user.id,
27 | full_name: fullName,
28 | username,
29 | website,
30 | updated_at: new Date().toISOString(),
31 | });
32 |
33 | if (error) {
34 | throw error;
35 | }
36 |
37 | revalidatePath("/profile");
38 | } catch (error) {
39 | throw error;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/components/modules/profile/schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | export const profileSchema = z.object({
4 | fullName: z.string().optional().or(z.literal("")),
5 | username: z.string().optional().or(z.literal("")),
6 | website: z.string().optional().or(z.literal("")),
7 | });
8 |
--------------------------------------------------------------------------------
/components/modules/profile/type.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | import { profileSchema } from "./schema";
4 |
5 | export type ProfileFormValues = z.infer;
6 |
--------------------------------------------------------------------------------
/components/navigation/NavigationBar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { cookies } from "next/headers";
3 | import { Menu } from "lucide-react";
4 |
5 | import { getCurrentUser } from "@/lib/session";
6 | import { createClient } from "@/lib/supabase/server";
7 | import { cn } from "@/lib/utils";
8 |
9 | import { AccountDropdownMenu } from "../modules/profile/AccountDropdownMenu";
10 | import { ThemeToggle } from "../theme";
11 | import { buttonVariants } from "../ui/Button";
12 | import { AppLogo } from "../ui/common/AppLogo";
13 | import { NavigationMainMenu } from "./NavigationMainMenu";
14 |
15 | export const NavigationBar = async () => {
16 | const cookieStore = cookies();
17 | const supabase = await createClient(cookieStore);
18 | const user = await getCurrentUser(supabase);
19 |
20 | return (
21 |
22 |
23 |
24 |
34 |
35 |
36 |
39 |
40 |
41 |
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/components/navigation/NavigationMainMenu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Github } from "lucide-react";
4 |
5 | import { siteConfig } from "@/config/site";
6 |
7 | import { buttonVariants } from "../ui/Button";
8 | import { CustomIcon } from "../ui/CustomIcon";
9 | import {
10 | NavigationMenu,
11 | NavigationMenuItem,
12 | NavigationMenuLink,
13 | NavigationMenuList,
14 | } from "../ui/NavigationMenu";
15 |
16 | export const NavigationMainMenu = () => {
17 | return (
18 |
19 |
20 |
21 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/components/navigation/SideBar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 |
4 | import { siteConfig } from "@/config/site";
5 |
6 | import { ThemeToggle } from "../theme";
7 | import { AppLogo } from "../ui/common/AppLogo";
8 | import { CustomIcon } from "../ui/CustomIcon";
9 | import { Separator } from "../ui/Separator";
10 |
11 | export const Sidebar = () => {
12 | return (
13 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/components/theme/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/components/ui/Button";
8 |
9 | export function ThemeToggle() {
10 | const { setTheme, theme } = useTheme();
11 |
12 | const toggleTheme = () => {
13 | setTheme(theme === "light" ? "dark" : "light");
14 | };
15 |
16 | return (
17 |
18 |
19 |
20 | Toggle theme
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/components/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ThemeToggle";
2 |
--------------------------------------------------------------------------------
/components/ui/Accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { ChevronDown } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
55 | {children}
56 |
57 | ));
58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
59 |
60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
61 |
--------------------------------------------------------------------------------
/components/ui/Avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { Loader } from "lucide-react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const buttonVariants = cva(
9 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
10 | {
11 | variants: {
12 | variant: {
13 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
16 | outline:
17 | "border border-input hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "underline-offset-4 hover:underline text-primary",
22 | },
23 | size: {
24 | default: "h-10 py-2 px-4",
25 | sm: "h-9 px-3 rounded-md",
26 | lg: "h-11 px-8 rounded-md",
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 | isLoading?: boolean;
41 | }
42 |
43 | const Button = React.forwardRef(
44 | (
45 | {
46 | className,
47 | variant,
48 | size,
49 | isLoading,
50 | children,
51 | asChild = false,
52 | ...props
53 | },
54 | ref
55 | ) => {
56 | const Comp = asChild ? Slot : "button";
57 | return (
58 |
63 | {isLoading ? : children}
64 |
65 | );
66 | }
67 | );
68 | Button.displayName = "Button";
69 |
70 | export { Button, buttonVariants };
71 |
--------------------------------------------------------------------------------
/components/ui/Card.tsx:
--------------------------------------------------------------------------------
1 | import 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 |
41 | ));
42 | CardTitle.displayName = "CardTitle";
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ));
54 | CardDescription.displayName = "CardDescription";
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ));
62 | CardContent.displayName = "CardContent";
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | CardFooter.displayName = "CardFooter";
75 |
76 | export {
77 | Card,
78 | CardHeader,
79 | CardFooter,
80 | CardTitle,
81 | CardDescription,
82 | CardContent,
83 | };
84 |
--------------------------------------------------------------------------------
/components/ui/CustomIcon.tsx:
--------------------------------------------------------------------------------
1 | import { LucideProps } from "lucide-react";
2 |
3 | export const CustomIcon = {
4 | google: (props: LucideProps) => (
5 |
14 |
30 |
31 |
32 |
33 |
34 |
35 | ),
36 | x: (props: LucideProps) => (
37 |
45 |
46 |
47 | ),
48 | };
49 |
--------------------------------------------------------------------------------
/components/ui/Flex.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { cva, VariantProps } from "class-variance-authority";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const flexVariants = cva("flex", {
9 | variants: {
10 | direction: {
11 | row: "flex-row",
12 | column: "flex-col",
13 | },
14 | justify: {
15 | start: "justify-start",
16 | end: "justify-end",
17 | center: "justify-center",
18 | between: "justify-between",
19 | around: "justify-around",
20 | evenly: "justify-evenly",
21 | },
22 | align: {
23 | start: "items-start",
24 | end: "items-end",
25 | center: "items-center",
26 | baseline: "items-baseline",
27 | stretch: "items-stretch",
28 | },
29 | wrap: {
30 | wrap: "flex-wrap",
31 | nowrap: "flex-nowrap",
32 | wrapReverse: "flex-wrap-reverse",
33 | },
34 | },
35 | });
36 |
37 | export interface FlexProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {}
40 |
41 | export const Flex = ({
42 | className,
43 | direction,
44 | justify,
45 | align,
46 | wrap,
47 | children,
48 | ...props
49 | }: FlexProps) => {
50 | return (
51 |
57 | {children}
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/components/ui/HoverCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ));
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent };
30 |
--------------------------------------------------------------------------------
/components/ui/Input.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { cva, VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | import { Label } from "./Label";
7 |
8 | const inputVariants = cva(
9 | `flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50`,
10 | {
11 | variants: {
12 | isError: {
13 | true: "border-red-500 dark:border-red-500",
14 | },
15 | },
16 | }
17 | );
18 |
19 | export interface InputProps
20 | extends React.InputHTMLAttributes {
21 | label?: string;
22 | helperText?: string;
23 | containerClassName?: string;
24 | }
25 |
26 | const Input = React.forwardRef<
27 | HTMLInputElement,
28 | InputProps & VariantProps
29 | >(
30 | (
31 | { className, label, helperText, containerClassName, isError, ...props },
32 | ref
33 | ) => {
34 | return (
35 |
41 | {label ?
{label} : null}
42 |
47 | {helperText ? (
48 |
51 | {helperText}
52 |
53 | ) : null}
54 |
55 | );
56 | }
57 | );
58 |
59 | Input.displayName = "Input";
60 |
61 | export { Input };
62 |
--------------------------------------------------------------------------------
/components/ui/Label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import 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 |
--------------------------------------------------------------------------------
/components/ui/Popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/components/ui/Resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DragHandleDots2Icon } from "@radix-ui/react-icons";
4 | import * as ResizablePrimitive from "react-resizable-panels";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | );
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel;
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean;
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | );
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
46 |
--------------------------------------------------------------------------------
/components/ui/ScrollArea.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/components/ui/Section.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { cva, VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | import { Flex } from "./Flex";
7 | import { Separator } from "./Separator";
8 | import { Heading4 } from "./typography";
9 |
10 | const sectionVariants = cva(`rounded-lg bg-white p-4 dark:bg-slate-900`, {
11 | variants: {
12 | compact: {
13 | true: "p-2",
14 | },
15 | shadow: {
16 | true: "shadow",
17 | },
18 | },
19 | });
20 |
21 | export type SectionProps = {
22 | title?: string | React.ReactNode;
23 | className?: string;
24 | rightElement?: React.ReactNode;
25 | leftElement?: React.ReactNode;
26 | children?: React.ReactNode;
27 | } & VariantProps;
28 |
29 | export const Section = ({
30 | title,
31 | compact,
32 | shadow = true,
33 | className,
34 | leftElement,
35 | rightElement,
36 | children,
37 | }: SectionProps) => {
38 | return (
39 |
40 | {title || rightElement || leftElement ? (
41 |
42 |
43 | {leftElement && {leftElement}
}
44 | {typeof title === "string" ? {title} : title}
45 |
46 | {rightElement && {rightElement}
}
47 |
48 | ) : null}
49 | {title ?
: null}
50 |
{children}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/components/ui/Separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | import { Label } from "./Label";
9 |
10 | export type SliderProps = React.ComponentPropsWithoutRef<
11 | typeof SliderPrimitive.Root
12 | > & {
13 | label?: string;
14 | isError?: boolean;
15 | helperText?: string;
16 | containerClassName?: string;
17 | };
18 |
19 | const Slider = React.forwardRef<
20 | React.ElementRef,
21 | SliderProps
22 | >(
23 | (
24 | {
25 | className,
26 | label,
27 | isError,
28 | helperText,
29 | containerClassName,
30 | defaultValue,
31 | onValueChange,
32 | ...props
33 | },
34 | ref
35 | ) => {
36 | const [value, setValue] = React.useState(defaultValue);
37 |
38 | const handleOnValueChange = (value: number[]) => {
39 | onValueChange?.(value);
40 | setValue(value);
41 | };
42 |
43 | return (
44 |
45 | {label ? (
46 |
47 | {label}
48 |
49 | {value}
50 |
51 |
52 | ) : null}
53 |
62 |
63 |
64 |
65 |
66 |
67 | {helperText ? (
68 |
74 | {helperText}
75 |
76 | ) : null}
77 |
78 | );
79 | }
80 | );
81 | Slider.displayName = SliderPrimitive.Root.displayName;
82 |
83 | export { Slider };
84 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/ui/Tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/components/ui/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { cva, VariantProps } from "class-variance-authority";
3 | import TextareaAutoResize, {
4 | TextareaAutosizeProps,
5 | } from "react-textarea-autosize";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | import { Label } from "./Label";
10 |
11 | export interface TextAreaProps
12 | extends React.TextareaHTMLAttributes {
13 | label?: string;
14 | helperText?: string;
15 | containerClassName?: string;
16 | }
17 |
18 | export const textAreaVariants = cva(
19 | "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
20 | {
21 | variants: {
22 | isError: {
23 | true: "border-red-500 dark:border-red-500",
24 | },
25 | },
26 | }
27 | );
28 |
29 | const TextArea = React.forwardRef<
30 | HTMLTextAreaElement,
31 | TextAreaProps & VariantProps & TextareaAutosizeProps
32 | >(
33 | (
34 | { className, label, helperText, isError, containerClassName, ...props },
35 | ref
36 | ) => {
37 | return (
38 |
41 | {label ?
{label} : null}
42 |
47 | {helperText ? (
48 |
{helperText}
49 | ) : null}
50 |
51 | );
52 | }
53 | );
54 |
55 | TextArea.displayName = "TextArea";
56 |
57 | export { TextArea };
58 |
--------------------------------------------------------------------------------
/components/ui/Toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useToast } from "@/components/ui/use-toast";
4 |
5 | import {
6 | Toast,
7 | ToastClose,
8 | ToastDescription,
9 | ToastProvider,
10 | ToastTitle,
11 | ToastViewport,
12 | } from "./Toast";
13 |
14 | export function Toaster() {
15 | const { toasts } = useToast();
16 |
17 | return (
18 |
19 | {toasts.map(function ({ id, title, description, action, ...props }) {
20 | return (
21 |
22 |
23 | {title && {title} }
24 | {description && (
25 | {description}
26 | )}
27 |
28 | {action}
29 |
30 |
31 | );
32 | })}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/ui/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import 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 |
--------------------------------------------------------------------------------
/components/ui/chat/ChatInput.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import {
5 | Mention,
6 | MentionsInput,
7 | MentionsInputProps,
8 | SuggestionDataItem,
9 | } from "react-mentions";
10 |
11 | import { cn } from "@/lib/utils";
12 | import { textAreaVariants } from "@/components/ui/TextArea";
13 |
14 | import { defaultStyle } from "./mention-input-default-style";
15 |
16 | type ChatTextAreaProps = Omit & {
17 | mentionData: SuggestionDataItem[];
18 | };
19 |
20 | export const ChatInput = ({ mentionData, ...rest }: ChatTextAreaProps) => {
21 | return (
22 |
32 | (
38 |
39 | {highlightedDisplay}
40 |
41 | )}
42 | className="bg-primary/40"
43 | />
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/components/ui/chat/ChatProfileHoverCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CalendarDays, Globe } from "lucide-react";
3 |
4 | import { Profile } from "@/lib/db";
5 | import { cn } from "@/lib/utils";
6 |
7 | import { Button } from "../Button";
8 | import { UserAvatar } from "../common/UserAvatar";
9 | import { HoverCard, HoverCardContent, HoverCardTrigger } from "../HoverCard";
10 | import { Subtle } from "../typography";
11 | import { ChatBubbleProps } from "./ChatBubble";
12 |
13 | type ProfileHoverCardProps = {
14 | direction?: ChatBubbleProps["direction"];
15 | profile: Profile;
16 | joinedDate?: string;
17 | children: React.ReactNode;
18 | };
19 |
20 | export const ChatProfileHoverCard = React.memo(function ChatProfileHoverCard({
21 | direction,
22 | profile,
23 | joinedDate,
24 | children,
25 | }: ProfileHoverCardProps) {
26 | return (
27 |
28 |
29 |
36 | {children}
37 |
38 |
39 |
40 |
41 |
45 |
46 |
47 |
{children}
48 | {profile.full_name ? (
49 |
50 |
51 | ({profile.full_name})
52 |
53 |
54 | ) : null}
55 |
56 | {profile.website ? (
57 |
70 | ) : null}
71 | {joinedDate ? (
72 |
73 |
74 |
75 | Joined on {new Date(joinedDate).toLocaleDateString()}
76 |
77 |
78 | ) : null}
79 |
80 |
81 |
82 |
83 | );
84 | });
85 |
--------------------------------------------------------------------------------
/components/ui/chat/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 |
--------------------------------------------------------------------------------
/components/ui/chat/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ChatBubble";
2 | export * from "./ChatList";
3 | export * from "./ChatInput";
4 |
--------------------------------------------------------------------------------
/components/ui/chat/mention-input-default-style.ts:
--------------------------------------------------------------------------------
1 | import { MAX_CHAT_INPUT_HEIGHT } from "@/lib/contants";
2 |
3 | export const defaultStyle = {
4 | control: {
5 | wordBreak: "break-word",
6 | maxHeight: MAX_CHAT_INPUT_HEIGHT,
7 | overflowY: "hidden",
8 | },
9 | "&multiLine": {
10 | highlighter: {
11 | padding: 8,
12 | border: "1px solid transparent",
13 | color: "hsl(var(--primary))",
14 | },
15 | input: {
16 | padding: 8,
17 | border: "1px solid hsl(var(--border))",
18 | borderRadius: "calc(var(--radius) - 2px)",
19 | maxHeight: MAX_CHAT_INPUT_HEIGHT,
20 | overflowY: "auto",
21 | paddingBottom: 48,
22 | outlineColor: "hsl(var(--primary))",
23 | },
24 | },
25 | suggestions: {
26 | list: {
27 | backgroundColor: "hsl(var(--background))",
28 | border: "1px solid hsl(var(--border))",
29 | },
30 | item: {
31 | backgroundColor: "hsl(var(--background))",
32 | color: "hsl(var(--foreground))",
33 | padding: "4px 16px",
34 | borderBottom: "1px solid hsl(var(--border))",
35 | "&focused": {
36 | backgroundColor: "hsl(var(--muted))",
37 | },
38 | },
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/components/ui/common/AppLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 |
5 | import { siteConfig } from "@/config/site";
6 |
7 | export const AppLogo = () => {
8 | return (
9 |
10 |
11 |
18 |
{siteConfig.name}
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/ui/common/ChatScrollAnchor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useInView } from "react-intersection-observer";
5 |
6 | import { useAtBottom } from "@/hooks/useAtBottom";
7 |
8 | interface ChatScrollAnchorProps {
9 | trackVisibility?: boolean;
10 | parentElement: HTMLElement | null;
11 | }
12 |
13 | export function ChatScrollAnchor({
14 | trackVisibility,
15 | parentElement,
16 | }: ChatScrollAnchorProps) {
17 | const isAtBottom = useAtBottom(0, parentElement ?? undefined);
18 | const { ref, entry, inView } = useInView({
19 | trackVisibility,
20 | delay: 100,
21 | // rootMargin: '0px 0px -150px 0px'
22 | });
23 |
24 | React.useEffect(() => {
25 | if (isAtBottom && trackVisibility && !inView) {
26 | entry?.target.scrollIntoView({
27 | block: "start",
28 | });
29 | }
30 | }, [inView, entry, isAtBottom, trackVisibility]);
31 |
32 | return
;
33 | }
34 |
--------------------------------------------------------------------------------
/components/ui/common/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import { NavigationBar } from "@/components/navigation/NavigationBar";
2 | import { Sidebar } from "@/components/navigation/SideBar";
3 |
4 | interface MainLayoutProps {
5 | children: React.ReactNode;
6 | }
7 |
8 | export const MainLayout = ({ children }: MainLayoutProps) => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | {children}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/ui/common/UserAvatar.tsx:
--------------------------------------------------------------------------------
1 | import { AvatarProps } from "@radix-ui/react-avatar";
2 | import { User } from "@supabase/supabase-js";
3 |
4 | import { Profile } from "@/lib/db";
5 | import { cn } from "@/lib/utils";
6 |
7 | import { Avatar, AvatarFallback, AvatarImage } from "../Avatar";
8 |
9 | type UserAvatarProps = {
10 | username: Profile["username"];
11 | avatarUrl?: Profile["avatar_url"];
12 | email?: User["email"];
13 | isOnline?: boolean;
14 | } & AvatarProps;
15 |
16 | export const UserAvatar = ({
17 | avatarUrl,
18 | username,
19 | email,
20 | isOnline,
21 | ...rest
22 | }: UserAvatarProps) => {
23 | const nameLabel = username || email || "";
24 | const fallback = nameLabel.slice(0, 1).toUpperCase();
25 | return (
26 |
27 |
28 | {avatarUrl ? : null}
29 | {fallback}
30 |
31 | {isOnline !== undefined && (
32 |
41 | )}
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/InputField/InputField.tsx:
--------------------------------------------------------------------------------
1 | import get from "lodash/get";
2 | import { FieldValues } from "react-hook-form";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { Input, InputProps } from "@/components/ui/Input";
6 |
7 | import { FormFieldProps } from "../types";
8 |
9 | type InputFieldProps = FormFieldProps &
10 | Omit & {
11 | onValueChange?: (value: string) => void;
12 | };
13 |
14 | export function InputField(props: InputFieldProps) {
15 | const { register, name, id, formState, containerClassName, ...rest } = props;
16 | const { errors } = formState;
17 | const error = get(errors, name);
18 | const errorText = error?.message;
19 |
20 | const renderHelperText = () => {
21 | if (errorText && typeof errorText === "string") {
22 | return errorText;
23 | }
24 | };
25 |
26 | const handleOnChange = (e: React.ChangeEvent) => {
27 | props.onValueChange?.(e.target.value);
28 | };
29 |
30 | return (
31 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/InputField/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./InputField";
2 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/SliderField/SliderField.tsx:
--------------------------------------------------------------------------------
1 | import get from "lodash/get";
2 | import { Controller, FieldValues } from "react-hook-form";
3 |
4 | import { Slider, SliderProps } from "@/components/ui/Slider";
5 |
6 | import { ControlledFormFieldProps } from "../types";
7 |
8 | type SliderFieldProps = ControlledFormFieldProps &
9 | Omit;
10 |
11 | export function SliderField(props: SliderFieldProps) {
12 | const { name, id, formState, min, max, control, ...rest } = props;
13 | const { errors } = formState;
14 | const error = get(errors, name);
15 | const errorText = error?.message;
16 |
17 | const renderHelperText = () => {
18 | if (errorText && typeof errorText === "string") {
19 | return errorText;
20 | }
21 | };
22 |
23 | return (
24 | (
28 |
38 | )}
39 | />
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/SliderField/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./SliderField";
2 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/TextAreaField/TextAreaField.tsx:
--------------------------------------------------------------------------------
1 | import get from "lodash/get";
2 | import { FieldValues } from "react-hook-form";
3 | import { TextareaAutosizeProps } from "react-textarea-autosize";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { TextArea, TextAreaProps } from "@/components/ui/TextArea";
7 |
8 | import { FormFieldProps } from "../types";
9 |
10 | type TextAreaFieldProps = FormFieldProps &
11 | Omit &
12 | TextareaAutosizeProps;
13 |
14 | export function TextAreaField(
15 | props: TextAreaFieldProps
16 | ) {
17 | const { register, name, id, formState, containerClassName, ...rest } = props;
18 | const { errors } = formState;
19 | const error = get(errors, name);
20 | const errorText = error?.message;
21 |
22 | const renderHelperText = () => {
23 | if (errorText && typeof errorText === "string") {
24 | return errorText;
25 | }
26 | };
27 |
28 | return (
29 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/TextAreaField/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./TextAreaField";
2 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./InputField";
2 | export * from "./SliderField";
3 | export * from "./TextAreaField";
4 | export * from "./types";
5 |
--------------------------------------------------------------------------------
/components/ui/form/form-fields/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Control,
3 | FieldPath,
4 | FieldValues,
5 | FormState,
6 | UseFormRegister,
7 | } from "react-hook-form";
8 |
9 | export type FormFieldProps = {
10 | register: UseFormRegister;
11 | formState: FormState;
12 | name: FieldPath;
13 | };
14 |
15 | export type ControlledFormFieldProps = {
16 | formState: FormState;
17 | name: FieldPath;
18 | control: Control;
19 | };
20 |
--------------------------------------------------------------------------------
/components/ui/typography/Blockquote.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Blockquote({ text, className, children }: TypographyProps) {
6 | return (
7 |
13 | {children || text}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/ui/typography/Heading1.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Heading1({ text, className, children }: TypographyProps) {
6 | return (
7 |
13 | {children || text}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/ui/typography/Heading2.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Heading2({ text, className, children }: TypographyProps) {
6 | return (
7 |
13 | {children || text}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/ui/typography/Heading3.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Heading3({ text, className, children }: TypographyProps) {
6 | return (
7 |
13 | {children || text}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/ui/typography/Heading4.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Heading4({ text, className, children }: TypographyProps) {
6 | return (
7 |
13 | {children || text}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/ui/typography/Heading5.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Heading5({ text, className, children }: TypographyProps) {
6 | return (
7 |
13 | {children || text}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/ui/typography/Paragraph.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Paragraph({ text, className, children }: TypographyProps) {
6 | return (
7 |
8 | {children || text}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/components/ui/typography/Subtle.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | import { TypographyProps } from "./types";
4 |
5 | export function Subtle({ text, className, children }: TypographyProps) {
6 | return (
7 |
8 | {children || text}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/components/ui/typography/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Blockquote";
2 | export * from "./Heading1";
3 | export * from "./Heading2";
4 | export * from "./Heading3";
5 | export * from "./Heading4";
6 | export * from "./Heading5";
7 | export * from "./Paragraph";
8 | export * from "./Subtle";
9 |
--------------------------------------------------------------------------------
/components/ui/typography/types.ts:
--------------------------------------------------------------------------------
1 | export type TypographyProps = {
2 | text?: string;
3 | className?: string;
4 | children?: React.ReactNode;
5 | };
6 |
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/env.mjs";
2 |
3 | export type SiteConfig = typeof siteConfig;
4 |
5 | export const siteConfig = {
6 | name: "AI Fusion Kit",
7 | description:
8 | "A feature-rich, highly customizable AI Chatbot Template, empowered by Next.js.",
9 | links: {
10 | x: "https://twitter.com/nphivu414",
11 | github: "https://github.com/nphivu414/ai-fusion-kit",
12 | docs: "",
13 | },
14 | };
15 |
16 | export const getURL = () => {
17 | let url = env.NEXT_PUBLIC_APP_URL || "http://localhost:3000/";
18 | // Make sure to include `https://` when not localhost.
19 | url = url.includes("http") ? url : `https://${url}`;
20 | // Make sure to include a trailing `/`.
21 | url = url.charAt(url.length - 1) === "/" ? url : `${url}/`;
22 | return url;
23 | };
24 |
--------------------------------------------------------------------------------
/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs"
2 | import { z } from "zod"
3 |
4 | export const env = createEnv({
5 | server: {
6 | OPENAI_API_KEY: z.string().min(1),
7 | NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
8 | NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
9 | },
10 | client: {
11 | NEXT_PUBLIC_APP_URL: z.string().min(1),
12 | NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
13 | NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
14 | },
15 | runtimeEnv: {
16 | OPENAI_API_KEY: process.env.OPENAI_API_KEY,
17 | NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
18 | NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
19 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
20 | },
21 | })
22 |
--------------------------------------------------------------------------------
/hooks/useActiveTheme.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes"
2 |
3 | export const useActiveThemeColor = () => {
4 | const { theme, systemTheme } = useTheme()
5 |
6 | if (theme === "dark" || (theme === "system" && systemTheme === "dark")) {
7 | return "dark"
8 | }
9 |
10 | return "light"
11 | }
--------------------------------------------------------------------------------
/hooks/useAtBottom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useAtBottom(offset = 0, element?: HTMLElement) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | let isAtBottom =
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset;
11 | if (element) {
12 | isAtBottom =
13 | element.scrollTop + element.offsetHeight >=
14 | element.scrollHeight - offset;
15 | }
16 | setIsAtBottom(isAtBottom);
17 | };
18 |
19 | if (!element) {
20 | window.addEventListener("scroll", handleScroll);
21 | } else {
22 | element.addEventListener("scroll", handleScroll);
23 | }
24 |
25 | handleScroll();
26 |
27 | return () => {
28 | if (!element) {
29 | window.removeEventListener("scroll", handleScroll);
30 | } else {
31 | element.removeEventListener("scroll", handleScroll);
32 | }
33 | };
34 | }, [element, offset]);
35 |
36 | return isAtBottom;
37 | }
38 |
--------------------------------------------------------------------------------
/hooks/useChatIdFromPathName.tsx:
--------------------------------------------------------------------------------
1 | import { usePathname } from "next/navigation";
2 |
3 | export const useChatIdFromPathName = () => {
4 | const pathname = usePathname();
5 |
6 | if (pathname.indexOf("/apps/chat") === -1) {
7 | return "";
8 | }
9 |
10 | const chatId = pathname.split("/").pop() || "";
11 | return chatId;
12 | };
13 |
--------------------------------------------------------------------------------
/hooks/useCopyToClipboard.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 = React.useCallback(
15 | (value: string) => {
16 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
17 | return;
18 | }
19 |
20 | if (!value) {
21 | return;
22 | }
23 |
24 | navigator.clipboard.writeText(value).then(() => {
25 | setIsCopied(true);
26 |
27 | setTimeout(() => {
28 | setIsCopied(false);
29 | }, timeout);
30 | });
31 | },
32 | [timeout]
33 | );
34 |
35 | return { isCopied, copyToClipboard };
36 | }
37 |
--------------------------------------------------------------------------------
/hooks/useEnterSubmit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject } from "react";
2 |
3 | type KeyboardEvent =
4 | | React.KeyboardEvent
5 | | React.KeyboardEvent;
6 |
7 | export function useEnterSubmit(): {
8 | formRef: RefObject;
9 | onKeyDown: (event: KeyboardEvent) => void;
10 | } {
11 | const formRef = useRef(null);
12 |
13 | const handleKeyDown = (event: KeyboardEvent): void => {
14 | if (
15 | event.key === "Enter" &&
16 | !event.shiftKey &&
17 | !event.nativeEvent.isComposing
18 | ) {
19 | formRef.current?.requestSubmit();
20 | event.preventDefault();
21 | }
22 | };
23 |
24 | return { formRef, onKeyDown: handleKeyDown };
25 | }
26 |
--------------------------------------------------------------------------------
/hooks/useMutationObserver.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | export const useMutationObserver = (
4 | ref: React.MutableRefObject,
5 | callback: MutationCallback,
6 | options = {
7 | attributes: true,
8 | characterData: true,
9 | childList: true,
10 | subtree: true,
11 | }
12 | ) => {
13 | React.useEffect(() => {
14 | if (ref.current) {
15 | const observer = new MutationObserver(callback)
16 | observer.observe(ref.current, options)
17 | return () => observer.disconnect()
18 | }
19 | }, [ref, callback, options])
20 | }
21 |
--------------------------------------------------------------------------------
/hooks/usePrevious.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const usePrevious = <_,T>(value: T) => {
4 | const ref = React.useRef();
5 | React.useEffect(() => {
6 | ref.current = value;
7 | }, [value]);
8 | return ref.current;
9 | }
--------------------------------------------------------------------------------
/hooks/useSubscribeChatMessages.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { RealtimePresenceState } from "@supabase/supabase-js";
3 | import { Message } from "ai";
4 | import { validate } from "uuid";
5 |
6 | import { createClient } from "@/lib/supabase/client";
7 |
8 | type ChatMemberStatus = {
9 | userId: string;
10 | onlineAt: string;
11 | };
12 |
13 | export type RealtimeChatMemberStatus = RealtimePresenceState;
14 |
15 | type UseSubscribeChatMessagesParams = {
16 | initialMessages: Message[];
17 | chatId: string;
18 | currentUserId?: string;
19 | newMessageInsertCallback: (messages: Message[]) => void;
20 | chatMemberPresenceCallback?: (presence: RealtimeChatMemberStatus) => void;
21 | chatMemberJoinCallback?: (presence: RealtimeChatMemberStatus[]) => void;
22 | chatMemberLeaveCallback?: (presence: RealtimeChatMemberStatus[]) => void;
23 | };
24 |
25 | export function useSubscribeChatMessages({
26 | initialMessages,
27 | chatId,
28 | currentUserId,
29 | newMessageInsertCallback,
30 | chatMemberJoinCallback,
31 | chatMemberLeaveCallback,
32 | chatMemberPresenceCallback,
33 | }: UseSubscribeChatMessagesParams) {
34 | const supabaseClient = createClient();
35 | const messageListRef = React.useRef([]);
36 |
37 | messageListRef.current = initialMessages;
38 |
39 | React.useEffect(() => {
40 | const subscription = supabaseClient
41 | .channel(`chat:${chatId}`)
42 | .on(
43 | "postgres_changes",
44 | { event: "INSERT", schema: "public", table: "messages" },
45 | (payload) => {
46 | if (payload.new.chat_id !== chatId) {
47 | return;
48 | }
49 | const newMessageId = payload.new.id;
50 | const newMessages = [
51 | ...messageListRef.current.filter(
52 | (message) => message.id !== newMessageId && validate(message.id)
53 | ),
54 | {
55 | id: payload.new.id,
56 | content: payload.new.content,
57 | role: payload.new.role,
58 | data: {
59 | profile_id: payload.new.profile_id,
60 | chat_id: payload.new.chat_id,
61 | },
62 | },
63 | ];
64 |
65 | newMessageInsertCallback(newMessages);
66 | }
67 | )
68 | .on("presence", { event: "sync" }, () => {
69 | const newState = subscription.presenceState();
70 | chatMemberPresenceCallback?.(newState);
71 | })
72 | .on("presence", { event: "join" }, ({ newPresences }) => {
73 | chatMemberJoinCallback?.(newPresences);
74 | })
75 | .on("presence", { event: "leave" }, ({ leftPresences }) => {
76 | chatMemberLeaveCallback?.(leftPresences);
77 | })
78 | .subscribe((status) => {
79 | if (status !== "SUBSCRIBED" || !currentUserId) {
80 | return;
81 | }
82 |
83 | subscription.track({
84 | userId: currentUserId,
85 | onlineAt: new Date().toISOString(),
86 | });
87 | });
88 |
89 | return () => {
90 | subscription.unsubscribe();
91 | };
92 | // eslint-disable-next-line react-hooks/exhaustive-deps
93 | }, [
94 | chatId,
95 | supabaseClient,
96 | currentUserId,
97 | chatMemberJoinCallback,
98 | chatMemberLeaveCallback,
99 | chatMemberPresenceCallback,
100 | newMessageInsertCallback,
101 | ]);
102 | }
103 |
--------------------------------------------------------------------------------
/lib/cache.ts:
--------------------------------------------------------------------------------
1 | export const CACHE_TTL = 3600;
2 |
3 | export const CACHE_KEYS = {
4 | CHATS: "chats",
5 | MESSAGES: "messages",
6 | APP_BY_SLUG: "appBySlug",
7 | };
8 |
--------------------------------------------------------------------------------
/lib/chat-input.ts:
--------------------------------------------------------------------------------
1 | import { CHAT_BOT_TRIGGER_WHITE_LIST } from "./contants";
2 |
3 | export const containsChatBotTrigger = (input: string) => {
4 | const whitelistPattern = CHAT_BOT_TRIGGER_WHITE_LIST.join("|");
5 | const pattern = new RegExp(`@\\[(${whitelistPattern})\\]\\(user:\\1\\)`, "i");
6 | return pattern.test(input);
7 | };
8 |
9 | export const getRawValueFromMentionInput = (input: string) => {
10 | const pattern = /@\[(.*?)\]\(user:(.*?)\)/i;
11 | return input.replace(pattern, "@$1");
12 | };
13 |
14 | export const isTaggedUserPattern = (
15 | input: string | number | true | (string | number)[]
16 | ) => {
17 | if (typeof input !== "string") {
18 | return false;
19 | }
20 | const pattern = /user:\w+/;
21 | return pattern.test(input);
22 | };
23 |
--------------------------------------------------------------------------------
/lib/contants.ts:
--------------------------------------------------------------------------------
1 | import GPTAvatar from "@/public/chat-gpt.jpeg";
2 |
3 | import { Profile } from "./db";
4 |
5 | export const APP_SLUGS = {
6 | CHAT: "/apps/chat",
7 | };
8 |
9 | export const CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE =
10 | "react-resizable-panels:member-sidebar-layout";
11 |
12 | export const DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT = [70, 30];
13 | export const MIN_CHAT_MEMBER_SIDEBAR_SIZE = 8;
14 | export const MAX_CHAT_MEMBER_SIDEBAR_SIZE = 70;
15 | export const CHAT_BOT_TRIGGER_WHITE_LIST = ["assistant"];
16 | export const MENTION_MARKUP = "@[__display__](user:__id__)";
17 | export const MENTION_TRIGGER = "@";
18 | export const MAX_CHAT_INPUT_HEIGHT = 210;
19 | export const AI_ASSISTANT_PROFILE: Profile = {
20 | id: "assistant",
21 | username: "Assissant",
22 | avatar_url: GPTAvatar.src,
23 | full_name: "Bot",
24 | billing_address: null,
25 | payment_method: null,
26 | updated_at: null,
27 | website: "https://openai.com",
28 | };
29 |
--------------------------------------------------------------------------------
/lib/db/apps.ts:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 | import { SupabaseClient } from "@supabase/supabase-js";
3 | import { Logger } from "next-axiom";
4 | import { LogLevel } from "next-axiom/dist/logger";
5 |
6 | import { App, Database } from ".";
7 | import { CACHE_KEYS, CACHE_TTL } from "../cache";
8 |
9 | const log = new Logger({
10 | logLevel: LogLevel.debug,
11 | args: {
12 | route: "[DB] Apps",
13 | },
14 | });
15 |
16 | export const getApps = async (supabase: SupabaseClient) => {
17 | log.info(`${getApps.name} called`);
18 | const { data, error, status } = await supabase.from("apps").select("*");
19 |
20 | if (error && status !== 406) {
21 | log.error(getApps.name, { error, status });
22 | return null;
23 | }
24 | log.info(`${getApps.name} fetched successfully`, { data });
25 |
26 | return data;
27 | };
28 |
29 | export const getAppBySlug = unstable_cache(
30 | async (supabase: SupabaseClient, slug: App["slug"]) => {
31 | log.info(`${getAppBySlug.name} called`, { slug });
32 | const { data, error, status } = await supabase
33 | .from("apps")
34 | .select("*")
35 | .eq("slug", slug)
36 | .single();
37 |
38 | if (error && status !== 406) {
39 | log.error(getAppBySlug.name, { error, status });
40 | return null;
41 | }
42 |
43 | log.info(`${getAppBySlug.name} fetched successfully`, { data });
44 | return data;
45 | },
46 | [CACHE_KEYS.APP_BY_SLUG],
47 | {
48 | revalidate: CACHE_TTL,
49 | }
50 | );
51 |
--------------------------------------------------------------------------------
/lib/db/chat-members.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient } from "@supabase/supabase-js";
2 | import { Logger } from "next-axiom";
3 | import { LogLevel } from "next-axiom/dist/logger";
4 |
5 | import { ChatMember, ChatMemberProfile, Database, TablesInsert } from ".";
6 |
7 | const log = new Logger({
8 | logLevel: LogLevel.debug,
9 | args: {
10 | route: "[DB] Chat Members",
11 | },
12 | });
13 |
14 | export const createNewChatMember = async (
15 | supabase: SupabaseClient,
16 | params: TablesInsert<"chat_members">
17 | ) => {
18 | log.info(`${createNewChatMember.name} called`, params);
19 |
20 | const { data, error } = await supabase
21 | .from("chat_members")
22 | .insert(params)
23 | .select();
24 |
25 | if (error) {
26 | log.error(createNewChatMember.name, { error });
27 | return null;
28 | }
29 |
30 | log.info(`${createNewChatMember.name} created successfully`, { data });
31 | return data;
32 | };
33 |
34 | export const getChatMembers = async (
35 | supabase: SupabaseClient,
36 | chatId: ChatMember["chat_id"]
37 | ) => {
38 | log.info(`${getChatMembers.name} called`, { chatId });
39 | const { data, error, status } = await supabase
40 | .from("chat_members")
41 | .select(`id, created_at,profiles (*)`)
42 | .eq("chat_id", chatId)
43 | .order("created_at", { ascending: true });
44 |
45 | if (error && status !== 406) {
46 | log.error(getChatMembers.name, { error, status });
47 | return null;
48 | }
49 |
50 | log.info(`${getChatMembers.name} fetched successfully`, { data });
51 | return data satisfies ChatMemberProfile[] | null;
52 | };
53 |
54 | export const deleteChatMember = async (
55 | supabase: SupabaseClient,
56 | id: ChatMember["id"]
57 | ) => {
58 | log.info(`${deleteChatMember.name} called`, { id });
59 |
60 | const { data, error } = await supabase
61 | .from("chat_members")
62 | .delete()
63 | .eq("id", id)
64 | .single();
65 |
66 | if (error) {
67 | log.error(deleteChatMember.name, { error });
68 | throw error;
69 | }
70 |
71 | log.info(`${deleteChatMember.name} deleted successfully`, { data });
72 | return data;
73 | };
74 |
--------------------------------------------------------------------------------
/lib/db/chats.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient } from "@supabase/supabase-js";
2 | import { Logger } from "next-axiom";
3 | import { LogLevel } from "next-axiom/dist/logger";
4 |
5 | import { Chat, Database, TablesInsert, TablesUpdate } from ".";
6 | import { getRawValueFromMentionInput } from "../chat-input";
7 |
8 | const log = new Logger({
9 | logLevel: LogLevel.debug,
10 | args: {
11 | route: "[DB] Chats",
12 | },
13 | });
14 |
15 | export const getChats = async (
16 | supabase: SupabaseClient,
17 | appId: Chat["app_id"]
18 | ) => {
19 | log.info(`${getChats.name} called`, { appId });
20 | const { data, error, status } = await supabase
21 | .from("chats")
22 | .select("*")
23 | .eq("app_id", appId)
24 | .order("created_at", { ascending: false });
25 |
26 | if (error && status !== 406) {
27 | log.error(getChats.name, { error, status });
28 | return null;
29 | }
30 |
31 | const formattedChat = data
32 | ? data.map((chat) => {
33 | return {
34 | ...chat,
35 | name: getRawValueFromMentionInput(chat?.name || ""),
36 | };
37 | })
38 | : null;
39 |
40 | log.info(`${getChats.name} fetched successfully`, { data: formattedChat });
41 | return formattedChat;
42 | };
43 |
44 | export const getChatById = async (
45 | supabase: SupabaseClient,
46 | id: Chat["id"]
47 | ) => {
48 | log.info(`${getChatById.name} called`, { id });
49 | const { data, error, status } = await supabase
50 | .from("chats")
51 | .select("*")
52 | .eq("id", id)
53 | .single();
54 |
55 | if (error && status !== 406) {
56 | log.error(getChatById.name, { error, status });
57 | return null;
58 | }
59 |
60 | log.info(`${getChatById.name} fetched successfully`, { data });
61 | return data;
62 | };
63 |
64 | export const createNewChat = async (
65 | supabase: SupabaseClient,
66 | params: TablesInsert<"chats">
67 | ) => {
68 | log.info(`${createNewChat.name} called`, params);
69 | const { data, error, status } = await supabase
70 | .from("chats")
71 | .insert([params])
72 | .select();
73 |
74 | if (error && status !== 406) {
75 | log.error(createNewChat.name, { error, status });
76 | return null;
77 | }
78 |
79 | log.info(`${createNewChat.name} created successfully`, { data });
80 | return data;
81 | };
82 |
83 | export const deleteChat = async (
84 | supabase: SupabaseClient,
85 | id: Chat["id"]
86 | ) => {
87 | log.info(`${deleteChat.name} called`, { id });
88 | const { data, error } = await supabase.from("chats").delete().eq("id", id);
89 |
90 | if (error) {
91 | log.error(deleteChat.name, { error });
92 | throw new Error(error.message);
93 | }
94 |
95 | log.info(`${deleteChat.name} deleted successfully`, { data });
96 | return data;
97 | };
98 |
99 | export const updateChat = async (
100 | supabase: SupabaseClient,
101 | params: TablesUpdate<"chats">
102 | ) => {
103 | log.info(`${updateChat.name} called`, params);
104 | const { id, ...rest } = params;
105 |
106 | if (!id) {
107 | log.error(`${updateChat.name} - Missing ID`, params);
108 | throw new Error("Missing ID");
109 | }
110 |
111 | const { data, error, status } = await supabase
112 | .from("chats")
113 | .update(rest)
114 | .eq("id", id)
115 | .select();
116 |
117 | if (error && status !== 406) {
118 | log.error(updateChat.name, { error, status });
119 | return null;
120 | }
121 |
122 | log.info(`${updateChat.name} updated successfully`, { data });
123 | return data?.[0];
124 | };
125 |
--------------------------------------------------------------------------------
/lib/db/index.ts:
--------------------------------------------------------------------------------
1 | import { Tables } from "./database.types";
2 |
3 | export * from "./database.types";
4 | export type Profile = Tables<"profiles">;
5 | export type App = Tables<"apps">;
6 | export type Chat = Tables<"chats">;
7 | export type ChatMember = Tables<"chat_members">;
8 | export type Message = Tables<"messages">;
9 | export type ChatMemberProfile = {
10 | id: ChatMember["id"];
11 | status?: "online" | "offline";
12 | profiles: Profile | null;
13 | created_at: ChatMember["created_at"];
14 | };
15 | export type MessageAdditionalData = Pick & {
16 | chatBubleDirection: "start" | "end";
17 | };
18 |
--------------------------------------------------------------------------------
/lib/db/message.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient } from "@supabase/supabase-js";
2 | import { Logger } from "next-axiom";
3 | import { LogLevel } from "next-axiom/dist/logger";
4 |
5 | import { Chat, Database, Message } from ".";
6 |
7 | export type CreateNewMessageParams = Pick<
8 | Message,
9 | "chat_id" | "content" | "role"
10 | > &
11 | Partial>;
12 | const log = new Logger({
13 | logLevel: LogLevel.debug,
14 | args: {
15 | route: "[DB] Messages",
16 | },
17 | });
18 |
19 | export const getMessages = async (
20 | supabase: SupabaseClient,
21 | chatId: Chat["id"]
22 | ) => {
23 | log.info(`${getMessages.name} called`, { chatId });
24 | const { data, error, status } = await supabase
25 | .from("messages")
26 | .select("*")
27 | .eq("chat_id", chatId)
28 | .order("created_at", { ascending: true });
29 |
30 | if (error && status !== 406) {
31 | log.error(getMessages.name, { chatId, error, status });
32 | return null;
33 | }
34 |
35 | log.info(`${getMessages.name} fetched successfully`, { chatId, data });
36 | return data;
37 | };
38 |
39 | export const getMessageById = async (
40 | supabase: SupabaseClient,
41 | id: Message["id"]
42 | ) => {
43 | log.info(`${getMessageById.name} called`, { params: { id } });
44 | const { data, error, status } = await supabase
45 | .from("messages")
46 | .select("*")
47 | .eq("id", id)
48 | .maybeSingle();
49 |
50 | if (error && status !== 406) {
51 | log.error(getMessageById.name, { params: { id }, error, status });
52 | return null;
53 | }
54 |
55 | log.info(`${getMessageById.name} fetched successfully`, { id, data });
56 | return data;
57 | };
58 |
59 | export const createNewMessage = async (
60 | supabase: SupabaseClient,
61 | params: CreateNewMessageParams
62 | ) => {
63 | log.info(`${createNewMessage.name} called`, params);
64 | const { data, error, status } = await supabase
65 | .from("messages")
66 | .insert([params])
67 | .select();
68 |
69 | if (error && status !== 406) {
70 | log.error(createNewMessage.name, { params, error, status });
71 | return null;
72 | }
73 |
74 | log.info(`${createNewMessage.name} created successfully`, { params, data });
75 | return data?.[0] || null;
76 | };
77 |
78 | export const deleteMessagesFrom = async (
79 | supabase: SupabaseClient,
80 | chatId: NonNullable,
81 | from: string
82 | ) => {
83 | log.info(`${deleteMessagesFrom.name} called`, {
84 | params: { chatId, from },
85 | });
86 | const { data, error } = await supabase
87 | .from("messages")
88 | .delete()
89 | .eq("chat_id", chatId)
90 | .gt("created_at", from);
91 |
92 | if (error) {
93 | log.error(deleteMessagesFrom.name, { error });
94 | }
95 |
96 | log.info(`${deleteMessagesFrom.name} deleted successfully`, { chatId });
97 | return data;
98 | };
99 |
--------------------------------------------------------------------------------
/lib/db/profile.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient } from "@supabase/supabase-js";
2 | import { Logger } from "next-axiom";
3 | import { LogLevel } from "next-axiom/dist/logger";
4 |
5 | import { Database } from ".";
6 | import { getCurrentUser } from "../session";
7 |
8 | const log = new Logger({
9 | logLevel: LogLevel.debug,
10 | args: {
11 | route: "[DB] Profile",
12 | },
13 | });
14 |
15 | export const getCurrentProfile = async (supabase: SupabaseClient) => {
16 | log.info(`${getCurrentProfile.name} called`);
17 |
18 | const user = await getCurrentUser(supabase);
19 |
20 | if (!user) return null;
21 |
22 | const { data, error, status } = await supabase
23 | .from("profiles")
24 | .select(`*`)
25 | .eq("id", user.id)
26 | .single();
27 |
28 | if (error && status !== 406) {
29 | log.error(getCurrentProfile.name, { error, status });
30 | return null;
31 | }
32 |
33 | log.info(`${getCurrentProfile.name} fetched successfully`, { data });
34 |
35 | return data;
36 | };
37 |
38 | export const getProfileByUsername = async (
39 | supabase: SupabaseClient,
40 | username: string
41 | ) => {
42 | log.info(`${getProfileByUsername.name} called`, { username });
43 |
44 | const { data, error } = await supabase
45 | .from("profiles")
46 | .select(`*`)
47 | .eq("username", username)
48 | .single();
49 |
50 | if (error) {
51 | log.error(getProfileByUsername.name, { error });
52 | return null;
53 | }
54 |
55 | log.info(`${getProfileByUsername.name} fetched successfully`, { data });
56 |
57 | return data;
58 | };
59 |
--------------------------------------------------------------------------------
/lib/session.ts:
--------------------------------------------------------------------------------
1 | import { SupabaseClient } from "@supabase/supabase-js";
2 |
3 | import { Database } from "./db";
4 |
5 | export const getCurrentUser = async (supabase: SupabaseClient) => {
6 | const {
7 | data: { user },
8 | } = await supabase.auth.getUser();
9 |
10 | return user;
11 | };
12 |
--------------------------------------------------------------------------------
/lib/stores/profile.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | import { Tables } from "../db";
4 |
5 | interface Profile {
6 | profile: Tables<"profiles"> | null;
7 | email?: string;
8 | setProfile: (profile: Tables<"profiles">) => void;
9 | }
10 |
11 | export const useProfileStore = create()((set) => ({
12 | profile: null,
13 | setProfile: (newProfile) =>
14 | set(() => ({
15 | profile: newProfile,
16 | })),
17 | }));
18 |
--------------------------------------------------------------------------------
/lib/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@/env.mjs";
2 | import { createBrowserClient } from "@supabase/ssr";
3 |
4 | export const createClient = () =>
5 | createBrowserClient(
6 | env.NEXT_PUBLIC_SUPABASE_URL,
7 | env.NEXT_PUBLIC_SUPABASE_ANON_KEY
8 | );
9 |
--------------------------------------------------------------------------------
/lib/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse, type NextRequest } from "next/server";
2 | import { env } from "@/env.mjs";
3 | import { createServerClient, type CookieOptions } from "@supabase/ssr";
4 |
5 | export const createClient = (request: NextRequest) => {
6 | // Create an unmodified response
7 | let response = NextResponse.next({
8 | request: {
9 | headers: request.headers,
10 | },
11 | });
12 |
13 | const supabase = createServerClient(
14 | env.NEXT_PUBLIC_SUPABASE_URL,
15 | env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
16 | {
17 | cookies: {
18 | get(name: string) {
19 | return request.cookies.get(name)?.value;
20 | },
21 | set(name: string, value: string, options: CookieOptions) {
22 | // If the cookie is updated, update the cookies for the request and response
23 | request.cookies.set({
24 | name,
25 | value,
26 | ...options,
27 | });
28 | response = NextResponse.next({
29 | request: {
30 | headers: request.headers,
31 | },
32 | });
33 | response.cookies.set({
34 | name,
35 | value,
36 | ...options,
37 | });
38 | },
39 | remove(name: string, options: CookieOptions) {
40 | // If the cookie is removed, update the cookies for the request and response
41 | request.cookies.set({
42 | name,
43 | value: "",
44 | ...options,
45 | });
46 | response = NextResponse.next({
47 | request: {
48 | headers: request.headers,
49 | },
50 | });
51 | response.cookies.set({
52 | name,
53 | value: "",
54 | ...options,
55 | });
56 | },
57 | },
58 | }
59 | );
60 |
61 | return { supabase, response };
62 | };
63 |
--------------------------------------------------------------------------------
/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 | import { env } from "@/env.mjs";
3 | import { createServerClient, type CookieOptions } from "@supabase/ssr";
4 |
5 | export const createClient = (cookieStore: ReturnType) => {
6 | return createServerClient(
7 | env.NEXT_PUBLIC_SUPABASE_URL,
8 | env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
9 | {
10 | cookies: {
11 | get(name: string) {
12 | return cookieStore.get(name)?.value;
13 | },
14 | set(name: string, value: string, options: CookieOptions) {
15 | try {
16 | cookieStore.set({ name, value, ...options });
17 | } catch (error) {
18 | // The `set` method was called from a Server Component.
19 | // This can be ignored if you have middleware refreshing
20 | // user sessions.
21 | }
22 | },
23 | remove(name: string, options: CookieOptions) {
24 | try {
25 | cookieStore.set({ name, value: "", ...options });
26 | } catch (error) {
27 | // The `delete` method was called from a Server Component.
28 | // This can be ignored if you have middleware refreshing
29 | // user sessions.
30 | }
31 | },
32 | },
33 | }
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 |
3 | import type { NextRequest } from 'next/server'
4 | import { createClient } from './lib/supabase/middleware'
5 |
6 | export async function middleware(req: NextRequest) {
7 | const { supabase, response } = createClient(req)
8 |
9 | const {
10 | data: { user },
11 | } = await supabase.auth.getUser()
12 |
13 | if (!user && req.nextUrl.pathname.indexOf('/apps') !== -1) {
14 | return NextResponse.redirect(new URL('/signin', req.url))
15 | }
16 |
17 | return response
18 | }
19 |
20 | export const config = {
21 | matcher: ['/', '/apps/:path*', '/profile'],
22 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const { withAxiom } = require('next-axiom');
3 |
4 | /** @type {import('next').NextConfig} */
5 | const nextConfig = {
6 | reactStrictMode: true,
7 | images: {
8 | remotePatterns: [
9 | {
10 | protocol: 'https',
11 | hostname: 'avatars.githubusercontent.com',
12 | port: '',
13 | pathname: '/*/**',
14 | },
15 | ],
16 | },
17 | }
18 |
19 | module.exports = withAxiom(nextConfig)
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ai-fusion-kit",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/openai": "^0.0.18",
13 | "@hookform/resolvers": "^3.3.0",
14 | "@radix-ui/react-accordion": "^1.1.2",
15 | "@radix-ui/react-alert-dialog": "^1.0.5",
16 | "@radix-ui/react-avatar": "^1.0.4",
17 | "@radix-ui/react-dialog": "^1.0.5",
18 | "@radix-ui/react-dropdown-menu": "^2.0.6",
19 | "@radix-ui/react-hover-card": "^1.0.7",
20 | "@radix-ui/react-icons": "^1.3.0",
21 | "@radix-ui/react-label": "^2.0.2",
22 | "@radix-ui/react-navigation-menu": "^1.1.4",
23 | "@radix-ui/react-popover": "^1.0.7",
24 | "@radix-ui/react-scroll-area": "^1.0.5",
25 | "@radix-ui/react-select": "^2.0.0",
26 | "@radix-ui/react-separator": "^1.0.3",
27 | "@radix-ui/react-slider": "^1.1.2",
28 | "@radix-ui/react-slot": "^1.0.2",
29 | "@radix-ui/react-switch": "^1.0.3",
30 | "@radix-ui/react-tabs": "^1.0.4",
31 | "@radix-ui/react-toast": "^1.1.5",
32 | "@radix-ui/react-tooltip": "^1.0.7",
33 | "@supabase/ssr": "^0.3.0",
34 | "@supabase/supabase-js": "^2.43.4",
35 | "@t3-oss/env-nextjs": "^0.9.2",
36 | "@types/node": "20.11.19",
37 | "@types/react": "18.3.1",
38 | "@types/react-dom": "18.3.0",
39 | "@vercel/analytics": "^1.3.1",
40 | "ai": "^3.1.25",
41 | "autoprefixer": "10.4.17",
42 | "class-variance-authority": "^0.7.0",
43 | "clsx": "^2.1.0",
44 | "cmdk": "^0.2.1",
45 | "eslint": "8.46.0",
46 | "eslint-config-next": "14.2.3",
47 | "framer-motion": "^11.0.5",
48 | "geist": "^1.3.0",
49 | "lodash": "^4.17.21",
50 | "lucide-react": "0.331.0",
51 | "mini-svg-data-uri": "^1.4.4",
52 | "next": "14.2.3",
53 | "next-axiom": "^1.1.1",
54 | "next-themes": "^0.3.0",
55 | "openai": "^4.47.3",
56 | "postcss": "8.4.35",
57 | "react": "18.3.1",
58 | "react-dom": "18.3.1",
59 | "react-hook-form": "^7.48.2",
60 | "react-intersection-observer": "^9.5.2",
61 | "react-markdown": "^8.0.7",
62 | "react-mentions": "^4.4.10",
63 | "react-resizable-panels": "^2.0.19",
64 | "react-syntax-highlighter": "^15.5.0",
65 | "react-textarea-autosize": "^8.5.3",
66 | "remark-gfm": "^3.0.1",
67 | "remark-math": "^5.1.1",
68 | "server-only": "^0.0.1",
69 | "tailwind-merge": "^2.3.0",
70 | "tailwindcss": "3.4.3",
71 | "tailwindcss-animate": "^1.0.7",
72 | "typescript": "5.1.6",
73 | "uuid": "^9.0.1",
74 | "vaul": "^0.9.0",
75 | "zod": "^3.23.8",
76 | "zod-form-data": "^2.0.2",
77 | "zustand": "^4.5.0"
78 | },
79 | "devDependencies": {
80 | "@ianvs/prettier-plugin-sort-imports": "^4.2.1",
81 | "@types/lodash": "^4.17.0",
82 | "@types/react-mentions": "^4.1.13",
83 | "@types/react-syntax-highlighter": "^15.5.7",
84 | "@types/uuid": "^9.0.8",
85 | "@typescript-eslint/eslint-plugin": "^5.62.0",
86 | "@typescript-eslint/parser": "^5.62.0",
87 | "encoding": "^0.1.13",
88 | "eslint-config-prettier": "^8.10.0",
89 | "eslint-plugin-prettier": "^5.1.3",
90 | "eslint-plugin-react": "^7.34.2",
91 | "eslint-plugin-tailwindcss": "^3.17.0",
92 | "prettier": "^3.2.5",
93 | "supabase": "^1.172.2"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "auto",
4 | semi: true,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
9 | importOrder: [
10 | "^(react/(.*)$)|^(react$)",
11 | "^(next/(.*)$)|^(next$)",
12 | "",
13 | "",
14 | "^types$",
15 | "^@/types/(.*)$",
16 | "^@/config/(.*)$",
17 | "^@/lib/(.*)$",
18 | "^@/hooks/(.*)$",
19 | "^@/components/ui/(.*)$",
20 | "^@/components/(.*)$",
21 | "^@/styles/(.*)$",
22 | "^@/app/(.*)$",
23 | "",
24 | "^[./]",
25 | ],
26 | importOrderSeparation: false,
27 | importOrderSortSpecifiers: true,
28 | importOrderBuiltinModulesToTop: true,
29 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
30 | importOrderMergeDuplicateImports: true,
31 | importOrderCombineTypeAndValueImports: true,
32 | };
33 |
--------------------------------------------------------------------------------
/public/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/public/avatar.png
--------------------------------------------------------------------------------
/public/chat-gpt.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/public/chat-gpt.jpeg
--------------------------------------------------------------------------------
/public/featured-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/public/featured-dark.jpg
--------------------------------------------------------------------------------
/public/featured-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/public/featured-mobile.png
--------------------------------------------------------------------------------
/public/featured.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/public/featured.jpg
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/public/logo.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nphivu414/ai-fusion-kit/1d250ee29b7291f9b69b09c3a427957bc0d0fbb0/public/screenshot.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | .env
5 |
--------------------------------------------------------------------------------
/supabase/migrations/20240403013936_rls.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."apps" enable row level security;
2 |
3 | alter table "public"."chats" enable row level security;
4 |
5 | alter table "public"."messages" enable row level security;
6 |
7 | ALTER TABLE "public"."apps"
8 | RENAME COLUMN "createdAt" TO created_at;
9 | ALTER TABLE "public"."apps"
10 | RENAME COLUMN "updatedAt" to updated_at;
11 | ALTER TABLE "public"."apps"
12 | RENAME COLUMN "logoUrl" to logo_url;
13 |
14 | ALTER TABLE "public"."chats"
15 | RENAME COLUMN "createdAt" TO created_at;
16 | ALTER TABLE "public"."chats"
17 | RENAME COLUMN "updatedAt" to updated_at;
18 | ALTER TABLE "public"."chats"
19 | RENAME COLUMN "profileId" to profile_id;
20 | ALTER TABLE "public"."chats"
21 | RENAME COLUMN "appId" to app_id;
22 |
23 | ALTER TABLE "public"."messages"
24 | RENAME COLUMN "createdAt" TO created_at;
25 | ALTER TABLE "public"."messages"
26 | RENAME COLUMN "updatedAt" to updated_at;
27 | ALTER TABLE "public"."messages"
28 | RENAME COLUMN "profileId" to profile_id;
29 | ALTER TABLE "public"."messages"
30 | RENAME COLUMN "chatId" to chat_id;
31 |
32 | CREATE POLICY "Allow read-only access to authenticated users" ON "public"."apps"
33 | AS PERMISSIVE FOR SELECT
34 | TO authenticated
35 | USING (true);
36 |
37 | CREATE POLICY "User can view their chat sessions" ON "public"."chats"
38 | AS PERMISSIVE FOR SELECT
39 | TO authenticated
40 | USING (profile_id = auth.uid());
41 |
42 |
43 | CREATE POLICY "User can create their chat sessions" ON "public"."chats"
44 | AS PERMISSIVE FOR INSERT
45 | TO authenticated
46 | WITH CHECK (profile_id = auth.uid());
47 |
48 |
49 | CREATE POLICY "User can update their chat sessions" ON "public"."chats"
50 | AS PERMISSIVE FOR UPDATE
51 | TO public
52 | USING (profile_id = auth.uid())
53 | WITH CHECK (profile_id = auth.uid());
54 |
55 |
56 | CREATE POLICY "User can delete their chat sessions" ON "public"."chats"
57 | AS PERMISSIVE FOR DELETE
58 | TO authenticated
59 | USING (profile_id = auth.uid());
60 |
61 | CREATE POLICY "User can view chat messages" ON "public"."messages"
62 | AS PERMISSIVE FOR SELECT
63 | TO authenticated
64 | USING (EXISTS ( SELECT 1 FROM chats WHERE chats.id = chat_id ));
65 |
66 | CREATE POLICY "User can create chat messages" ON "public"."messages"
67 | AS PERMISSIVE FOR INSERT
68 | TO authenticated
69 | WITH CHECK (EXISTS ( SELECT 1 FROM chats WHERE chat_id = chats.id ) AND profile_id = auth.uid());
70 |
71 | CREATE POLICY "User can update chat messages" ON "public"."messages"
72 | AS PERMISSIVE FOR UPDATE
73 | TO authenticated
74 | USING (EXISTS ( SELECT 1 FROM chats WHERE chat_id = chats.id ) AND profile_id = auth.uid())
75 | WITH CHECK (EXISTS ( SELECT 1 FROM chats WHERE chat_id = chats.id ) AND profile_id = auth.uid());
76 |
--------------------------------------------------------------------------------
/supabase/migrations/20240405151156_default_profile_id.sql:
--------------------------------------------------------------------------------
1 | alter table "public"."chats" alter column "profile_id" set default auth.uid();
2 | alter table "public"."messages" alter column "profile_id" set default auth.uid();
3 |
4 |
5 |
--------------------------------------------------------------------------------
/supabase/migrations/20240504083818_chat_members.sql:
--------------------------------------------------------------------------------
1 | drop policy "User can view their joined chat sessions" on "public"."chat_members";
2 |
3 | create policy "User can view their joined chat sessions"
4 | on "public"."chat_members"
5 | as permissive
6 | for select
7 | to authenticated
8 | using (true);
9 |
10 | ALTER TABLE "public"."chat_members"
11 | ADD CONSTRAINT unique_chat_member UNIQUE (chat_id, member_id);
12 |
--------------------------------------------------------------------------------
/supabase/migrations/20240609070425_handle_new_user_update.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION public.handle_new_user()
2 | RETURNS trigger
3 | LANGUAGE plpgsql
4 | SECURITY DEFINER
5 | AS $function$
6 | begin
7 | insert into public.profiles (id, full_name, avatar_url, username)
8 | values (
9 | new.id,
10 | new.raw_user_meta_data->>'full_name',
11 | new.raw_user_meta_data->>'avatar_url',
12 | split_part(new.email, '@', 1)
13 | );
14 | return new;
15 | end;
16 | $function$
17 | ;
18 |
--------------------------------------------------------------------------------
/supabase/migrations/20240626065103_migrate_username_from_email.sql:
--------------------------------------------------------------------------------
1 | UPDATE profiles AS p
2 | SET username = u.email
3 | FROM auth.users as u
4 | WHERE p.id = u.id AND p.username IS NULL;
5 |
--------------------------------------------------------------------------------
/supabase/migrations/20240626065226_update_handle_new_user.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION public.handle_new_user()
2 | RETURNS trigger
3 | LANGUAGE plpgsql
4 | SECURITY DEFINER
5 | AS $function$
6 | begin
7 | insert into public.profiles (id, full_name, avatar_url, username)
8 | values (
9 | new.id,
10 | new.raw_user_meta_data->>'full_name',
11 | new.raw_user_meta_data->>'avatar_url',
12 | new.email
13 | );
14 | return new;
15 | end;
16 | $function$
17 | ;
18 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO public.apps
2 | (id, "name", description, "created_at", "updated_at", slug, "logo_url")
3 | VALUES('0fc8886d-8ab8-4d5e-858d-a22083b35d2a'::uuid, 'GPT AI Assistant', 'Build your own AI assistant in minutes', '2023-09-11 10:04:05.996', NULL, '/apps/chat', '/_next/static/media/chat-gpt.2fe64c6b.jpeg');
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------