Repository: nphivu414/ai-fusion-kit
Branch: main
Commit: 087ab1c7d7f2
Files: 190
Total size: 329.0 KB
Directory structure:
gitextract_6e60vi3g/
├── .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
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ ├── profile/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── styles/
│ └── custom.css
├── 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
├── components.json
├── 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
├── 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
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"$schema": "https://json.schemastore.org/eslintrc",
"root": true,
"extends": [
"next/core-web-vitals",
"plugin:tailwindcss/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["tailwindcss", "prettier"],
"rules": {
"@next/next/no-html-link-for-pages": "off",
"react/jsx-key": "off",
"tailwindcss/no-custom-classname": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"prettier/prettier": "error"
},
"settings": {
"tailwindcss": {
"callees": ["cn"],
"config": "tailwind.config.js"
},
"next": {
"rootDir": ["./"]
}
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
}
]
}
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [nphivu414]
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
================================================
FILE: .prettierignore
================================================
cache
.cache
package.json
package-lock.json
public
CHANGELOG.md
.yarn
dist
node_modules
.next
build
.contentlayer
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Vu Nguyen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
AI Fusion Kit
A feature-rich, highly customizable AI Web App Template, empowered by Next.js.
Tech stacks ·
Installation ·
Run Locally ·
Authors
## Tech stacks
- [Typescript](https://www.typescriptlang.org/)
- [ReactJS](https://reactjs.org/)
- [NextJS](https://nextjs.org/)
- [Supabase](https://supabase.com/)
- [Open AI API](https://platform.openai.com/docs/api-reference)
- [Vercel AI SDK](https://github.com/vercel/ai)
- [TailwindCSS](https://tailwindcss.com/)
- [Shadcn UI](https://ui.shadcn.com/)
- [Aceternity UI](https://ui.aceternity.com/)
- [Next.js AI Chatbot](https://github.com/vercel-labs/ai-chatbot)
## Installation
1. Clone the repo
```sh
git clone https://github.com/nphivu414/ai-fusion-kit
```
2. Install dependencies
```sh
yarn install
```
3. Setup Supabase local development
- Install [Docker](https://www.docker.com/get-started/)
- 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.
```sh
supabase start
```
- 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:
```sh
Started supabase local development setup.
API URL: http://localhost:54321
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324
anon key: eyJh......
service_role key: eyJh......
```
- The API URL will be used as the `NEXT_PUBLIC_SUPABASE_URL` in `.env.local`
- For more information about how to use Supabase on your local development machine: https://supabase.com/docs/guides/cli/local-development
4. Get an account from OpenAI and generate your own API key
5. Rename `.env.example` to `.env.local` and populate with your values
> 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.
## Run Locally
1. Go to the project directory
```bash
cd ai-fusion-kit
```
2. Start the web app
```bash
yarn dev
```
## Authors
- [@nphivu414](https://github.com/nphivu414)
- [@toproad1407](https://github.com/toproad1407)
================================================
FILE: app/(auth)/auth-code-error/page.tsx
================================================
import { Metadata } from "next";
import { Heading3 } from "@/components/ui/typography/Heading3";
export const metadata: Metadata = {
title: "Error",
description: "Failed to sign in",
};
export default async function AuthCodeError() {
return (
<>
>
);
}
================================================
FILE: app/(auth)/layout.tsx
================================================
import { AppLogo } from "@/components/ui/common/AppLogo";
type AuthLayoutProps = {
children: React.ReactNode;
};
export default function AuthLayout({ children }: AuthLayoutProps) {
return (
);
}
================================================
FILE: app/(auth)/signin/page.tsx
================================================
import { Metadata } from "next";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { siteConfig } from "@/config/site";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { Heading3 } from "@/components/ui/typography";
import { UserAuthForm } from "@/components/modules/auth/UserAuthForm";
export const metadata: Metadata = {
title: "Sigin",
description: "Sigin to your account",
};
export const runtime = "edge";
export const dynamic = "force-dynamic";
export default async function LoginPage() {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
if (user) {
redirect(`/apps/chat`);
}
return (
<>
{siteConfig.name}
Empowering Your Imagination with AI Services
>
);
}
================================================
FILE: app/(auth)/signup/page.tsx
================================================
import { Metadata } from "next";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { siteConfig } from "@/config/site";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { Heading3 } from "@/components/ui/typography";
import { UserSignupForm } from "@/components/modules/auth/UserSignupForm";
export const runtime = "edge";
export const metadata: Metadata = {
title: "Signup",
description: "Signup a new account",
};
export const dynamic = "force-dynamic";
export default async function LoginPage() {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
if (user) {
redirect(`/apps/chat`);
}
return (
<>
{siteConfig.name}
Empowering Your Imagination with AI Services
>
);
}
================================================
FILE: app/api/auth/callback/route.ts
================================================
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
// if "next" is in param, use it as the redirect URL
const next = searchParams.get("next") ?? "/";
if (code) {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth-code-error`);
}
================================================
FILE: app/api/auth/logout/route.ts
================================================
import { cookies } from "next/headers";
import { NextResponse, type NextRequest } from "next/server";
import { createClient } from "@/lib/supabase/server";
export const dynamic = "force-dynamic";
export async function POST(req: NextRequest) {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
// Check if we have a session
const {
data: { user },
} = await supabase.auth.getUser();
if (user) {
await supabase.auth.signOut();
}
return NextResponse.redirect(new URL("/signin", req.url), {
status: 302,
});
}
================================================
FILE: app/api/chat/route.ts
================================================
import { cookies } from "next/headers";
import { env } from "@/env.mjs";
import { createOpenAI } from "@ai-sdk/openai";
import { Message, streamText } from "ai";
import { pick } from "lodash";
import { AxiomRequest, withAxiom } from "next-axiom";
import { getAppBySlug } from "@/lib/db/apps";
import { createNewChatMember } from "@/lib/db/chat-members";
import { createNewChat } from "@/lib/db/chats";
import {
createNewMessage,
deleteMessagesFrom,
getMessageById,
} from "@/lib/db/message";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
export const dynamic = "force-dynamic";
export const runtime = "edge";
export const preferredRegion = "home";
const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
});
export const POST = withAxiom(async (req: AxiomRequest) => {
const log = req.log.with({
route: "api/chat",
});
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const params = await req.json();
const {
messages,
temperature,
model,
maxTokens,
topP,
frequencyPenalty,
presencePenalty,
chatId,
isRegenerate,
regenerateMessageId,
isNewChat,
enableChatAssistant = true,
} = params;
const user = await getCurrentUser(supabase);
const currentApp = await getAppBySlug(supabase, "/apps/chat");
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
const lastMessage = messages[messages.length - 1];
if (!isRegenerate) {
if (isNewChat && currentApp) {
await createNewChat(supabase, {
id: chatId,
app_id: currentApp.id,
name: lastMessage.content,
});
await createNewChatMember(supabase, {
chat_id: chatId,
member_id: user.id,
});
}
await createNewMessage(supabase, {
chat_id: chatId,
content: lastMessage.content,
role: "user",
id: lastMessage.id,
});
} else if (regenerateMessageId) {
const fromMessage = await getMessageById(supabase, regenerateMessageId);
if (fromMessage?.created_at) {
await deleteMessagesFrom(supabase, chatId, fromMessage.created_at);
}
}
if (!enableChatAssistant) {
return new Response(null, {
status: 200,
headers: {
"Content-Type": "application/json",
"should-redirect-to-new-chat": "true",
},
});
}
log.debug("Start stream text");
const response = await streamText({
model: openai(model),
temperature,
messages: messages.map((message: Message) =>
pick(message, "content", "role")
),
maxTokens,
topP,
frequencyPenalty,
presencePenalty,
onFinish: async ({ text }) => {
await createNewMessage(supabase, {
chat_id: chatId,
content: text,
role: "assistant",
});
},
});
log.debug("End stream text");
return response.toAIStreamResponse();
});
================================================
FILE: app/apps/chat/[id]/page.tsx
================================================
import React from "react";
import { Metadata } from "next";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { Message } from "ai";
import {
CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE,
DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT,
} from "@/lib/contants";
import { getAppBySlug } from "@/lib/db/apps";
import { getChatMembers } from "@/lib/db/chat-members";
import { getChatById, getChats } from "@/lib/db/chats";
import { getMessages } from "@/lib/db/message";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { ChatPanel } from "@/components/modules/apps/chat/ChatPanel";
import { ChatParams } from "@/components/modules/apps/chat/types";
export const runtime = "edge";
export const preferredRegion = "home";
export const metadata: Metadata = {
title: "Chat",
description:
"Chat with your AI assistant to generate new ideas and get inspired.",
};
export default async function ChatPage({ params }: { params: { id: string } }) {
const chatId = params.id;
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
const currentApp = await getAppBySlug(supabase, "/apps/chat");
if (!currentApp || !user) {
return No app found
;
}
const chats = await getChats(supabase, currentApp.id);
const dbMessages = await getMessages(supabase, chatId);
const chatDetails = await getChatById(supabase, chatId);
if (!chatDetails) {
redirect("/apps/chat");
}
const chatParams = chatDetails?.settings as ChatParams | undefined;
const isChatHost = chatDetails?.profile_id === user.id;
const initialChatMessages: Message[] = dbMessages?.length
? dbMessages.map((message) => {
return {
id: message.id,
role: message.role || "system",
content: message.content || "",
data: {
profile_id: message.profile_id,
chat_id: message.chat_id,
chatBubleDirection:
message.role === "user" && message.profile_id === user.id
? "end"
: "start",
},
};
})
: [];
if (chatParams?.description) {
initialChatMessages.unshift({
id: "description",
role: "system",
content: chatParams.description,
});
}
const chatMembers = await getChatMembers(supabase, chatId);
const memberSidebarLayout = cookies().get(CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE);
let defaultMemberSidebarLayout = DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT;
if (memberSidebarLayout) {
defaultMemberSidebarLayout = JSON.parse(memberSidebarLayout.value);
}
return (
);
}
================================================
FILE: app/apps/chat/loading.tsx
================================================
import { Loader } from "lucide-react";
import { Separator } from "@/components/ui/Separator";
import { Skeleton } from "@/components/ui/Skeleton";
import { Heading2 } from "@/components/ui/typography";
export default function Page() {
return (
);
}
================================================
FILE: app/apps/chat/page.tsx
================================================
import React from "react";
import { Metadata } from "next";
import { cookies } from "next/headers";
import { v4 as uuidv4 } from "uuid";
import { getAppBySlug } from "@/lib/db/apps";
import { getChats } from "@/lib/db/chats";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { ChatPanel } from "@/components/modules/apps/chat/ChatPanel";
export const metadata: Metadata = {
title: "Create a New Chat",
};
export default async function NewChatPage() {
const chatId = uuidv4();
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
const currentApp = await getAppBySlug(supabase, "/apps/chat");
if (!currentApp || !user) {
return No app found
;
}
const chats = await getChats(supabase, currentApp.id);
return (
);
}
================================================
FILE: app/apps/layout.tsx
================================================
import { cookies } from "next/headers";
import { getAppBySlug } from "@/lib/db/apps";
import { getChats } from "@/lib/db/chats";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { MainLayout } from "@/components/ui/common/MainLayout";
import { ChatHistory } from "@/components/modules/apps/chat/ChatHistory";
interface AppLayoutProps {
children: React.ReactNode;
}
export default async function AppLayout({ children }: AppLayoutProps) {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
const currentApp = await getAppBySlug(supabase, "/apps/chat");
if (!currentApp || !user) {
return No app found
;
}
const chats = await getChats(supabase, currentApp.id);
return (
);
}
================================================
FILE: app/apps/page.tsx
================================================
import { Heading1 } from "@/components/ui/typography";
export const runtime = "edge";
export default function Apps() {
return Apps ;
}
================================================
FILE: app/docs/page.tsx
================================================
export default async function Docs() {
return docs
;
}
================================================
FILE: app/globals.css
================================================
@import url('./styles/custom.css');
@tailwind base;
@tailwind components;
@tailwind utilities;
.drawer-toggle:checked ~ .drawer-side {
backdrop-filter: blur(5px);
}
@media screen and (-webkit-min-device-pixel-ratio:0) {
select,
textarea,
input {
font-size: 16px !important;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 262.1 83.3% 57.8%;
--radius: 0.5rem;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 0 0% 95%;
--card: 24 9.8% 10%;
--card-foreground: 0 0% 95%;
--popover: 0 0% 9%;
--popover-foreground: 0 0% 95%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 15%;
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 263.4 70% 50.4%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
================================================
FILE: app/layout.tsx
================================================
import { AxiomWebVitals } from "next-axiom";
import "./globals.css";
import { Metadata, Viewport } from "next";
import { Analytics } from "@vercel/analytics/react";
import { GeistMono } from "geist/font/mono";
import { GeistSans } from "geist/font/sans";
import { ThemeProvider } from "next-themes";
import { siteConfig } from "@/config/site";
import { Toaster } from "@/components/ui/Toaster";
export const metadata: Metadata = {
title: {
default: siteConfig.name,
template: `%s - ${siteConfig.name}`,
},
description: siteConfig.description,
icons: {
icon: "/favicon.ico",
shortcut: "/favicon-16x16.png",
apple: "/apple-touch-icon.png",
},
};
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<>
{children}
>
);
}
================================================
FILE: app/page.tsx
================================================
import { Metadata } from "next";
import Link from "next/link";
import { Play } from "lucide-react";
import { siteConfig } from "@/config/site";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/Button";
import { MainLayout } from "@/components/ui/common/MainLayout";
import { Heading1 } from "@/components/ui/typography";
import { DescriptionHeadingText } from "@/components/modules/home/DescriptionHeadingText";
import { FeatureItems } from "@/components/modules/home/FeatureItems";
import { HeroBannerImage } from "@/components/modules/home/HeroBannerImage";
export const metadata: Metadata = {
title: siteConfig.name,
description: siteConfig.description,
};
export const runtime = "edge";
export default async function Home() {
return (
);
}
================================================
FILE: app/profile/layout.tsx
================================================
import { MainLayout } from "@/components/ui/common/MainLayout";
interface AppLayoutProps {
children: React.ReactNode;
}
export default function AppLayout({ children }: AppLayoutProps) {
return (
{children}
);
}
================================================
FILE: app/profile/page.tsx
================================================
import { cookies } from "next/headers";
import { getCurrentProfile } from "@/lib/db/profile";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { Header } from "@/components/modules/profile/Header";
import { ProfileForm } from "@/components/modules/profile/ProfileForm";
import { ProfileFormValues } from "@/components/modules/profile/type";
export const dynamic = "force-dynamic";
export const runtime = "edge";
export default async function Profile() {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const profile = await getCurrentProfile(supabase);
const user = await getCurrentUser(supabase);
if (!profile) {
return null;
}
const { avatar_url, full_name, username, website } = profile;
const profileFormValues: ProfileFormValues = {
fullName: full_name || undefined,
username: username || undefined,
website: website || undefined,
};
return (
);
}
================================================
FILE: app/styles/custom.css
================================================
/* Styles extracted from https://github.com/saadeghi/daisyui */
.avatar {
position: relative;
display: inline-flex
}
.avatar>div {
display: block;
aspect-ratio: 1/1;
overflow: hidden
}
.avatar img {
height: 100%;
width: 100%;
object-fit: cover
}
.avatar.placeholder>div {
display: flex;
align-items: center;
justify-content: center
}
.chat {
display: grid;
grid-template-columns: repeat(2,minmax(0,1fr));
column-gap: .75rem;
padding-top: .25rem;
padding-bottom: .25rem
}
.chat-image {
grid-row: span 2/span 2;
align-self: flex-end
}
.chat-header {
grid-row-start: 1;
font-size: .875rem;
line-height: 1.25rem
}
.chat-footer {
grid-row-start: 3;
font-size: .875rem;
line-height: 1.25rem
}
.chat-bubble {
position: relative;
display: block;
width: -moz-fit-content;
width: fit-content;
padding: .5rem 1rem;
max-width: 90%;
border-radius: var(--rounded-box,1rem);
min-height: 2.75rem;
min-width: 2.75rem;
--tw-bg-opacity: 1;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))
}
.chat-bubble:before {
position: absolute;
bottom: 0;
height: .75rem;
width: .75rem;
background-color: inherit;
content: "";
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center
}
.chat-start {
place-items: start;
grid-template-columns: auto 1fr
}
.chat-start .chat-header,.chat-start .chat-footer {
grid-column-start: 2
}
.chat-start .chat-image {
grid-column-start: 1
}
.chat-start .chat-bubble {
grid-column-start: 2;
border-end-start-radius: 0
}
.chat-start .chat-bubble:before {
-webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDMgMyBMIDMgMCBDIDMgMSAxIDMgMCAzJy8+PC9zdmc+);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDMgMyBMIDMgMCBDIDMgMSAxIDMgMCAzJy8+PC9zdmc+);
inset-inline-start: -.749rem
}
[dir=rtl] .chat-start .chat-bubble:before {
-webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDEgMyBMIDMgMyBDIDIgMyAwIDEgMCAwJy8+PC9zdmc+);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDEgMyBMIDMgMyBDIDIgMyAwIDEgMCAwJy8+PC9zdmc+)
}
.chat-end {
place-items: end;
grid-template-columns: 1fr auto
}
.chat-end .chat-header,.chat-end .chat-footer {
grid-column-start: 1
}
.chat-end .chat-image {
grid-column-start: 2
}
.chat-end .chat-bubble {
grid-column-start: 1;
border-end-end-radius: 0
}
.chat-end .chat-bubble:before {
-webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDEgMyBMIDMgMyBDIDIgMyAwIDEgMCAwJy8+PC9zdmc+);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDEgMyBMIDMgMyBDIDIgMyAwIDEgMCAwJy8+PC9zdmc+);
inset-inline-start: 99.9%
}
[dir=rtl] .chat-end .chat-bubble:before {
-webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDMgMyBMIDMgMCBDIDMgMSAxIDMgMCAzJy8+PC9zdmc+);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMycgaGVpZ2h0PSczJyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnPjxwYXRoIGZpbGw9J2JsYWNrJyBkPSdtIDAgMyBMIDMgMyBMIDMgMCBDIDMgMSAxIDMgMCAzJy8+PC9zdmc+)
}
.drawer {
position: relative;
display: grid;
grid-auto-columns: max-content auto;
width: 100%
}
.drawer-content {
grid-column-start: 2;
grid-row-start: 1;
min-width: 0px
}
.drawer-side {
pointer-events: none;
position: fixed;
inset-inline-start: 0px;
top: 0;
grid-column-start: 1;
grid-row-start: 1;
display: grid;
width: 100%;
grid-template-columns: repeat(1,minmax(0,1fr));
grid-template-rows: repeat(1,minmax(0,1fr));
align-items: flex-start;
justify-items: start;
overflow-x: hidden;
overflow-y: hidden;
overscroll-behavior: contain;
height: 100vh;
height: 100dvh
}
.drawer-side>.drawer-overlay {
position: sticky;
top: 0;
place-self: stretch;
cursor: pointer;
background-color: transparent;
transition-property: color,background-color,border-color,text-decoration-color,fill,stroke;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-timing-function: cubic-bezier(0,0,.2,1);
transition-duration: .2s
}
.drawer-side>* {
grid-column-start: 1;
grid-row-start: 1
}
.drawer-side>*:not(.drawer-overlay) {
transition-property: transform;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-timing-function: cubic-bezier(0,0,.2,1);
transition-duration: .3s;
will-change: transform;
transform: translate(-100%)
}
[dir=rtl] .drawer-side>*:not(.drawer-overlay) {
transform: translate(100%)
}
.drawer-toggle {
position: fixed;
height: 0px;
width: 0px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
opacity: 0
}
.drawer-toggle:checked~.drawer-side {
pointer-events: auto;
visibility: visible;
overflow-y: auto
}
.drawer-toggle:checked~.drawer-side>*:not(.drawer-overlay) {
transform: translate(0)
}
.drawer-end {
grid-auto-columns: auto max-content
}
.drawer-end .drawer-toggle~.drawer-content {
grid-column-start: 1
}
.drawer-end .drawer-toggle~.drawer-side {
grid-column-start: 2;
justify-items: end
}
.drawer-end .drawer-toggle~.drawer-side>*:not(.drawer-overlay) {
transform: translate(100%)
}
[dir=rtl] .drawer-end .drawer-toggle~.drawer-side>*:not(.drawer-overlay) {
transform: translate(-100%)
}
.drawer-end .drawer-toggle:checked~.drawer-side>*:not(.drawer-overlay) {
transform: translate(0)
}
================================================
FILE: components/modules/apps/app-side-bar/AppSideBar.tsx
================================================
import React from "react";
import { cookies } from "next/headers";
import { getApps } from "@/lib/db/apps";
import { createClient } from "@/lib/supabase/server";
import { AppSideBarList } from "./AppSideBarList";
export const AppSideBar = async () => {
const cookieStore = cookies();
const supabase = await createClient(cookieStore);
const apps = await getApps(supabase);
return (
);
};
================================================
FILE: components/modules/apps/app-side-bar/AppSideBarItem.tsx
================================================
import React from "react";
import Link from "next/link";
import { App } from "@/lib/db";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar";
type AppSideBarItemProps = Pick<
App,
"name" | "slug" | "description" | "logo_url"
>;
export const AppSideBarItem = ({
name,
slug,
description,
logo_url: logoUrl,
}: AppSideBarItemProps) => {
return (
{name}
{description ? (
{description}
) : null}
);
};
================================================
FILE: components/modules/apps/app-side-bar/AppSideBarList.tsx
================================================
import { App } from "@/lib/db";
import { AppSideBarItem } from "./AppSideBarItem";
type AppSideBarListProps = {
apps: App[] | null;
};
export const AppSideBarList = ({ apps }: AppSideBarListProps) => {
if (!apps?.length) {
return No apps found
;
}
return (
{apps.map((app) => {
const { id, name, description, slug, logo_url } = app;
return (
);
})}
);
};
================================================
FILE: components/modules/apps/app-side-bar/AppSidebarSection.tsx
================================================
import React from "react";
import { Heading5 } from "@/components/ui/typography";
type AppSidebarSectionProps = {
title: string;
children: React.ReactNode;
};
export const AppSidebarSection = ({
title,
children,
}: AppSidebarSectionProps) => {
return (
{title}
{children}
);
};
================================================
FILE: components/modules/apps/app-side-bar/index.ts
================================================
export * from "./AppSideBar";
================================================
FILE: components/modules/apps/chat/ChatForm.tsx
================================================
import React from "react";
import { SendHorizonal } from "lucide-react";
import { MentionsInputProps, SuggestionDataItem } from "react-mentions";
import { Chat, ChatMemberProfile } from "@/lib/db";
import { useProfileStore } from "@/lib/stores/profile";
import { useEnterSubmit } from "@/hooks/useEnterSubmit";
import { Button } from "@/components/ui/Button";
import { ChatInput } from "@/components/ui/chat";
import { MobileDrawerControl } from "./MobileDrawerControls";
type ChatFormProps = {
chatInput: string;
chats: Chat[] | null;
isChatStreamming: boolean;
chatMembers: ChatMemberProfile[] | null;
onSubmit: (e: React.FormEvent) => void;
onInputChange: (
e:
| React.ChangeEvent
| React.ChangeEvent
) => void;
};
export const ChatForm = ({
chats,
isChatStreamming,
chatMembers,
chatInput,
onSubmit,
onInputChange,
}: ChatFormProps) => {
const { formRef, onKeyDown } = useEnterSubmit();
const currentProfile = useProfileStore((state) => state.profile);
const mentionData: SuggestionDataItem[] = React.useMemo(() => {
const mentionData = [{ id: "assistant", display: "Assistant" }];
if (!chatMembers) return mentionData;
chatMembers.forEach((member) => {
if (!member.profiles) return;
mentionData.push({
id: member.profiles.id,
display: member.profiles.username || "",
});
});
return mentionData.filter((mention) => mention.id !== currentProfile?.id);
}, [chatMembers, currentProfile?.id]);
const handleOnChange: MentionsInputProps["onChange"] = (e) => {
onInputChange({
target: { value: e.target.value },
} as React.ChangeEvent);
};
return (
);
};
================================================
FILE: components/modules/apps/chat/ChatHistory.tsx
================================================
"use client";
import React from "react";
import { Chat } from "@/lib/db";
import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
import { Separator } from "@/components/ui/Separator";
import { Paragraph } from "@/components/ui/typography";
import { ChatHistoryItem } from "./ChatHistoryItem";
import { NewChatButton } from "./NewChatButton";
type ChatHistoryProps = {
data: Chat[] | null;
closeDrawer?: () => void;
};
export const ChatHistory = ({ data, closeDrawer }: ChatHistoryProps) => {
const chatId = useChatIdFromPathName();
return (
{data?.length ? null : (
No chats found
)}
{data?.map((chat) => {
return (
);
})}
);
};
================================================
FILE: components/modules/apps/chat/ChatHistoryDrawer.tsx
================================================
"use client";
import React from "react";
import { History } from "lucide-react";
import { Drawer } from "vaul";
import { Chat } from "@/lib/db";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/Button";
import { ChatHistory } from "./ChatHistory";
type ChatHistoryDrawerProps = {
data: Chat[] | null;
};
export const ChatHistoryDrawer = ({ data }: ChatHistoryDrawerProps) => {
const [drawerOpen, setDrawerOpen] = React.useState(false);
const onHistoryButtonClick = () => {
setDrawerOpen(true);
};
const closeDrawer = React.useCallback(() => {
setDrawerOpen(false);
}, []);
return (
);
};
================================================
FILE: components/modules/apps/chat/ChatHistoryItem.tsx
================================================
import React from "react";
import Link from "next/link";
import { MessageCircle } from "lucide-react";
import { Chat } from "@/lib/db";
import { cn } from "@/lib/utils";
import { DeleteChatAction } from "./DeleteChatAction";
import { EditChatAction } from "./EditChatAction";
type ChatHistoryItemProps = {
chat: Chat;
isActive: boolean;
closeDrawer?: () => void;
};
export const ChatHistoryItem = ({
chat,
isActive,
closeDrawer,
}: ChatHistoryItemProps) => {
const renderActionButtons = () => {
return (
);
};
return (
{chat.name}
{renderActionButtons()}
);
};
================================================
FILE: components/modules/apps/chat/ChatLayout.tsx
================================================
import { MainLayout } from "@/components/ui/common/MainLayout";
interface ChatLayoutProps {
children: React.ReactNode;
leftSidebarElement: React.ReactNode;
}
export const ChatLayout = ({
children,
leftSidebarElement,
}: ChatLayoutProps) => {
return (
{leftSidebarElement}
{children}
);
};
================================================
FILE: components/modules/apps/chat/ChatPanel.tsx
================================================
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { Message, useChat } from "ai/react";
import { useForm } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { containsChatBotTrigger } from "@/lib/chat-input";
import { Chat, ChatMemberProfile, Message as SupabaseMessage } from "@/lib/db";
import { useProfileStore } from "@/lib/stores/profile";
import {
RealtimeChatMemberStatus,
useSubscribeChatMessages,
} from "@/hooks/useSubscribeChatMessages";
import { ChatList } from "@/components/ui/chat";
import { ChatScrollAnchor } from "@/components/ui/common/ChatScrollAnchor";
import { Separator } from "@/components/ui/Separator";
import { Sheet } from "@/components/ui/Sheet";
import { useToast } from "@/components/ui/use-toast";
import { revalidateChatLayout } from "./action";
import { ChatForm } from "./ChatForm";
import { ControlSidebarSheet } from "./control-side-bar/ControlSidebarSheet";
import { defaultSystemPrompt } from "./control-side-bar/data/models";
import { Header } from "./Header";
import { ChatParamSchema } from "./schema";
import { ChatParams } from "./types";
import { buildChatRequestParams } from "./utils";
const defaultValues: ChatParams = {
description: defaultSystemPrompt,
model: "gpt-3.5-turbo",
temperature: [1],
topP: [0.5],
maxTokens: [250],
frequencyPenalty: [0],
presencePenalty: [0],
};
export type ChatPanelProps = {
chatId: Chat["id"];
initialMessages: Message[];
chats: Chat[] | null;
chatParams?: ChatParams;
isNewChat?: boolean;
isChatHost?: boolean;
chatMembers: ChatMemberProfile[] | null;
defaultMemberSidebarLayout: number[];
};
export const ChatPanel = ({
chatId,
chats,
initialMessages,
chatParams,
isNewChat,
isChatHost,
chatMembers,
defaultMemberSidebarLayout,
}: ChatPanelProps) => {
const { toast } = useToast();
const profile = useProfileStore((state) => state.profile);
const scrollAreaRef = React.useRef(null);
const [sidebarSheetOpen, setSidebarSheetOpen] = React.useState(false);
const router = useRouter();
const [chatMemberWithStatus, setChatMemberWithStatus] = React.useState<
ChatMemberProfile[] | null
>(chatMembers);
const {
messages,
input,
setInput,
handleInputChange,
isLoading,
stop,
reload,
error,
setMessages,
append,
} = useChat({
id: chatId,
api: "/api/chat",
initialMessages,
sendExtraMessageFields: true,
onResponse: async (response) => {
if (response?.headers.get("should-redirect-to-new-chat") === "true") {
await revalidateChatLayout();
router.replace(`/apps/chat/${chatId}`);
}
},
onFinish: async () => {
if (isNewChat) {
await revalidateChatLayout();
router.replace(`/apps/chat/${chatId}`);
}
},
});
const handleChatMemberPresense = React.useCallback(
(newState: RealtimeChatMemberStatus) => {
if (!chatMembers?.length) {
return;
}
const onlineMemberProfileIds = Object.values(newState).map(
(value) => value[0].userId
);
const updatedChatMembers: ChatMemberProfile[] = chatMembers.map(
(member) => ({
...member,
status: onlineMemberProfileIds.includes(member.profiles?.id || "")
? "online"
: "offline",
})
);
setChatMemberWithStatus(updatedChatMembers);
},
[chatMembers]
);
const handleNewMessageInsert = React.useCallback(
(newMessages: Message[]) => {
setMessages(newMessages);
},
[setMessages]
);
useSubscribeChatMessages({
initialMessages: messages,
chatId,
currentUserId: profile?.id,
newMessageInsertCallback: handleNewMessageInsert,
chatMemberPresenceCallback: handleChatMemberPresense,
});
const formReturn = useForm({
defaultValues: chatParams || defaultValues,
mode: "onChange",
resolver: zodResolver(ChatParamSchema),
});
const chatRequestParams = React.useMemo(() => {
const formValues = formReturn.getValues();
return buildChatRequestParams(formValues);
}, [formReturn]);
React.useEffect(() => {
if (error) {
toast({
title: "Error",
description:
"AI Assistant is not available at the moment. Please try again later.",
variant: "destructive",
});
}
}, [error, toast]);
React.useEffect(() => {
if (!messages.length) {
return;
}
scrollAreaRef.current?.scrollTo({
top: scrollAreaRef.current.scrollHeight,
});
window.scrollTo({ top: document.body.scrollHeight });
}, [messages.length]);
React.useEffect(() => {
scrollAreaRef.current?.scrollTo({
top: scrollAreaRef.current.scrollHeight,
});
window.scrollTo({ top: document.body.scrollHeight });
}, []);
const handleOnChange = (
e:
| React.ChangeEvent
| React.ChangeEvent
) => {
handleInputChange(e);
};
const handleReloadMessages = React.useCallback(
(id: SupabaseMessage["id"]) => {
reload({
options: {
body: {
...chatRequestParams,
chatId,
isRegenerate: true,
regenerateMessageId: id,
},
},
});
},
[chatId, chatRequestParams]
);
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input || isLoading) {
return;
}
append(
{
content: input,
role: "user",
id: uuidv4(),
data: {
profile_id: profile?.id || "",
chat_id: chatId,
},
},
{
options: {
body: {
...chatRequestParams,
chatId,
isNewChat,
enableChatAssistant: containsChatBotTrigger(input),
},
},
}
);
setInput("");
};
const closeSidebarSheet = React.useCallback(() => {
setSidebarSheetOpen(false);
}, []);
return (
);
};
================================================
FILE: components/modules/apps/chat/CodeBlock.tsx
================================================
// Inspired by Chatbot-UI and modified to fit the needs of this project
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
"use client";
import React, { FC, memo } from "react";
import { Check, Copy, Download } from "lucide-react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import {
oneDark,
oneLight,
} from "react-syntax-highlighter/dist/cjs/styles/prism";
import { useActiveThemeColor } from "@/hooks/useActiveTheme";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import { Button } from "@/components/ui/Button";
interface Props {
language: string;
value: string;
}
interface languageMap {
[key: string]: string | undefined;
}
export const programmingLanguages: languageMap = {
javascript: ".js",
python: ".py",
java: ".java",
c: ".c",
cpp: ".cpp",
"c++": ".cpp",
"c#": ".cs",
ruby: ".rb",
php: ".php",
swift: ".swift",
"objective-c": ".m",
kotlin: ".kt",
typescript: ".ts",
go: ".go",
perl: ".pl",
rust: ".rs",
scala: ".scala",
haskell: ".hs",
lua: ".lua",
shell: ".sh",
sql: ".sql",
html: ".html",
css: ".css",
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
};
export const generateRandomString = (length: number, lowercase = false) => {
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return lowercase ? result.toLowerCase() : result;
};
const CodeBlock: FC = memo(({ language, value }) => {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
const [codeBlockStyle, setCodeBlockStyle] = React.useState(oneLight);
const theme = useActiveThemeColor();
React.useEffect(() => {
setCodeBlockStyle(theme === "dark" ? oneDark : oneLight);
}, [theme]);
const downloadAsFile = () => {
if (typeof window === "undefined") {
return;
}
const fileExtension = programmingLanguages[language] || ".file";
const suggestedFileName = `file-${generateRandomString(
3,
true
)}${fileExtension}`;
const fileName = window.prompt("Enter file name" || "", suggestedFileName);
if (!fileName) {
// User pressed cancel on prompt.
return;
}
const blob = new Blob([value], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = fileName;
link.href = url;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const onCopy = () => {
if (isCopied) return;
copyToClipboard(value);
};
return (
{language}
Download
{isCopied ? : }
Copy code
{value}
);
});
CodeBlock.displayName = "CodeBlock";
export { CodeBlock };
================================================
FILE: components/modules/apps/chat/DeleteChatAction.tsx
================================================
import React from "react";
import { useRouter } from "next/navigation";
import { Loader, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/AlertDialog";
import { Button, buttonVariants } from "@/components/ui/Button";
import { toast } from "@/components/ui/use-toast";
import { deleteChat } from "./action";
import { ChatActionProps } from "./types";
export const DeleteChatAction = ({ chat, ...rest }: ChatActionProps) => {
const [isAlertOpen, setIsAlertOpen] = React.useState(false);
const [pendingDeleteChat, startDeleteChat] = React.useTransition();
const { replace } = useRouter();
const chatIdFromPathName = useChatIdFromPathName();
const onDelete = (e: React.MouseEvent) => {
e.preventDefault();
startDeleteChat(async () => {
try {
await deleteChat(chat.id);
toast({
title: "Success",
description: "Your chat has been deleted.",
});
if (chatIdFromPathName === chat.id) {
replace("/apps/chat");
}
setIsAlertOpen(false);
} catch (error) {
toast({
title: "Error",
description: "Failed to delete chat. Please try again.",
variant: "destructive",
});
}
});
};
return (
Are you sure you want to delete this chat?
This action cannot be undone. This will permanently delete your
chat.
Cancel
{pendingDeleteChat ? (
) : (
"Delete"
)}
);
};
================================================
FILE: components/modules/apps/chat/EditChatAction.tsx
================================================
import React from "react";
import { Edit, Loader } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/AlertDialog";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { toast } from "@/components/ui/use-toast";
import { updateChat } from "./action";
import { ChatActionProps } from "./types";
export const EditChatAction = ({ chat, ...rest }: ChatActionProps) => {
const [isAlertOpen, setIsAlertOpen] = React.useState(false);
const [pendingUpdateChat, startUpdateChat] = React.useTransition();
const [inputValue, setInputValue] = React.useState(chat.name || "");
const handleDelete = () => {
startUpdateChat(async () => {
try {
await updateChat({
id: chat.id,
name: inputValue,
});
toast({
title: "Success",
description: "Your chat has been updated.",
});
setIsAlertOpen(false);
} catch (error) {
toast({
title: "Error",
description: "Failed to update chat. Please try again.",
variant: "destructive",
});
}
});
};
const onChange = (e: React.ChangeEvent) => {
setInputValue(e.target.value);
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
handleDelete();
}
};
const onEdit = (e: React.MouseEvent) => {
e.stopPropagation();
};
const onDelete = (e: React.MouseEvent) => {
e.preventDefault();
handleDelete();
};
return (
Edit your chat title
Cancel
{pendingUpdateChat ? (
) : (
"Update"
)}
);
};
================================================
FILE: components/modules/apps/chat/Header.tsx
================================================
import React from "react";
import { Heading2 } from "@/components/ui/typography";
export const Header = () => {
return (
GPT AI Assistant
);
};
================================================
FILE: components/modules/apps/chat/MobileDrawerControls.tsx
================================================
import React from "react";
import { PanelRight } from "lucide-react";
import { Chat } from "@/lib/db";
import { Button } from "@/components/ui/Button";
import { SheetTrigger } from "@/components/ui/Sheet";
import { ChatHistoryDrawer } from "./ChatHistoryDrawer";
type MobileDrawerControlProps = {
chats: Chat[] | null;
};
export const MobileDrawerControl = React.memo(function MobileDrawerControl({
chats,
}: MobileDrawerControlProps) {
return (
<>
>
);
});
================================================
FILE: components/modules/apps/chat/NewChatButton.tsx
================================================
"use client";
import React from "react";
import Link from "next/link";
import { Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/Button";
type NewChatButtonProps = {
closeDrawer?: () => void;
};
export const NewChatButton = ({ closeDrawer }: NewChatButtonProps) => {
return (
);
};
================================================
FILE: components/modules/apps/chat/SystemPromptControl.tsx
================================================
import React from "react";
import { Message, UseChatHelpers } from "ai/react";
import { useFormContext } from "react-hook-form";
import { Button } from "@/components/ui/Button";
import { Label } from "@/components/ui/Label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/Popover";
import { TextArea } from "@/components/ui/TextArea";
import { Subtle } from "@/components/ui/typography";
import { ChatParams } from "./types";
type SystemPromptControlProps = Pick<
UseChatHelpers,
"setMessages" | "messages"
>;
export const SystemPromptControl = ({
setMessages,
messages,
}: SystemPromptControlProps) => {
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const { getValues, setValue } = useFormContext();
const formValues = getValues();
const { description } = formValues;
const [systemPromptInputValue, setSystemPromptInputValue] = React.useState<
string | undefined
>(description);
const handlePopoverOpenChange = (isOpen: boolean) => {
if (isOpen) {
if (description !== systemPromptInputValue) {
setSystemPromptInputValue(description);
}
}
setIsPopoverOpen(isOpen);
};
const handleSystemPromptInputChange = (
e: React.ChangeEvent
) => {
setSystemPromptInputValue(e.target.value);
};
const handleSave = () => {
if (!systemPromptInputValue) {
return;
}
const systemMessage: Message = {
role: "system",
content: systemPromptInputValue,
id: "system-prompt",
};
setMessages([systemMessage, ...messages]);
setValue("description", systemPromptInputValue);
setIsPopoverOpen(false);
};
return (
Set system prompt
{`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.`}
Done
);
};
================================================
FILE: components/modules/apps/chat/action.ts
================================================
"use server";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import { TablesUpdate } from "@/lib/db";
import {
deleteChat as deleteChatDb,
updateChat as updateChatDb,
} from "@/lib/db/chats";
import { createClient } from "@/lib/supabase/server";
export const deleteChat = async (id: string) => {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
try {
await deleteChatDb(supabase, id);
revalidatePath(`/apps`, "layout");
} catch (error) {
throw new Error("Failed to delete chat");
}
};
export const updateChat = async (params: TablesUpdate<"chats">) => {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const { id, ...rest } = params;
if (!id) {
throw new Error("Missing ID");
}
try {
await updateChatDb(supabase, {
id,
...rest,
});
revalidatePath(`/apps`, "layout");
} catch (error) {
throw new Error("Failed to update chat");
}
};
export const revalidateChatLayout = async () => {
revalidatePath("/apps", "layout");
};
================================================
FILE: components/modules/apps/chat/chat-members/AddMembersForm.tsx
================================================
import React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { SubmitHandler, useForm } from "react-hook-form";
import z from "zod";
import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
import { Button } from "@/components/ui/Button";
import { InputField } from "@/components/ui/form/form-fields";
import { useToast } from "@/components/ui/use-toast";
import { addNewMember } from "./action";
import { addMemberSchema } from "./schema";
type AddMembersFormProps = {
onCloseAddMemberPopover: () => void;
};
type AddMembersFormParams = z.infer;
const defaultValues: AddMembersFormParams = {
username: "",
};
export const AddMembersForm = ({
onCloseAddMemberPopover,
}: AddMembersFormProps) => {
const { toast } = useToast();
const chatId = useChatIdFromPathName();
const [isPending, startTransition] = React.useTransition();
const { handleSubmit, formState, register } = useForm({
defaultValues: defaultValues,
mode: "onChange",
resolver: zodResolver(addMemberSchema),
});
const fieldProps = { register, formState };
const onSubmit: SubmitHandler = (data) => {
handleAddMember(data.username);
};
const handleAddMember = async (username: string) => {
startTransition(async () => {
try {
await addNewMember(username, chatId);
toast({
title: "Success",
description: `${username} has been added to this chat.`,
});
} catch (error) {
toast({
title: "Error",
description:
"Failed to add the member to this chat. Please try again.",
variant: "destructive",
});
} finally {
onCloseAddMemberPopover();
}
});
};
return (
);
};
================================================
FILE: components/modules/apps/chat/chat-members/ChatMemberItem.tsx
================================================
import React from "react";
import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
import { UserAvatar } from "@/components/ui/common/UserAvatar";
import { DeleteMemberAction } from "./DeleteMemberAction";
type ChatMemberItemProps = {
id: string;
username: string;
avatarUrl: string | null;
fullname?: string;
removeable?: boolean;
isOnline?: boolean;
};
export const ChatMemberItem = ({
id,
fullname,
username,
avatarUrl,
removeable = false,
isOnline,
}: ChatMemberItemProps) => {
const chatId = useChatIdFromPathName();
return (
);
};
================================================
FILE: components/modules/apps/chat/chat-members/ChatMembers.tsx
================================================
"use client";
import React from "react";
import { Plus } from "lucide-react";
import { ChatMemberProfile } from "@/lib/db";
import { useProfileStore } from "@/lib/stores/profile";
import { Button } from "@/components/ui/Button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/Popover";
import { Separator } from "@/components/ui/Separator";
import { Paragraph } from "@/components/ui/typography";
import { ChatPanelProps } from "../ChatPanel";
import { AddMembersForm } from "./AddMembersForm";
import { ChatMemberItem } from "./ChatMemberItem";
type ChatMembersProps = {
data: ChatMemberProfile[] | null;
isChatHost: ChatPanelProps["isChatHost"];
closeDrawer?: () => void;
};
export const ChatMembers = ({ data, isChatHost }: ChatMembersProps) => {
const [addMemberPopoverOpen, setAddMemberPopoverOpen] = React.useState(false);
const currentProfile = useProfileStore((state) => state.profile);
const handleAddMemberPopoverOpen = (isOpen: boolean) => {
setAddMemberPopoverOpen(isOpen);
};
const closeAddMemberPopover = () => {
setAddMemberPopoverOpen(false);
};
return (
{data?.length ? null : (
No members in this chat.
)}
{data?.map((member) => {
const { profiles, id } = member;
if (!profiles) return null;
let removeable = false;
const isMemberChat =
currentProfile && currentProfile.id !== profiles.id ? true : false;
if (isChatHost && isMemberChat) {
removeable = true;
}
return (
);
})}
);
};
================================================
FILE: components/modules/apps/chat/chat-members/DeleteMemberAction.tsx
================================================
import React from "react";
import { AlertDialogProps } from "@radix-ui/react-alert-dialog";
import { Loader, Trash2 } from "lucide-react";
import { Chat, Profile } from "@/lib/db";
import { cn } from "@/lib/utils";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/AlertDialog";
import { Button, buttonVariants } from "@/components/ui/Button";
import { useToast } from "@/components/ui/use-toast";
import { deleteMember } from "./action";
type ChatActionProps = {
memberId: Profile["id"];
memberUsername: Profile["username"];
chatId: Chat["id"];
} & AlertDialogProps;
export const DeleteMemberAction = ({
memberId,
memberUsername,
chatId,
...rest
}: ChatActionProps) => {
const { toast } = useToast();
const [isAlertOpen, setIsAlertOpen] = React.useState(false);
const [pendingDeleteMember, startDeleteMember] = React.useTransition();
const handleRemoveMember = (
e: React.MouseEvent
) => {
e.preventDefault();
startDeleteMember(async () => {
try {
await deleteMember(memberId, chatId);
toast({
title: "Success",
description: `${memberUsername || "The member"} has been removed to this chat.`,
});
} catch (error) {
toast({
title: "Error",
description:
"Failed to remove the member to this chat. Please try again.",
variant: "destructive",
});
} finally {
setIsAlertOpen(false);
}
});
};
return (
Are you sure you want to remove{" "}
"{memberUsername}"
from this chat?
This action cannot be undone.
Cancel
{pendingDeleteMember ? (
) : (
"Delete"
)}
);
};
================================================
FILE: components/modules/apps/chat/chat-members/action.ts
================================================
"use server";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import { createNewChatMember, deleteChatMember } from "@/lib/db/chat-members";
import { getProfileByUsername } from "@/lib/db/profile";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
export const addNewMember = async (username: string, chatId: string) => {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
if (!user) {
throw new Error("You must be logged in to create a chat");
}
const profile = await getProfileByUsername(supabase, username);
if (!profile) {
throw new Error("Profile not found");
}
const newMember = await createNewChatMember(supabase, {
chat_id: chatId,
member_id: profile.id,
});
if (!newMember) {
throw new Error("Failed to add the member to this chat");
}
revalidatePath(`/apps/chat/${chatId}`);
return newMember;
};
export const deleteMember = async (memberId: string, chatId: string) => {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
if (!user) {
throw new Error("You must be logged in to delete a chat member");
}
try {
await deleteChatMember(supabase, memberId);
revalidatePath(`/apps/chat/${chatId}`);
} catch (error) {
throw new Error("Failed to remove the member from this chat");
}
return null;
};
================================================
FILE: components/modules/apps/chat/chat-members/index.ts
================================================
export * from "./ChatMembers";
================================================
FILE: components/modules/apps/chat/chat-members/schema.ts
================================================
import z from "zod";
export const addMemberSchema = z.object({
username: z.string(),
});
================================================
FILE: components/modules/apps/chat/control-side-bar/ControlSidebar.tsx
================================================
import React from "react";
import { UseChatHelpers } from "ai/react";
import { Loader } from "lucide-react";
import { useFormContext } from "react-hook-form";
import { useChatIdFromPathName } from "@/hooks/useChatIdFromPathName";
import { Button } from "@/components/ui/Button";
import { Separator } from "@/components/ui/Separator";
import {
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/Sheet";
import { toast } from "@/components/ui/use-toast";
import { SystemPromptControl } from "../SystemPromptControl";
import { ChatParams } from "../types";
import { updateChatSettings } from "./action";
import { models, types } from "./data/models";
import { FrequencyPenaltySelector } from "./FrequencyPenaltySelector";
import { MaxLengthSelector } from "./MaxLengthSelector";
import { ModelSelector } from "./ModelSelector";
import { PresencePenaltySelector } from "./PresencePenaltySelector";
import { TemperatureSelector } from "./TemperatureSelector";
import { TopPSelector } from "./TopPSelector";
type ControlSidebarProps = Pick & {
closeSidebarSheet?: () => void;
isNewChat?: boolean;
};
export const ControlSidebar = ({
setMessages,
messages,
closeSidebarSheet,
isNewChat,
}: ControlSidebarProps) => {
const [pendingUpdateSettings, startUpdateSettings] = React.useTransition();
const currentChatId = useChatIdFromPathName();
const { getValues } = useFormContext();
const onSave = () => {
if (!currentChatId) {
return;
}
const formValues = getValues();
startUpdateSettings(async () => {
try {
updateChatSettings(currentChatId, formValues);
toast({
title: "Success",
description: "Your chat settings have been saved.",
});
closeSidebarSheet?.();
} catch (error) {
toast({
title: "Error",
description: "Failed to save chat settings. Please try again.",
variant: "destructive",
});
}
});
};
return (
<>
Settings
{`Combining these parameters allows you to fine-tune the AI's output to suit different use cases.`}
{!isNewChat && (
{pendingUpdateSettings ? (
) : (
"Save"
)}
)}
>
);
};
================================================
FILE: components/modules/apps/chat/control-side-bar/ControlSidebarSheet.tsx
================================================
import React from "react";
import { Message, UseChatHelpers } from "ai/react";
import { FormProvider, FormProviderProps } from "react-hook-form";
import {
CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE,
MAX_CHAT_MEMBER_SIDEBAR_SIZE,
MIN_CHAT_MEMBER_SIDEBAR_SIZE,
} from "@/lib/contants";
import { ChatMemberProfile } from "@/lib/db";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/Resizable";
import { SheetContent } from "@/components/ui/Sheet";
import { ChatMembers } from "../chat-members";
import { ChatPanelProps } from "../ChatPanel";
import { ChatParams } from "../types";
import { ControlSidebar } from "./ControlSidebar";
type ControlSidebarSheetProps = {
setMessages: UseChatHelpers["setMessages"];
messages: Message[];
chatMembers: ChatMemberProfile[] | null;
closeSidebarSheet: () => void;
isNewChat: ChatPanelProps["isNewChat"];
isChatHost: ChatPanelProps["isChatHost"];
formReturn: Omit, "children">;
defaultMemberSidebarLayout: number[];
};
export const ControlSidebarSheet = React.memo(function ControlSidebarSheet({
setMessages,
messages,
chatMembers,
closeSidebarSheet,
isNewChat,
isChatHost,
formReturn,
defaultMemberSidebarLayout,
}: ControlSidebarSheetProps) {
const onLayout = (sizes: number[]) => {
document.cookie = `${CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE}=${JSON.stringify(sizes)}`;
};
const renderControlSidebar = () => {
return (
);
};
return (
{renderControlSidebar()}
{!isNewChat && (
)}
{isNewChat ? (
renderControlSidebar()
) : (
{renderControlSidebar()}
)}
);
});
================================================
FILE: components/modules/apps/chat/control-side-bar/FrequencyPenaltySelector.tsx
================================================
"use client";
import * as React from "react";
import { useFormContext } from "react-hook-form";
import { SliderField } from "@/components/ui/form/form-fields";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/HoverCard";
import { ChatParams } from "../types";
export function FrequencyPenaltySelector() {
const { control, formState, getValues } = useFormContext();
return (
Adjust this to encourage the AI to use less common words. A higher
value like 1.2 makes it prefer unique words, while a lower value
like 0.8 lets it use common words more often.
);
}
================================================
FILE: components/modules/apps/chat/control-side-bar/MaxLengthSelector.tsx
================================================
"use client";
import * as React from "react";
import { useFormContext } from "react-hook-form";
import { SliderField } from "@/components/ui/form/form-fields";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/HoverCard";
import { ChatParams } from "../types";
export function MaxLengthSelector() {
const { control, formState, getValues } = useFormContext();
return (
{`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.`}
);
}
================================================
FILE: components/modules/apps/chat/control-side-bar/ModelSelector.tsx
================================================
"use client";
import * as React from "react";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { PopoverProps } from "@radix-ui/react-popover";
import { useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils";
import { useMutationObserver } from "@/hooks/useMutationObserver";
import { Button } from "@/components/ui/Button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/Command";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/HoverCard";
import { Label } from "@/components/ui/Label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/Popover";
import { ChatParams } from "../types";
import { Model, ModelType } from "./data/models";
interface ModelSelectorProps extends PopoverProps {
types: readonly ModelType[];
models: Model[];
}
export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
const [open, setOpen] = React.useState(false);
const [selectedModel, setSelectedModel] = React.useState(models[0]);
const [peekedModel, setPeekedModel] = React.useState(models[0]);
const { getValues, setValue } = useFormContext();
const modelValue = getValues("model");
React.useEffect(() => {
if (modelValue) {
const model = models.find((model) => model.name === modelValue);
if (model) {
setSelectedModel(model);
}
}
}, [modelValue, models]);
const onModelSelect = (model: Model) => {
return () => {
setSelectedModel(model);
setValue("model", model.name);
setOpen(false);
};
};
const onModelPeek = (model: Model) => {
setPeekedModel(model);
};
return (
Model
{`This is like choosing the AI's brain. Different models have different knowledge and abilities. You pick the model that suits your task. The model determines what the AI can do.`}
{selectedModel ? selectedModel.name : "Select a model..."}
{peekedModel.name}
{peekedModel.description}
No Models found.
{types.map((type) => (
{models
.filter((model) => model.type === type)
.map((model) => (
))}
))}
);
}
interface ModelItemProps {
model: Model;
isSelected: boolean;
onSelect: () => void;
onPeek: (model: Model) => void;
}
function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
const ref = React.useRef(null);
useMutationObserver(ref, (mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes") {
const target = mutation.target;
if (
target instanceof HTMLElement &&
target.getAttribute("aria-selected") === "true"
) {
onPeek(model);
}
}
}
});
return (
{model.name}
);
}
================================================
FILE: components/modules/apps/chat/control-side-bar/PresencePenaltySelector.tsx
================================================
"use client";
import * as React from "react";
import { useFormContext } from "react-hook-form";
import { SliderField } from "@/components/ui/form/form-fields";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/HoverCard";
import { ChatParams } from "../types";
export function PresencePenaltySelector() {
const { control, formState, getValues } = useFormContext();
return (
This nudges the AI to include specific topics or words in its
responses. Use a higher value like 1.2 to make sure it talks about
certain things, or a lower value like 0.8 for more freedom in topic
choice.
);
}
================================================
FILE: components/modules/apps/chat/control-side-bar/TemperatureSelector.tsx
================================================
"use client";
import * as React from "react";
import { useFormContext } from "react-hook-form";
import { SliderField } from "@/components/ui/form/form-fields/SliderField";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/HoverCard";
import { ChatParams } from "../types";
export function TemperatureSelector() {
const { control, formState, getValues } = useFormContext();
return (
{`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.`}
);
}
================================================
FILE: components/modules/apps/chat/control-side-bar/TopPSelector.tsx
================================================
"use client";
import * as React from "react";
import { useFormContext } from "react-hook-form";
import { SliderField } from "@/components/ui/form/form-fields";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/HoverCard";
import { ChatParams } from "../types";
export function TopPSelector() {
const { control, formState, getValues } = useFormContext();
return (
It determines the randomness in AI responses. Higher values like 0.8
mean more randomness, while lower values like 0.2 make responses
more focused on a single idea or word.
We generally recommend altering this or Creativity but not both.
);
}
================================================
FILE: components/modules/apps/chat/control-side-bar/action.ts
================================================
"use server";
import { cookies } from "next/headers";
import { Chat, TablesUpdate } from "@/lib/db";
import { getAppBySlug } from "@/lib/db/apps";
import { updateChat } from "@/lib/db/chats";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
export const updateChatSettings = async (
id: Chat["id"],
params: TablesUpdate<"chats">["settings"]
) => {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
const currentApp = await getAppBySlug(supabase, "/apps/chat");
if (!currentApp || !user) {
throw new Error("You must be logged in to create a chat");
}
const currentProfileId = user.id;
try {
await updateChat(supabase, {
id: id,
settings: params,
profile_id: currentProfileId,
app_id: currentApp.id,
});
} catch (error) {
throw new Error("Failed to save chat settings");
}
};
================================================
FILE: components/modules/apps/chat/control-side-bar/data/models.ts
================================================
export const defaultSystemPrompt =
"You are an AI assistant. You can answer questions, generate code snippets, and more.";
export const types = ["GPT-3.5", "GPT-4"] as const;
export type ModelType = (typeof types)[number];
export interface Model {
id: string;
name: string;
description: string;
type: Type;
}
export const models: Model[] = [
{
id: "1",
name: "gpt-3.5-turbo",
description:
"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.",
type: "GPT-3.5",
},
{
id: "2",
name: "gpt-3.5-turbo-16k",
description:
" Same capabilities as the standard gpt-3.5-turbo model but with 4 times the context.",
type: "GPT-3.5",
},
{
id: "3",
name: "gpt-3.5-turbo-0613",
description:
"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.",
type: "GPT-3.5",
},
{
id: "4",
name: "gpt-4",
description:
"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.",
type: "GPT-4",
},
{
id: "5",
name: "gpt-4-0613",
description:
"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.",
type: "GPT-4",
},
{
id: "6",
name: "gpt-4-32k",
description:
"Same capabilities as the standard gpt-4 mode but with 4x the context length. Will be updated with our latest model iteration.",
type: "GPT-4",
},
];
================================================
FILE: components/modules/apps/chat/control-side-bar/index.ts
================================================
export * from "./ControlSidebar";
================================================
FILE: components/modules/apps/chat/schema.ts
================================================
import { z } from "zod";
// create schema based on CompletionCreateParams from openai
export const ChatParamSchema = z
.object({
model: z.string().optional(),
description: z.string().optional(),
temperature: z.array(z.number()).optional(),
maxTokens: z.array(z.number()).optional(),
topP: z.array(z.number()).optional(),
presencePenalty: z.array(z.number()).optional(),
frequencyPenalty: z.array(z.number()).optional(),
})
.strict();
================================================
FILE: components/modules/apps/chat/types.ts
================================================
import { AlertDialogProps } from "@radix-ui/react-alert-dialog";
import { z } from "zod";
import { Chat } from "@/lib/db";
import { ChatParamSchema } from "./schema";
export type ChatParams = z.infer;
export type ChatActionProps = {
chat: Chat;
} & Omit;
================================================
FILE: components/modules/apps/chat/utils.ts
================================================
import { ChatParams } from "./types";
export const buildChatRequestParams = (formValues: ChatParams) => {
const {
model,
temperature,
topP,
maxTokens,
frequencyPenalty,
presencePenalty,
description,
} = formValues;
return {
model,
description,
temperature: temperature?.[0],
topP: topP?.[0],
maxTokens: maxTokens?.[0],
frequencyPenalty: frequencyPenalty?.[0],
presencePenalty: presencePenalty?.[0],
};
};
================================================
FILE: components/modules/auth/LogoutButton.tsx
================================================
"use client";
import React from "react";
import { LogOut } from "lucide-react";
import { Button } from "@/components/ui/Button";
export default function LogoutButton() {
return (
);
}
================================================
FILE: components/modules/auth/SocialLoginButton.tsx
================================================
import React from "react";
import { Provider } from "@supabase/supabase-js";
import { Loader } from "lucide-react";
import { getURL } from "@/config/site";
import { createClient } from "@/lib/supabase/client";
import { Button, ButtonProps } from "@/components/ui/Button";
import { useToast } from "@/components/ui/use-toast";
type SocialLoginButtonProps = ButtonProps & {
provider: Provider;
};
export const SocialLoginButton = ({
provider,
children,
...rest
}: SocialLoginButtonProps) => {
const supabase = createClient();
const [isLoading, setIsLoading] = React.useState(false);
const { toast } = useToast();
const signIn = async () => {
setIsLoading(true);
const { error } = await supabase.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo: `${getURL()}api/auth/callback`,
},
});
if (error) {
setIsLoading(false);
return toast({
title: "Error",
description: "Your email or password is incorrect. Please try again.",
variant: "destructive",
});
}
};
return (
{isLoading ? : children}
);
};
================================================
FILE: components/modules/auth/SocialLoginOptions.tsx
================================================
import React from "react";
import { Github } from "lucide-react";
import { CustomIcon } from "@/components/ui/CustomIcon";
import { SocialLoginButton } from "./SocialLoginButton";
export const SocialLoginOptions = () => {
return (
);
};
================================================
FILE: components/modules/auth/UserAuthForm.tsx
================================================
"use client";
import React from "react";
import Link from "next/link";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader } from "lucide-react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/Button";
import { InputField } from "@/components/ui/form/form-fields";
import { useToast } from "@/components/ui/use-toast";
import { credentialAuthSchema } from "./schema";
import { SocialLoginOptions } from "./SocialLoginOptions";
type UserAuthFormProps = React.HTMLAttributes;
type FormData = z.infer;
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const supabase = createClient();
const { register, formState, handleSubmit } = useForm({
mode: "onChange",
resolver: zodResolver(credentialAuthSchema),
});
const [isLoading, setIsLoading] = React.useState(false);
const { toast } = useToast();
const fieldProps = { register, formState };
async function onSubmit(data: FormData) {
setIsLoading(true);
const signInResult = await supabase.auth.signInWithPassword({
email: data.email,
password: data.password,
});
if (signInResult?.error) {
setIsLoading(false);
return toast({
title: "Error",
description: "Your email or password is incorrect. Please try again.",
variant: "destructive",
});
}
window.location.href = "/apps/chat";
}
return (
{`Don't have an account yet?`}
Sign Up
);
}
================================================
FILE: components/modules/auth/UserSignupForm.tsx
================================================
"use client";
import React from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader } from "lucide-react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/Button";
import { InputField } from "@/components/ui/form/form-fields";
import { useToast } from "@/components/ui/use-toast";
import { registerProfileSchema } from "./schema";
type UserSignupFormProps = React.HTMLAttributes;
type FormData = z.infer;
export function UserSignupForm({ className, ...props }: UserSignupFormProps) {
const supabase = createClient();
const { replace } = useRouter();
const { register, formState, handleSubmit } = useForm({
mode: "onChange",
resolver: zodResolver(registerProfileSchema),
});
const [isLoading, setIsLoading] = React.useState(false);
const { toast } = useToast();
const fieldProps = { register, formState };
async function onSubmit(data: FormData) {
setIsLoading(true);
const signUpResult = await supabase.auth.signUp({
email: data.email,
password: data.password,
options: {
data: {
full_name: data.fullName,
},
},
});
if (signUpResult?.error) {
setIsLoading(false);
return toast({
title: "Error",
description: "There was an error signing up. Please try again.",
variant: "destructive",
});
}
replace("/apps/chat");
}
return (
Already have an account?
Sign in
);
}
================================================
FILE: components/modules/auth/schema.ts
================================================
import * as z from "zod";
const credentialAuthObject = {
email: z.string().email(),
password: z.string().min(6, "Password must be at least 6 characters long."),
};
export const credentialAuthSchema = z.object({
...credentialAuthObject,
});
export const registerProfileSchema = z
.object({
...credentialAuthObject,
fullName: z.string().optional(),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match.",
path: ["confirmPassword"],
});
================================================
FILE: components/modules/home/DescriptionHeadingText.tsx
================================================
"use client";
import React from "react";
import { motion } from "framer-motion";
export function DescriptionHeadingText() {
const text =
"A feature-rich, highly customizable AI Chatbot Template, powered by Next.js and Supabase.";
const [displayedText, setDisplayedText] = React.useState("");
const [i, setI] = React.useState(0);
React.useEffect(() => {
const typingEffect = setInterval(() => {
if (i < text.length) {
setDisplayedText((prevState) => prevState + text.charAt(i));
setI(i + 1);
} else {
clearInterval(typingEffect);
}
}, 50);
return () => {
clearInterval(typingEffect);
};
}, [i]);
return (
{displayedText
? displayedText
: "A feature-rich, highly customizable AI Chatbot Template, empowered by Next.js x Supabase."}
);
}
================================================
FILE: components/modules/home/FeatureItems.tsx
================================================
"use client";
import React from "react";
import { motion } from "framer-motion";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/Card";
export const FeatureItems = () => {
return (
Modern Web Tools for Chatbot App
Revolutionize the way you build chatbot applications with the power of
Next.js, Server Components, and Supabase. This template provides you
with a solid foundation to create cutting-edge chatbot apps that are
feature-rich and responsive, all while taking advantage of the latest
Next.js technology.
Next.js 14
App Router, Routing, Layouts, Route Handlers and Server Actions.
React 18
Server and Client Components. New Hooks.
Database
Secure and high-performance Postgres backends with Supabase.
Components
Shadcn UI, React Hook Form and styled with Tailwind CSS.
Authentication
Authenticate and authorize your users with Supabase Auth.
OpenAI icon
Open AI
Using Open AI and Vercel AI SDK for building conversational
streaming UI.
);
};
================================================
FILE: components/modules/home/HeroBannerImage.tsx
================================================
"use client";
import React from "react";
import Image from "next/image";
import { motion } from "framer-motion";
import { useActiveThemeColor } from "@/hooks/useActiveTheme";
export const HeroBannerImage = () => {
const [isMounted, setIsMounted] = React.useState(false);
const theme = useActiveThemeColor();
React.useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) return
;
const getImageSrc = () => {
if (theme === "dark") {
return "/featured-dark.jpg";
}
return "/featured.jpg";
};
return (
);
};
================================================
FILE: components/modules/profile/AccountDropdownMenu.tsx
================================================
"use client";
import React from "react";
import Link from "next/link";
import { Loader, User } from "lucide-react";
import { getCurrentProfile } from "@/lib/db/profile";
import { useProfileStore } from "@/lib/stores/profile";
import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/Button";
import { UserAvatar } from "@/components/ui/common/UserAvatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/DropdownMenu";
import LogoutButton from "../auth/LogoutButton";
type AccountDropdownMenuProps = {
userEmail?: string;
};
export const AccountDropdownMenu = ({
userEmail,
}: AccountDropdownMenuProps) => {
const supabase = createClient();
const [isLoading, setIsLoading] = React.useState();
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
setIsMounted(true);
}, []);
const { profile, setProfile } = useProfileStore((state) => state);
React.useEffect(() => {
const fetchProfile = async () => {
setIsLoading(true);
const profile = await getCurrentProfile(supabase);
setIsLoading(false);
if (!profile) {
return;
}
setProfile(profile);
};
fetchProfile();
}, [setProfile, supabase]);
if (!isMounted) {
return null;
}
if (isLoading) {
return (
);
}
if (!profile) {
return (
Signin
);
}
const { username, avatar_url } = profile;
const nameLabel = username || userEmail || "";
return (
Profile
);
};
================================================
FILE: components/modules/profile/Header.tsx
================================================
import React from "react";
import { User } from "@supabase/supabase-js";
import { Profile } from "@/lib/db";
import { UserAvatar } from "@/components/ui/common/UserAvatar";
import { Subtle } from "@/components/ui/typography";
type HeaderProps = {
email: User["email"];
fullName: Profile["full_name"];
username: Profile["username"];
avatarUrl: Profile["avatar_url"];
website: Profile["website"];
};
export const Header = ({
username,
avatarUrl,
email,
fullName,
}: HeaderProps) => {
return (
{fullName ?
{fullName}
: null}
{username ? (
(@{username})
) : null}
{email ?
{email} : null}
);
};
================================================
FILE: components/modules/profile/ProfileForm.tsx
================================================
"use client";
import React from "react";
import Link from "next/link";
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader } from "lucide-react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/Button";
import { InputField } from "@/components/ui/form/form-fields";
import { useToast } from "@/components/ui/use-toast";
import { updateProfile } from "./action";
import { profileSchema } from "./schema";
import { ProfileFormValues } from "./type";
type ProfileFormProps = {
formValues: ProfileFormValues;
} & React.HTMLAttributes;
type FormData = z.infer;
export function ProfileForm({
className,
formValues,
...props
}: ProfileFormProps) {
const [isPendingUpdate, startUpdate] = React.useTransition();
const { register, formState, handleSubmit, reset } = useForm({
mode: "onChange",
resolver: zodResolver(profileSchema),
});
const { toast } = useToast();
const fieldProps = { register, formState };
React.useEffect(() => {
reset(formValues);
}, [formValues, reset]);
async function onSubmit(data: FormData) {
startUpdate(async () => {
try {
await updateProfile(data);
toast({
title: "Success",
description: "Your profile has been updated.",
});
} catch (error) {
toast({
title: "Error",
description: "Failed to update profile. Please try again.",
variant: "destructive",
});
}
});
}
return (
);
}
================================================
FILE: components/modules/profile/action.ts
================================================
"use server";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { ProfileFormValues } from "./type";
export async function updateProfile({
fullName,
username,
website,
}: ProfileFormValues) {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const user = await getCurrentUser(supabase);
if (!user) {
throw new Error("You must be logged in to update your profile");
}
try {
const { error } = await supabase.from("profiles").upsert({
id: user.id,
full_name: fullName,
username,
website,
updated_at: new Date().toISOString(),
});
if (error) {
throw error;
}
revalidatePath("/profile");
} catch (error) {
throw error;
}
}
================================================
FILE: components/modules/profile/schema.ts
================================================
import * as z from "zod";
export const profileSchema = z.object({
fullName: z.string().optional().or(z.literal("")),
username: z.string().optional().or(z.literal("")),
website: z.string().optional().or(z.literal("")),
});
================================================
FILE: components/modules/profile/type.ts
================================================
import { z } from "zod";
import { profileSchema } from "./schema";
export type ProfileFormValues = z.infer;
================================================
FILE: components/navigation/NavigationBar.tsx
================================================
import React from "react";
import { cookies } from "next/headers";
import { Menu } from "lucide-react";
import { getCurrentUser } from "@/lib/session";
import { createClient } from "@/lib/supabase/server";
import { cn } from "@/lib/utils";
import { AccountDropdownMenu } from "../modules/profile/AccountDropdownMenu";
import { ThemeToggle } from "../theme";
import { buttonVariants } from "../ui/Button";
import { AppLogo } from "../ui/common/AppLogo";
import { NavigationMainMenu } from "./NavigationMainMenu";
export const NavigationBar = async () => {
const cookieStore = cookies();
const supabase = await createClient(cookieStore);
const user = await getCurrentUser(supabase);
return (
);
};
================================================
FILE: components/navigation/NavigationMainMenu.tsx
================================================
"use client";
import { Github } from "lucide-react";
import { siteConfig } from "@/config/site";
import { buttonVariants } from "../ui/Button";
import { CustomIcon } from "../ui/CustomIcon";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
} from "../ui/NavigationMenu";
export const NavigationMainMenu = () => {
return (
);
};
================================================
FILE: components/navigation/SideBar.tsx
================================================
import React from "react";
import Link from "next/link";
import { siteConfig } from "@/config/site";
import { ThemeToggle } from "../theme";
import { AppLogo } from "../ui/common/AppLogo";
import { CustomIcon } from "../ui/CustomIcon";
import { Separator } from "../ui/Separator";
export const Sidebar = () => {
return (
);
};
================================================
FILE: components/theme/ThemeToggle.tsx
================================================
"use client";
import React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/Button";
export function ThemeToggle() {
const { setTheme, theme } = useTheme();
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
Toggle theme
);
}
================================================
FILE: components/theme/index.ts
================================================
export * from "./ThemeToggle";
================================================
FILE: components/ui/Accordion.tsx
================================================
"use client";
import React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
svg]:rotate-180",
className
)}
{...props}
>
{children}
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
================================================
FILE: components/ui/AlertDialog.tsx
================================================
"use client";
import React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/Button";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = (
props: AlertDialogPrimitive.AlertDialogPortalProps
) => ;
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
================================================
FILE: components/ui/Avatar.tsx
================================================
"use client";
import React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };
================================================
FILE: components/ui/Badge.tsx
================================================
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"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",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes,
VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
);
}
export { Badge, badgeVariants };
================================================
FILE: components/ui/Button.tsx
================================================
import React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader } from "lucide-react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
},
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
isLoading?: boolean;
}
const Button = React.forwardRef(
(
{
className,
variant,
size,
isLoading,
children,
asChild = false,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button";
return (
{isLoading ? : children}
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
================================================
FILE: components/ui/Card.tsx
================================================
import React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
================================================
FILE: components/ui/Command.tsx
================================================
"use client";
import * as React from "react";
import { DialogProps } from "@radix-ui/react-dialog";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/Dialog";
const Command = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
Command.displayName = CommandPrimitive.displayName;
type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
{children}
);
};
const CommandInput = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>((props, ref) => (
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes) => {
return (
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
================================================
FILE: components/ui/CustomIcon.tsx
================================================
import { LucideProps } from "lucide-react";
export const CustomIcon = {
google: (props: LucideProps) => (
),
x: (props: LucideProps) => (
),
};
================================================
FILE: components/ui/Dialog.tsx
================================================
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = (props: DialogPrimitive.DialogPortalProps) => (
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
Close
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes) => (
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes) => (
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
================================================
FILE: components/ui/DropdownMenu.tsx
================================================
"use client";
import React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
{children}
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, sideOffset = 4, ...props }, ref) => (
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, checked, ...props }, ref) => (
{children}
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes) => {
return (
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
================================================
FILE: components/ui/Flex.tsx
================================================
"use client";
import React from "react";
import { cva, VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const flexVariants = cva("flex", {
variants: {
direction: {
row: "flex-row",
column: "flex-col",
},
justify: {
start: "justify-start",
end: "justify-end",
center: "justify-center",
between: "justify-between",
around: "justify-around",
evenly: "justify-evenly",
},
align: {
start: "items-start",
end: "items-end",
center: "items-center",
baseline: "items-baseline",
stretch: "items-stretch",
},
wrap: {
wrap: "flex-wrap",
nowrap: "flex-nowrap",
wrapReverse: "flex-wrap-reverse",
},
},
});
export interface FlexProps
extends React.ButtonHTMLAttributes,
VariantProps {}
export const Flex = ({
className,
direction,
justify,
align,
wrap,
children,
...props
}: FlexProps) => {
return (
{children}
);
};
================================================
FILE: components/ui/HoverCard.tsx
================================================
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };
================================================
FILE: components/ui/Input.tsx
================================================
import React from "react";
import { cva, VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Label } from "./Label";
const inputVariants = cva(
`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`,
{
variants: {
isError: {
true: "border-red-500 dark:border-red-500",
},
},
}
);
export interface InputProps
extends React.InputHTMLAttributes {
label?: string;
helperText?: string;
containerClassName?: string;
}
const Input = React.forwardRef<
HTMLInputElement,
InputProps & VariantProps
>(
(
{ className, label, helperText, containerClassName, isError, ...props },
ref
) => {
return (
{label ?
{label} : null}
{helperText ? (
{helperText}
) : null}
);
}
);
Input.displayName = "Input";
export { Input };
================================================
FILE: components/ui/Label.tsx
================================================
"use client";
import React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
const Label = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, ...props }, ref) => (
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
================================================
FILE: components/ui/NavigationMenu.tsx
================================================
import * as React from "react";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}{" "}
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
const NavigationListItem = React.forwardRef<
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
>(({ className, title, children, ...props }, ref) => {
return (
{title}
{children}
);
});
NavigationListItem.displayName = "NavigationListItem";
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
NavigationListItem,
};
================================================
FILE: components/ui/Popover.tsx
================================================
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
================================================
FILE: components/ui/Resizable.tsx
================================================
"use client";
import { DragHandleDots2Icon } from "@radix-ui/react-icons";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps) => (
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps & {
withHandle?: boolean;
}) => (
div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
)}
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
================================================
FILE: components/ui/ScrollArea.tsx
================================================
"use client";
import React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, orientation = "vertical", ...props }, ref) => (
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
================================================
FILE: components/ui/Section.tsx
================================================
import React from "react";
import { cva, VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Flex } from "./Flex";
import { Separator } from "./Separator";
import { Heading4 } from "./typography";
const sectionVariants = cva(`rounded-lg bg-white p-4 dark:bg-slate-900`, {
variants: {
compact: {
true: "p-2",
},
shadow: {
true: "shadow",
},
},
});
export type SectionProps = {
title?: string | React.ReactNode;
className?: string;
rightElement?: React.ReactNode;
leftElement?: React.ReactNode;
children?: React.ReactNode;
} & VariantProps;
export const Section = ({
title,
compact,
shadow = true,
className,
leftElement,
rightElement,
children,
}: SectionProps) => {
return (
{title || rightElement || leftElement ? (
{leftElement && {leftElement}
}
{typeof title === "string" ? {title} : title}
{rightElement && {rightElement}
}
) : null}
{title ?
: null}
{children}
);
};
================================================
FILE: components/ui/Select.tsx
================================================
"use client";
import React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, position = "popper", ...props }, ref) => (
{children}
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};
================================================
FILE: components/ui/Separator.tsx
================================================
"use client";
import React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
================================================
FILE: components/ui/Sheet.tsx
================================================
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = (props: SheetPrimitive.DialogPortalProps) => (
);
SheetPortal.displayName = SheetPrimitive.Portal.displayName;
const SheetOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef,
VariantProps {}
const SheetContent = React.forwardRef<
React.ElementRef,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
{children}
Close
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes) => (
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes) => (
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
================================================
FILE: components/ui/Skeleton.tsx
================================================
import { cn } from "@/lib/utils";
function Skeleton({
className,
...props
}: React.HTMLAttributes) {
return (
);
}
export { Skeleton };
================================================
FILE: components/ui/Slider.tsx
================================================
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
import { Label } from "./Label";
export type SliderProps = React.ComponentPropsWithoutRef<
typeof SliderPrimitive.Root
> & {
label?: string;
isError?: boolean;
helperText?: string;
containerClassName?: string;
};
const Slider = React.forwardRef<
React.ElementRef,
SliderProps
>(
(
{
className,
label,
isError,
helperText,
containerClassName,
defaultValue,
onValueChange,
...props
},
ref
) => {
const [value, setValue] = React.useState(defaultValue);
const handleOnValueChange = (value: number[]) => {
onValueChange?.(value);
setValue(value);
};
return (
{label ? (
{label}
{value}
) : null}
{helperText ? (
{helperText}
) : null}
);
}
);
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
================================================
FILE: components/ui/Switch.tsx
================================================
"use client";
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
================================================
FILE: components/ui/Tabs.tsx
================================================
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
================================================
FILE: components/ui/TextArea.tsx
================================================
import React from "react";
import { cva, VariantProps } from "class-variance-authority";
import TextareaAutoResize, {
TextareaAutosizeProps,
} from "react-textarea-autosize";
import { cn } from "@/lib/utils";
import { Label } from "./Label";
export interface TextAreaProps
extends React.TextareaHTMLAttributes {
label?: string;
helperText?: string;
containerClassName?: string;
}
export const textAreaVariants = cva(
"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",
{
variants: {
isError: {
true: "border-red-500 dark:border-red-500",
},
},
}
);
const TextArea = React.forwardRef<
HTMLTextAreaElement,
TextAreaProps & VariantProps & TextareaAutosizeProps
>(
(
{ className, label, helperText, isError, containerClassName, ...props },
ref
) => {
return (
{label ?
{label} : null}
{helperText ? (
{helperText}
) : null}
);
}
);
TextArea.displayName = "TextArea";
export { TextArea };
================================================
FILE: components/ui/Toast.tsx
================================================
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
const Toast = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, variant, ...props }, ref) => {
return (
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef;
type ToastActionElement = React.ReactElement;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};
================================================
FILE: components/ui/Toaster.tsx
================================================
"use client";
import { useToast } from "@/components/ui/use-toast";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "./Toast";
export function Toaster() {
const { toasts } = useToast();
return (
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
{title && {title} }
{description && (
{description}
)}
{action}
);
})}
);
}
================================================
FILE: components/ui/Tooltip.tsx
================================================
"use client";
import React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, sideOffset = 4, ...props }, ref) => (
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
================================================
FILE: components/ui/chat/ChatBubble.tsx
================================================
import React from "react";
import { Copy, RefreshCcw, StopCircle } from "lucide-react";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { isTaggedUserPattern } from "@/lib/chat-input";
import { AI_ASSISTANT_PROFILE } from "@/lib/contants";
import { ChatMemberProfile, Message } from "@/lib/db";
import { cn } from "@/lib/utils";
import { badgeVariants } from "@/components/ui/Badge";
import { CodeBlock } from "@/components/modules/apps/chat/CodeBlock";
import { UserAvatar } from "../common/UserAvatar";
import { ChatProfileHoverCard } from "./ChatProfileHoverCard";
import { MemoizedReactMarkdown } from "./Markdown";
export type ChatBubbleProps = {
id: Message["id"];
prevId?: Message["id"];
direction?: "start" | "end";
avatar?: string;
name: string;
time?: string;
status?: string;
content: string;
isLoading: boolean;
isLast: boolean;
chatMemberMap?: Record;
onCopy: (message: string) => void;
onRegenerate: (id: Message["id"]) => void;
onStopGenerating?: () => void;
};
export const ChatBubble = React.memo(function ChatBubble({
prevId,
content,
name,
avatar,
direction,
status,
time,
isLoading,
isLast,
chatMemberMap,
onCopy,
onRegenerate,
onStopGenerating,
}: ChatBubbleProps) {
const chatClass = cn(`chat-${direction} chat mb-4`, {
"place-items-start grid-cols-[auto_1fr]": direction === "start",
"place-items-end grid-cols-[1fr_auto]": direction === "end",
});
const chatBubbleClass = cn(
"chat-bubble min-h-fit w-auto max-w-full rounded-md px-4 py-2 lg:max-w-[90%]",
{
"bg-secondary text-secondary-foreground": direction === "start",
"bg-primary text-primary-foreground": direction === "end",
}
);
const handleOnCopy = () => {
onCopy(content);
};
const renderActionButtons = () => {
const copyButton = (
Copy
);
if (isLast) {
return isLoading ? (
direction === "start" && (
Stop generating
)
) : (
<>
{copyButton}
{direction === "start" && (
prevId && onRegenerate(prevId)}
>
Regenerate response
)}
>
);
}
return copyButton;
};
return (
{name}
{time ? {time} : null}
{children};
},
a({ children, node: { properties } }) {
if (!isTaggedUserPattern(properties?.href || "")) {
return (
{children}
);
}
const userName = properties?.href?.toString()?.split(":")[1];
if (!userName) {
return (
{children}
);
}
let profile = userName && chatMemberMap?.[userName]?.profiles;
const profileCreatedAt =
userName && chatMemberMap?.[userName]?.created_at;
if (userName === "assistant") {
profile = AI_ASSISTANT_PROFILE;
}
if (!profile) {
return {children} ;
}
return (
{children}
);
},
code({ inline, className, children, ...props }) {
if (children.length) {
if (children[0] == "▍") {
return (
▍
);
}
children[0] = (children[0] as string).replace("`▍`", "▍");
}
const match = /language-(\w+)/.exec(className || "");
if (inline) {
return (
{children}
);
}
return (
);
},
}}
>
{content}
{status}
{renderActionButtons()}
);
});
================================================
FILE: components/ui/chat/ChatInput.tsx
================================================
"use client";
import React from "react";
import {
Mention,
MentionsInput,
MentionsInputProps,
SuggestionDataItem,
} from "react-mentions";
import { cn } from "@/lib/utils";
import { textAreaVariants } from "@/components/ui/TextArea";
import { defaultStyle } from "./mention-input-default-style";
type ChatTextAreaProps = Omit & {
mentionData: SuggestionDataItem[];
};
export const ChatInput = ({ mentionData, ...rest }: ChatTextAreaProps) => {
return (
(
{highlightedDisplay}
)}
className="bg-primary/40"
/>
);
};
================================================
FILE: components/ui/chat/ChatList.tsx
================================================
import React from "react";
import GPTAvatar from "@/public/chat-gpt.jpeg";
import { Message } from "ai/react";
import { getRawValueFromMentionInput } from "@/lib/chat-input";
import { AI_ASSISTANT_PROFILE } from "@/lib/contants";
import {
ChatMemberProfile,
MessageAdditionalData,
Message as SupabaseMessage,
} from "@/lib/db";
import { useProfileStore } from "@/lib/stores/profile";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
import { Heading5 } from "@/components/ui/typography";
import { toast } from "@/components/ui/use-toast";
import { ChatPanelProps } from "@/components/modules/apps/chat/ChatPanel";
import { ChatBubble, ChatBubbleProps } from "./ChatBubble";
import { ChatProfileHoverCard } from "./ChatProfileHoverCard";
type ChatListProps = {
data: Message[];
isLoading: boolean;
stop: () => void;
reload: (id: SupabaseMessage["id"]) => void;
chatMembers: ChatPanelProps["chatMembers"];
};
export const ChatList = ({
data,
chatMembers,
isLoading,
stop,
reload,
}: ChatListProps) => {
const currentProfile = useProfileStore((state) => state.profile);
const { isCopied, copyToClipboard } = useCopyToClipboard({});
const hasConversation =
data.filter((message) => message.role !== "system").length > 0;
const chatMemberMap = React.useMemo(() => {
return chatMembers?.reduce(
(acc, member) => {
if (!member.profiles) return acc;
acc[member.profiles.id] = {
...member,
profiles: member.profiles,
created_at: member.created_at,
};
return acc;
},
{} as Record
);
}, [chatMembers]);
React.useEffect(() => {
if (isCopied) {
toast({
description: "Copied to clipboard",
});
}
}, [isCopied]);
const handleOnCopy = React.useCallback(
(message: string) => {
const rawMessage = getRawValueFromMentionInput(message);
copyToClipboard(rawMessage);
},
[copyToClipboard]
);
return (
<>
{hasConversation ? (
<>
{data.map((m, index) => {
const messageAdditionalData = m.data as
| MessageAdditionalData
| undefined;
const messageProfileId = messageAdditionalData?.profile_id;
const member = chatMembers?.find(
(member) => member.profiles?.id === messageProfileId
)?.profiles;
const memberUsername = !chatMembers
? currentProfile?.username
: member?.username;
const memberAvatar = member?.avatar_url;
if (m.role === "system") {
return null;
}
const name =
m.role === "assistant"
? "AI Assistant"
: memberUsername || "Unknown User";
const avatar =
m.role === "assistant" ? GPTAvatar.src : memberAvatar || "";
let direction: ChatBubbleProps["direction"];
if (
messageAdditionalData?.profile_id === currentProfile?.id &&
m.role === "user"
) {
direction = "end";
} else if (messageAdditionalData?.profile_id && m.role === "user") {
direction = "start";
} else if (m.role === "assistant") {
direction = "start";
}
const isLast = index === data.length - 1;
return (
);
})}
>
) : (
Unleash Your Creativity
Chat with your{" "}
@Assistant
{" "}
to generate new ideas and get inspired.
)}
>
);
};
================================================
FILE: components/ui/chat/ChatProfileHoverCard.tsx
================================================
import React from "react";
import { CalendarDays, Globe } from "lucide-react";
import { Profile } from "@/lib/db";
import { cn } from "@/lib/utils";
import { Button } from "../Button";
import { UserAvatar } from "../common/UserAvatar";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../HoverCard";
import { Subtle } from "../typography";
import { ChatBubbleProps } from "./ChatBubble";
type ProfileHoverCardProps = {
direction?: ChatBubbleProps["direction"];
profile: Profile;
joinedDate?: string;
children: React.ReactNode;
};
export const ChatProfileHoverCard = React.memo(function ChatProfileHoverCard({
direction,
profile,
joinedDate,
children,
}: ProfileHoverCardProps) {
return (
{children}
{children}
{profile.full_name ? (
({profile.full_name})
) : null}
{profile.website ? (
) : null}
{joinedDate ? (
Joined on {new Date(joinedDate).toLocaleDateString()}
) : null}
);
});
================================================
FILE: components/ui/chat/Markdown.tsx
================================================
import { FC, memo } from "react";
import ReactMarkdown, { Options } from "react-markdown";
export const MemoizedReactMarkdown: FC = memo(
ReactMarkdown,
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.className === nextProps.className
);
================================================
FILE: components/ui/chat/index.ts
================================================
export * from "./ChatBubble";
export * from "./ChatList";
export * from "./ChatInput";
================================================
FILE: components/ui/chat/mention-input-default-style.ts
================================================
import { MAX_CHAT_INPUT_HEIGHT } from "@/lib/contants";
export const defaultStyle = {
control: {
wordBreak: "break-word",
maxHeight: MAX_CHAT_INPUT_HEIGHT,
overflowY: "hidden",
},
"&multiLine": {
highlighter: {
padding: 8,
border: "1px solid transparent",
color: "hsl(var(--primary))",
},
input: {
padding: 8,
border: "1px solid hsl(var(--border))",
borderRadius: "calc(var(--radius) - 2px)",
maxHeight: MAX_CHAT_INPUT_HEIGHT,
overflowY: "auto",
paddingBottom: 48,
outlineColor: "hsl(var(--primary))",
},
},
suggestions: {
list: {
backgroundColor: "hsl(var(--background))",
border: "1px solid hsl(var(--border))",
},
item: {
backgroundColor: "hsl(var(--background))",
color: "hsl(var(--foreground))",
padding: "4px 16px",
borderBottom: "1px solid hsl(var(--border))",
"&focused": {
backgroundColor: "hsl(var(--muted))",
},
},
},
};
================================================
FILE: components/ui/common/AppLogo.tsx
================================================
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { siteConfig } from "@/config/site";
export const AppLogo = () => {
return (
);
};
================================================
FILE: components/ui/common/ChatScrollAnchor.tsx
================================================
"use client";
import * as React from "react";
import { useInView } from "react-intersection-observer";
import { useAtBottom } from "@/hooks/useAtBottom";
interface ChatScrollAnchorProps {
trackVisibility?: boolean;
parentElement: HTMLElement | null;
}
export function ChatScrollAnchor({
trackVisibility,
parentElement,
}: ChatScrollAnchorProps) {
const isAtBottom = useAtBottom(0, parentElement ?? undefined);
const { ref, entry, inView } = useInView({
trackVisibility,
delay: 100,
// rootMargin: '0px 0px -150px 0px'
});
React.useEffect(() => {
if (isAtBottom && trackVisibility && !inView) {
entry?.target.scrollIntoView({
block: "start",
});
}
}, [inView, entry, isAtBottom, trackVisibility]);
return
;
}
================================================
FILE: components/ui/common/MainLayout.tsx
================================================
import { NavigationBar } from "@/components/navigation/NavigationBar";
import { Sidebar } from "@/components/navigation/SideBar";
interface MainLayoutProps {
children: React.ReactNode;
}
export const MainLayout = ({ children }: MainLayoutProps) => {
return (
);
};
================================================
FILE: components/ui/common/UserAvatar.tsx
================================================
import { AvatarProps } from "@radix-ui/react-avatar";
import { User } from "@supabase/supabase-js";
import { Profile } from "@/lib/db";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "../Avatar";
type UserAvatarProps = {
username: Profile["username"];
avatarUrl?: Profile["avatar_url"];
email?: User["email"];
isOnline?: boolean;
} & AvatarProps;
export const UserAvatar = ({
avatarUrl,
username,
email,
isOnline,
...rest
}: UserAvatarProps) => {
const nameLabel = username || email || "";
const fallback = nameLabel.slice(0, 1).toUpperCase();
return (
{avatarUrl ? : null}
{fallback}
{isOnline !== undefined && (
)}
);
};
================================================
FILE: components/ui/form/form-fields/InputField/InputField.tsx
================================================
import get from "lodash/get";
import { FieldValues } from "react-hook-form";
import { cn } from "@/lib/utils";
import { Input, InputProps } from "@/components/ui/Input";
import { FormFieldProps } from "../types";
type InputFieldProps = FormFieldProps &
Omit & {
onValueChange?: (value: string) => void;
};
export function InputField(props: InputFieldProps) {
const { register, name, id, formState, containerClassName, ...rest } = props;
const { errors } = formState;
const error = get(errors, name);
const errorText = error?.message;
const renderHelperText = () => {
if (errorText && typeof errorText === "string") {
return errorText;
}
};
const handleOnChange = (e: React.ChangeEvent) => {
props.onValueChange?.(e.target.value);
};
return (
);
}
================================================
FILE: components/ui/form/form-fields/InputField/index.ts
================================================
export * from "./InputField";
================================================
FILE: components/ui/form/form-fields/SliderField/SliderField.tsx
================================================
import get from "lodash/get";
import { Controller, FieldValues } from "react-hook-form";
import { Slider, SliderProps } from "@/components/ui/Slider";
import { ControlledFormFieldProps } from "../types";
type SliderFieldProps = ControlledFormFieldProps &
Omit;
export function SliderField(props: SliderFieldProps) {
const { name, id, formState, min, max, control, ...rest } = props;
const { errors } = formState;
const error = get(errors, name);
const errorText = error?.message;
const renderHelperText = () => {
if (errorText && typeof errorText === "string") {
return errorText;
}
};
return (
(
)}
/>
);
}
================================================
FILE: components/ui/form/form-fields/SliderField/index.ts
================================================
export * from "./SliderField";
================================================
FILE: components/ui/form/form-fields/TextAreaField/TextAreaField.tsx
================================================
import get from "lodash/get";
import { FieldValues } from "react-hook-form";
import { TextareaAutosizeProps } from "react-textarea-autosize";
import { cn } from "@/lib/utils";
import { TextArea, TextAreaProps } from "@/components/ui/TextArea";
import { FormFieldProps } from "../types";
type TextAreaFieldProps = FormFieldProps &
Omit &
TextareaAutosizeProps;
export function TextAreaField(
props: TextAreaFieldProps
) {
const { register, name, id, formState, containerClassName, ...rest } = props;
const { errors } = formState;
const error = get(errors, name);
const errorText = error?.message;
const renderHelperText = () => {
if (errorText && typeof errorText === "string") {
return errorText;
}
};
return (
);
}
================================================
FILE: components/ui/form/form-fields/TextAreaField/index.ts
================================================
export * from "./TextAreaField";
================================================
FILE: components/ui/form/form-fields/index.ts
================================================
export * from "./InputField";
export * from "./SliderField";
export * from "./TextAreaField";
export * from "./types";
================================================
FILE: components/ui/form/form-fields/types.ts
================================================
import {
Control,
FieldPath,
FieldValues,
FormState,
UseFormRegister,
} from "react-hook-form";
export type FormFieldProps = {
register: UseFormRegister;
formState: FormState;
name: FieldPath;
};
export type ControlledFormFieldProps = {
formState: FormState;
name: FieldPath;
control: Control;
};
================================================
FILE: components/ui/typography/Blockquote.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Blockquote({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/Heading1.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Heading1({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/Heading2.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Heading2({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/Heading3.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Heading3({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/Heading4.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Heading4({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/Heading5.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Heading5({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/Paragraph.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Paragraph({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/Subtle.tsx
================================================
import { cn } from "@/lib/utils";
import { TypographyProps } from "./types";
export function Subtle({ text, className, children }: TypographyProps) {
return (
{children || text}
);
}
================================================
FILE: components/ui/typography/index.ts
================================================
export * from "./Blockquote";
export * from "./Heading1";
export * from "./Heading2";
export * from "./Heading3";
export * from "./Heading4";
export * from "./Heading5";
export * from "./Paragraph";
export * from "./Subtle";
================================================
FILE: components/ui/typography/types.ts
================================================
export type TypographyProps = {
text?: string;
className?: string;
children?: React.ReactNode;
};
================================================
FILE: components/ui/use-toast.ts
================================================
// Inspired by react-hot-toast library
import * as React from "react";
import { ToastActionElement, ToastProps } from "./Toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_VALUE;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
================================================
FILE: config/site.ts
================================================
import { env } from "@/env.mjs";
export type SiteConfig = typeof siteConfig;
export const siteConfig = {
name: "AI Fusion Kit",
description:
"A feature-rich, highly customizable AI Chatbot Template, empowered by Next.js.",
links: {
x: "https://twitter.com/nphivu414",
github: "https://github.com/nphivu414/ai-fusion-kit",
docs: "",
},
};
export const getURL = () => {
let url = env.NEXT_PUBLIC_APP_URL || "http://localhost:3000/";
// Make sure to include `https://` when not localhost.
url = url.includes("http") ? url : `https://${url}`;
// Make sure to include a trailing `/`.
url = url.charAt(url.length - 1) === "/" ? url : `${url}/`;
return url;
};
================================================
FILE: env.mjs
================================================
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"
export const env = createEnv({
server: {
OPENAI_API_KEY: z.string().min(1),
NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().min(1),
NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
},
runtimeEnv: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
},
})
================================================
FILE: hooks/useActiveTheme.tsx
================================================
import { useTheme } from "next-themes"
export const useActiveThemeColor = () => {
const { theme, systemTheme } = useTheme()
if (theme === "dark" || (theme === "system" && systemTheme === "dark")) {
return "dark"
}
return "light"
}
================================================
FILE: hooks/useAtBottom.tsx
================================================
import * as React from "react";
export function useAtBottom(offset = 0, element?: HTMLElement) {
const [isAtBottom, setIsAtBottom] = React.useState(false);
React.useEffect(() => {
const handleScroll = () => {
let isAtBottom =
window.innerHeight + window.scrollY >=
document.body.offsetHeight - offset;
if (element) {
isAtBottom =
element.scrollTop + element.offsetHeight >=
element.scrollHeight - offset;
}
setIsAtBottom(isAtBottom);
};
if (!element) {
window.addEventListener("scroll", handleScroll);
} else {
element.addEventListener("scroll", handleScroll);
}
handleScroll();
return () => {
if (!element) {
window.removeEventListener("scroll", handleScroll);
} else {
element.removeEventListener("scroll", handleScroll);
}
};
}, [element, offset]);
return isAtBottom;
}
================================================
FILE: hooks/useChatIdFromPathName.tsx
================================================
import { usePathname } from "next/navigation";
export const useChatIdFromPathName = () => {
const pathname = usePathname();
if (pathname.indexOf("/apps/chat") === -1) {
return "";
}
const chatId = pathname.split("/").pop() || "";
return chatId;
};
================================================
FILE: hooks/useCopyToClipboard.tsx
================================================
"use client";
import * as React from "react";
export interface useCopyToClipboardProps {
timeout?: number;
}
export function useCopyToClipboard({
timeout = 2000,
}: useCopyToClipboardProps) {
const [isCopied, setIsCopied] = React.useState(false);
const copyToClipboard = React.useCallback(
(value: string) => {
if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
return;
}
if (!value) {
return;
}
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, timeout);
});
},
[timeout]
);
return { isCopied, copyToClipboard };
}
================================================
FILE: hooks/useEnterSubmit.tsx
================================================
import { useRef, type RefObject } from "react";
type KeyboardEvent =
| React.KeyboardEvent
| React.KeyboardEvent;
export function useEnterSubmit(): {
formRef: RefObject;
onKeyDown: (event: KeyboardEvent) => void;
} {
const formRef = useRef(null);
const handleKeyDown = (event: KeyboardEvent): void => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
formRef.current?.requestSubmit();
event.preventDefault();
}
};
return { formRef, onKeyDown: handleKeyDown };
}
================================================
FILE: hooks/useMutationObserver.ts
================================================
import * as React from "react"
export const useMutationObserver = (
ref: React.MutableRefObject,
callback: MutationCallback,
options = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
}
) => {
React.useEffect(() => {
if (ref.current) {
const observer = new MutationObserver(callback)
observer.observe(ref.current, options)
return () => observer.disconnect()
}
}, [ref, callback, options])
}
================================================
FILE: hooks/usePrevious.tsx
================================================
import React from "react";
export const usePrevious = <_,T>(value: T) => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
================================================
FILE: hooks/useSubscribeChatMessages.ts
================================================
import React from "react";
import { RealtimePresenceState } from "@supabase/supabase-js";
import { Message } from "ai";
import { validate } from "uuid";
import { createClient } from "@/lib/supabase/client";
type ChatMemberStatus = {
userId: string;
onlineAt: string;
};
export type RealtimeChatMemberStatus = RealtimePresenceState;
type UseSubscribeChatMessagesParams = {
initialMessages: Message[];
chatId: string;
currentUserId?: string;
newMessageInsertCallback: (messages: Message[]) => void;
chatMemberPresenceCallback?: (presence: RealtimeChatMemberStatus) => void;
chatMemberJoinCallback?: (presence: RealtimeChatMemberStatus[]) => void;
chatMemberLeaveCallback?: (presence: RealtimeChatMemberStatus[]) => void;
};
export function useSubscribeChatMessages({
initialMessages,
chatId,
currentUserId,
newMessageInsertCallback,
chatMemberJoinCallback,
chatMemberLeaveCallback,
chatMemberPresenceCallback,
}: UseSubscribeChatMessagesParams) {
const supabaseClient = createClient();
const messageListRef = React.useRef