Showing preview only (373K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<a href="https://ai-fusion-kit.vercel.app/">
<img alt="AI Fusion Kit" src="https://ai-fusion-kit.vercel.app/_next/image?url=%2Fscreenshot.png&w=1920&q=75">
<h1 align="center">AI Fusion Kit</h1>
</a>
<p align="center">
A feature-rich, highly customizable AI Web App Template, empowered by Next.js.
</p>
<p align="center">
<a href="#tech-stacks"><strong>Tech stacks</strong></a> ·
<a href="#installation"><strong>Installation</strong></a> ·
<a href="#run-locally"><strong>Run Locally</strong></a> ·
<a href="#authors"><strong>Authors</strong></a>
</p>
<br/>
## 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 (
<>
<div className="flex flex-col space-y-2 text-center">
<Heading3>Error</Heading3>
<p className="text-sm text-muted-foreground">Failed to sign in</p>
</div>
</>
);
}
================================================
FILE: app/(auth)/layout.tsx
================================================
import { AppLogo } from "@/components/ui/common/AppLogo";
type AuthLayoutProps = {
children: React.ReactNode;
};
export default function AuthLayout({ children }: AuthLayoutProps) {
return (
<div className="min-h-screen">
<div className="container relative grid h-screen flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0">
<div className="absolute hidden size-full flex-col bg-muted p-4 text-white dark:block dark:border-r lg:relative lg:flex lg:p-10">
<div
className="absolute inset-0 bg-ring bg-right-bottom brightness-50 lg:bg-center lg:brightness-100"
style={{
backgroundImage:
"url(https://images.unsplash.com/photo-1627645835237-0743e52b991f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1587&q=80)",
}}
/>
<div className="relative z-20 flex items-center text-lg font-medium">
<AppLogo />
</div>
</div>
<div className="z-10 w-screen px-4 lg:w-full lg:p-8">
<div className="mx-auto flex w-full max-w-full flex-col justify-center space-y-6 sm:w-[400px]">
{children}
</div>
</div>
</div>
</div>
);
}
================================================
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 (
<>
<div className="flex flex-col space-y-2 text-center">
<Heading3>{siteConfig.name}</Heading3>
<p className="text-sm text-muted-foreground">
Empowering Your Imagination with AI Services
</p>
</div>
<UserAuthForm />
</>
);
}
================================================
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 (
<>
<div className="flex flex-col space-y-2 text-center">
<Heading3>{siteConfig.name}</Heading3>
<p className="text-sm text-muted-foreground">
Empowering Your Imagination with AI Services
</p>
</div>
<UserSignupForm />
</>
);
}
================================================
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 <div className="pt-4">No app found</div>;
}
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 (
<ChatPanel
chatId={chatId}
chats={chats}
initialMessages={initialChatMessages}
chatParams={chatParams}
isChatHost={isChatHost}
chatMembers={chatMembers}
defaultMemberSidebarLayout={defaultMemberSidebarLayout}
/>
);
}
================================================
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 (
<div className="flex flex-1">
<div className="flex w-full flex-col rounded-lg pb-4 lg:bg-background">
<div className="mx-auto flex w-full flex-1 flex-col">
<div className="flex flex-row items-center justify-between space-y-0 p-4 lg:h-16">
<Heading2 className="pb-0">GPT AI Assistant</Heading2>
</div>
<Separator />
<div className="flex grow basis-0 justify-center pt-4 lg:overflow-y-auto lg:pb-0">
<div className="flex w-full flex-col items-center justify-center">
<Loader className="size-8 animate-spin" />
</div>
</div>
</div>
</div>
<div className="size-0 lg:h-auto lg:max-h-[calc(100vh_-_60px)] lg:w-[450px] lg:border-x lg:p-4">
<Skeleton className="mt-2 h-4 w-[210px]" />
<div className="mt-4 grid grid-cols-1 gap-2">
<Skeleton className="h-2 w-[320px]" />
<Skeleton className="h-2 w-[280px]" />
<Skeleton className="h-2 w-[300px]" />
<Skeleton className="h-2 w-[250px]" />
</div>
<Separator className="my-4" />
<div className="mt-6 grid grid-cols-1 gap-2">
<Skeleton className="h-2 w-[100px]" />
<Skeleton className="h-2 w-[320px]" />
<Skeleton className="h-2 w-[280px]" />
</div>
<div className="mt-6 grid grid-cols-1 gap-2">
<Skeleton className="h-2 w-[100px]" />
<Skeleton className="h-2 w-[320px]" />
<Skeleton className="h-2 w-[280px]" />
</div>
<div className="mt-6 grid grid-cols-1 gap-2">
<Skeleton className="h-2 w-[100px]" />
<Skeleton className="h-2 w-[320px]" />
<Skeleton className="h-2 w-[280px]" />
</div>
<div className="mt-6 grid grid-cols-1 gap-2">
<Skeleton className="h-2 w-[100px]" />
<Skeleton className="h-2 w-[320px]" />
<Skeleton className="h-2 w-[280px]" />
</div>
</div>
</div>
);
}
================================================
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 <div className="pt-4">No app found</div>;
}
const chats = await getChats(supabase, currentApp.id);
return (
<ChatPanel
chatId={chatId}
initialMessages={[]}
chats={chats}
isNewChat
chatMembers={null}
defaultMemberSidebarLayout={[]}
/>
);
}
================================================
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 <div className="pt-4">No app found</div>;
}
const chats = await getChats(supabase, currentApp.id);
return (
<MainLayout>
<div className="flex h-screen flex-1 flex-row pt-16">
<div className="flex flex-1 flex-row">
<div className="flex flex-1 flex-col overflow-y-hidden">
<div className="relative flex flex-1 bg-background">
<div className="flex size-0 flex-col justify-between overflow-x-hidden transition-[width] lg:h-auto lg:max-h-[calc(100vh_-_65px)] lg:w-[300px] lg:border-r">
<ChatHistory data={chats} />
</div>
{children}
</div>
</div>
</div>
</div>
</MainLayout>
);
}
================================================
FILE: app/apps/page.tsx
================================================
import { Heading1 } from "@/components/ui/typography";
export const runtime = "edge";
export default function Apps() {
return <Heading1>Apps</Heading1>;
}
================================================
FILE: app/docs/page.tsx
================================================
export default async function Docs() {
return <div className="pt-16">docs</div>;
}
================================================
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 (
<>
<html
lang="en"
suppressHydrationWarning
className={`${GeistSans.variable} ${GeistMono.variable}`}
>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<head />
<body className="min-h-screen bg-background font-sans antialiased">
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
<Toaster />
<AxiomWebVitals />
<Analytics />
</body>
</html>
</>
);
}
================================================
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 (
<MainLayout>
<div className="pt-16">
<section className="space-y-6 pb-8 md:pb-12">
<div className="relative flex h-60 w-full items-center justify-center bg-white bg-dot-black/[0.2] dark:bg-black dark:bg-dot-white/[0.2] md:h-80">
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)] dark:bg-black"></div>
<div className="container flex max-w-5xl flex-col items-center gap-6 text-center">
<Heading1 className="font-heading bg-gradient-to-r from-gray-900 to-gray-500 bg-clip-text text-3xl font-bold leading-[1.1] tracking-tighter text-transparent dark:from-white dark:to-gray-500 sm:!text-5xl md:!text-7xl">
{siteConfig.name}
</Heading1>
<DescriptionHeadingText />
<div className="flex items-center space-x-4">
<Link
href="https://www.youtube.com/watch?v=GWalKzuC0Rg"
target="_blank"
className={cn(
buttonVariants({
variant: "outline",
})
)}
>
<Play className="mr-2" size={16} /> Demo
</Link>
<Link href="/apps/chat" className={cn(buttonVariants())}>
Get Started
</Link>
</div>
</div>
</div>
</section>
<div className="px-4">
<HeroBannerImage />
<FeatureItems />
</div>
</div>
</MainLayout>
);
}
================================================
FILE: app/profile/layout.tsx
================================================
import { MainLayout } from "@/components/ui/common/MainLayout";
interface AppLayoutProps {
children: React.ReactNode;
}
export default function AppLayout({ children }: AppLayoutProps) {
return (
<MainLayout>
<div className="flex flex-1 flex-col pt-16">{children}</div>
</MainLayout>
);
}
================================================
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 (
<div className="container mx-auto sm:max-w-screen-sm">
<Header
avatarUrl={avatar_url}
email={user?.email}
fullName={full_name}
username={username}
website={website}
/>
<ProfileForm className="mt-8" formValues={profileFormValues} />
</div>
);
}
================================================
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 (
<aside
className="sticky top-16 flex w-0 flex-col overflow-x-hidden bg-muted transition-[width] lg:w-[73px] lg:border-r lg:hover:w-80"
aria-label="Sidenav"
>
<AppSideBarList apps={apps} />
</aside>
);
};
================================================
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 (
<li className="px-2 py-1 lg:min-w-[319px]">
<Link className="h-16 w-full" href={slug || ""}>
<div className="flex flex-row items-center rounded-lg p-2 transition-colors hover:bg-background/30">
<div className="flex flex-col">
<Avatar>
{logoUrl ? (
<AvatarImage src={logoUrl} className="w-12" alt={name} />
) : null}
<AvatarFallback>
<div className="size-12 bg-card-foreground" />
</AvatarFallback>
</Avatar>
</div>
<div className="flex flex-col px-4">
<p className="font-bold">{name}</p>
{description ? (
<p className="text-xs text-muted-foreground">{description}</p>
) : null}
</div>
</div>
</Link>
</li>
);
};
================================================
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 <div className="p-4">No apps found</div>;
}
return (
<ul>
{apps.map((app) => {
const { id, name, description, slug, logo_url } = app;
return (
<AppSideBarItem
key={id}
name={name}
description={description}
slug={slug}
logo_url={logo_url}
/>
);
})}
</ul>
);
};
================================================
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 (
<div className="py-2">
<Heading5 className="px-4 py-2 text-muted-foreground">{title}</Heading5>
{children}
</div>
);
};
================================================
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<HTMLFormElement>) => void;
onInputChange: (
e:
| React.ChangeEvent<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>
) => 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<HTMLTextAreaElement>);
};
return (
<div className="fixed bottom-0 left-0 w-full bg-background p-4 lg:relative lg:mt-2 lg:bg-transparent lg:py-0">
<form onSubmit={onSubmit} className="relative" ref={formRef}>
<ChatInput
value={chatInput}
onKeyDown={onKeyDown}
onChange={handleOnChange}
mentionData={mentionData}
/>
<MobileDrawerControl chats={chats} />
<div className="absolute bottom-[2px] right-1 flex w-1/2 justify-end bg-background px-2 pb-2">
<Button size="sm" type="submit" disabled={isChatStreamming}>
Send
<SendHorizonal size={14} className="ml-1" />
</Button>
</div>
</form>
</div>
);
};
================================================
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 (
<aside className="pb-4">
<div className="sticky top-0 flex h-16 items-center justify-between bg-background lg:px-4">
<p className="lg:text-md text-lg font-bold lg:font-normal lg:text-muted-foreground">
Chat history
</p>
<div>
<NewChatButton closeDrawer={closeDrawer} />
</div>
</div>
<Separator className="sticky top-16" />
<ul className="mt-2 lg:px-2">
{data?.length ? null : (
<Paragraph className="text-center text-sm text-muted-foreground">
No chats found
</Paragraph>
)}
{data?.map((chat) => {
return (
<ChatHistoryItem
key={chat.id}
isActive={chat.id === chatId}
chat={chat}
closeDrawer={closeDrawer}
/>
);
})}
</ul>
</aside>
);
};
================================================
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 (
<Drawer.Root open={drawerOpen} onOpenChange={setDrawerOpen}>
<Button variant="ghost" size="sm" onClick={onHistoryButtonClick}>
<History />
</Button>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 backdrop-blur-lg" />
<Drawer.Content
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-full max-h-[80%] flex-col rounded-t-xl border bg-background"
)}
>
<div className="mx-auto my-4 h-1.5 w-12 shrink-0 rounded-full bg-border" />
<div className="relative flex-1 overflow-y-auto px-4 pb-4">
<ChatHistory data={data} closeDrawer={closeDrawer} />
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
};
================================================
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 (
<div
className={cn("hidden w-20 lg:group-hover:block", {
block: isActive,
})}
>
<div
className={cn("grid grid-cols-2 ", {
"lg:visible": isActive,
})}
>
<EditChatAction chat={chat} />
<DeleteChatAction chat={chat} />
</div>
</div>
);
};
return (
<li className="w-full pb-1">
<div
className={cn(
"group h-10 rounded-lg px-2 transition-colors hover:bg-accent",
isActive ? "bg-accent" : "bg-background"
)}
>
<div className="flex h-full max-w-full flex-1 items-center">
<div className="flex size-full justify-between">
<Link
href={`/apps/chat/${chat.id}`}
onClick={closeDrawer}
className={cn(
"flex h-full w-[90%] items-center px-2 lg:group-hover:w-[65%]",
{
"w-[65%]": isActive,
}
)}
>
<div className="mr-1 w-4">
<MessageCircle size={16} />
</div>
<p className="truncate text-sm text-muted-foreground">
{chat.name}
</p>
</Link>
{renderActionButtons()}
</div>
</div>
</div>
</li>
);
};
================================================
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 (
<MainLayout>
<div className="flex h-screen flex-1 flex-row pt-16">
<div className="flex flex-1 flex-row">
<div className="flex flex-1 flex-col overflow-y-hidden">
<div className="relative flex flex-1 bg-background">
<div className="flex size-0 flex-col justify-between overflow-x-hidden transition-[width] lg:h-auto lg:max-h-[calc(100vh_-_65px)] lg:w-[300px] lg:border-r">
{leftSidebarElement}
</div>
{children}
</div>
</div>
</div>
</div>
</MainLayout>
);
};
================================================
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<HTMLDivElement>(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<ChatParams>({
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<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>
) => {
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<HTMLFormElement>) => {
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 (
<Sheet open={sidebarSheetOpen} onOpenChange={setSidebarSheetOpen}>
<div className="flex flex-1 flex-col">
<div className="flex flex-1">
<div className="flex w-full flex-col rounded-lg pb-4 lg:bg-background">
<div className="mx-auto flex w-full flex-1 flex-col">
<Header />
<Separator />
<div
ref={scrollAreaRef}
className="mx-auto flex w-full max-w-screen-2xl grow basis-0 flex-col overflow-visible px-4 pb-[130px] lg:overflow-y-auto lg:pb-0"
>
<ChatList
data={messages}
isLoading={isLoading}
stop={stop}
reload={handleReloadMessages}
chatMembers={chatMemberWithStatus}
/>
<ChatScrollAnchor
trackVisibility={isLoading}
parentElement={scrollAreaRef?.current}
/>
</div>
<ChatForm
chatInput={input}
chats={chats}
onInputChange={handleOnChange}
isChatStreamming={isLoading}
onSubmit={onSubmit}
chatMembers={chatMemberWithStatus}
/>
</div>
</div>
<ControlSidebarSheet
closeSidebarSheet={closeSidebarSheet}
formReturn={formReturn}
isNewChat={isNewChat}
messages={messages}
setMessages={setMessages}
chatMembers={chatMemberWithStatus}
isChatHost={isChatHost}
defaultMemberSidebarLayout={defaultMemberSidebarLayout}
/>
</div>
</div>
</Sheet>
);
};
================================================
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<Props> = 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 (
<div className="codeblock relative w-full rounded-md border bg-background font-sans">
<div className="flex w-full items-center justify-between px-6 py-2 pr-4">
<span className="text-xs lowercase">{language}</span>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
className="hover:bg-card focus-visible:ring-1 focus-visible:ring-muted focus-visible:ring-offset-0"
onClick={downloadAsFile}
size="sm"
>
<Download />
<span className="sr-only">Download</span>
</Button>
<Button
variant="ghost"
size="sm"
className="text-xs hover:bg-card focus-visible:ring-1 focus-visible:ring-muted focus-visible:ring-offset-0"
onClick={onCopy}
>
{isCopied ? <Check /> : <Copy />}
<span className="sr-only">Copy code</span>
</Button>
</div>
</div>
<SyntaxHighlighter
language={language}
style={codeBlockStyle}
PreTag="div"
showLineNumbers
customStyle={{
margin: 0,
width: "100%",
background: "transparent",
padding: "1.5rem 1rem",
}}
codeTagProps={{
style: {
fontSize: "0.9rem",
fontFamily: "var(--font-mono)",
},
}}
>
{value}
</SyntaxHighlighter>
</div>
);
});
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<HTMLButtonElement, 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 (
<AlertDialog open={isAlertOpen} onOpenChange={setIsAlertOpen} {...rest}>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm">
<Trash2 size={16} />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to delete this chat?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
chat.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className={cn(
buttonVariants({
variant: "destructive",
})
)}
onClick={onDelete}
>
{pendingDeleteChat ? (
<Loader size={16} className="animate-spin" />
) : (
"Delete"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
================================================
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<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleDelete();
}
};
const onEdit = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
};
const onDelete = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
handleDelete();
};
return (
<AlertDialog open={isAlertOpen} onOpenChange={setIsAlertOpen} {...rest}>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm" onClick={onEdit}>
<Edit size={16} />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Edit your chat title</AlertDialogTitle>
<AlertDialogDescription></AlertDialogDescription>
</AlertDialogHeader>
<div className="pb-4">
<Input
name="name"
value={inputValue}
onChange={onChange}
onKeyDown={onKeyDown}
autoFocus
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onDelete}>
{pendingUpdateChat ? (
<Loader size={16} className="animate-spin" />
) : (
"Update"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
================================================
FILE: components/modules/apps/chat/Header.tsx
================================================
import React from "react";
import { Heading2 } from "@/components/ui/typography";
export const Header = () => {
return (
<div className="flex flex-row items-center justify-between space-y-0 p-4 lg:h-16">
<Heading2 className="pb-0">GPT AI Assistant</Heading2>
</div>
);
};
================================================
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 (
<>
<div className="absolute bottom-[2px] left-1 flex w-1/2 bg-background px-2 pb-2 lg:hidden">
<ChatHistoryDrawer data={chats} />
</div>
<div className="absolute bottom-[2px] left-16 flex w-1/2 justify-start bg-background px-2 pb-2 lg:hidden">
<SheetTrigger asChild>
<Button size="sm" variant="ghost">
<PanelRight />
</Button>
</SheetTrigger>
</div>
</>
);
});
================================================
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 (
<Link
href="/apps/chat"
className={cn(
buttonVariants({
size: "sm",
variant: "outline",
})
)}
onClick={closeDrawer}
>
<Plus size={16} />
</Link>
);
};
================================================
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<ChatParams>();
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<HTMLTextAreaElement>
) => {
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 (
<Popover open={isPopoverOpen} onOpenChange={handlePopoverOpenChange}>
<div>
<div className="flex items-center justify-between">
<Label>Description</Label>
<PopoverTrigger asChild>
<Button size="sm" variant="ghost">
Edit
</Button>
</PopoverTrigger>
</div>
<Subtle className="mb-4 mt-2">{description}</Subtle>
</div>
<PopoverContent className="w-96">
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Set system prompt</h4>
<p className="text-sm text-muted-foreground">
{`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.`}
</p>
</div>
<div>
<TextArea
minRows={2}
placeholder="Your custom prompt"
value={systemPromptInputValue}
onChange={handleSystemPromptInputChange}
/>
<Button size="sm" className="mt-4 w-full" onClick={handleSave}>
Done
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
};
================================================
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<typeof addMemberSchema>;
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<AddMembersFormParams>({
defaultValues: defaultValues,
mode: "onChange",
resolver: zodResolver(addMemberSchema),
});
const fieldProps = { register, formState };
const onSubmit: SubmitHandler<AddMembersFormParams> = (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 (
<form className="grid gap-4" onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-2">
<h4 className="font-medium leading-none">Add new members</h4>
<p className="text-sm text-muted-foreground">
You can add new members to this chat to discuss and share ideas.
</p>
</div>
<div className="space-y-4">
<InputField
name="username"
label="Enter member username"
autoFocus
required
{...fieldProps}
/>
<Button
type="submit"
size="sm"
className="w-full"
isLoading={formState.isSubmitting || isPending}
>
Add member
</Button>
</div>
</form>
);
};
================================================
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 (
<div className="flex items-center gap-3 rounded-md p-2 text-sm">
<UserAvatar
className="z-0"
username={username}
avatarUrl={avatarUrl}
email=""
isOnline={isOnline}
/>
<div className="flex-1">
<p>{fullname || username}</p>
</div>
{removeable && (
<DeleteMemberAction
chatId={chatId}
memberId={id}
memberUsername={username}
/>
)}
</div>
);
};
================================================
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 (
<aside className="max-h-full overflow-auto pb-4">
<div className="sticky top-0 z-10 flex h-16 items-center justify-between bg-card px-4">
<p className="text-muted-foreground">Chat members</p>
<div>
<Popover
open={addMemberPopoverOpen}
onOpenChange={handleAddMemberPopoverOpen}
>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<Plus size={16} />
</Button>
</PopoverTrigger>
<PopoverContent>
<AddMembersForm onCloseAddMemberPopover={closeAddMemberPopover} />
</PopoverContent>
</Popover>
</div>
</div>
<Separator className="sticky top-16" />
<ul className="mt-2 lg:px-2">
{data?.length ? null : (
<Paragraph className="text-center text-sm text-muted-foreground">
No members in this chat.
</Paragraph>
)}
{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 (
<React.Fragment key={id}>
<ChatMemberItem
id={id}
username={profiles.username || ""}
avatarUrl={profiles.avatar_url}
removeable={removeable}
isOnline={member.status === "online"}
/>
<Separator />
</React.Fragment>
);
})}
</ul>
</aside>
);
};
================================================
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<HTMLButtonElement, 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 (
<AlertDialog open={isAlertOpen} onOpenChange={setIsAlertOpen} {...rest}>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm">
<Trash2 size={16} />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to remove{" "}
<span className="font-semibold">"{memberUsername}" </span>
from this chat?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className={cn(
buttonVariants({
variant: "destructive",
})
)}
onClick={handleRemoveMember}
>
{pendingDeleteMember ? (
<Loader size={16} className="animate-spin" />
) : (
"Delete"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
================================================
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<UseChatHelpers, "setMessages" | "messages"> & {
closeSidebarSheet?: () => void;
isNewChat?: boolean;
};
export const ControlSidebar = ({
setMessages,
messages,
closeSidebarSheet,
isNewChat,
}: ControlSidebarProps) => {
const [pendingUpdateSettings, startUpdateSettings] = React.useTransition();
const currentChatId = useChatIdFromPathName();
const { getValues } = useFormContext<ChatParams>();
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 (
<>
<SheetHeader className="lg:px-4 lg:pt-4">
<SheetTitle className="text-left">Settings</SheetTitle>
<SheetDescription className="text-left">
{`Combining these parameters allows you to fine-tune the AI's output to suit different use cases.`}
</SheetDescription>
</SheetHeader>
<Separator className="my-4" />
<div className="pb-4 lg:px-4">
<SystemPromptControl setMessages={setMessages} messages={messages} />
<ModelSelector types={types} models={models} />
<TemperatureSelector />
<MaxLengthSelector />
<TopPSelector />
<FrequencyPenaltySelector />
<PresencePenaltySelector />
</div>
{!isNewChat && (
<div className="w-full lg:sticky lg:bottom-0 lg:bg-transparent lg:p-4 lg:backdrop-blur-sm">
<Button className="w-full" onClick={onSave}>
{pendingUpdateSettings ? (
<Loader className="animate-spin" />
) : (
"Save"
)}
</Button>
</div>
)}
</>
);
};
================================================
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<FormProviderProps<ChatParams>, "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 (
<ControlSidebar
setMessages={setMessages}
messages={messages}
closeSidebarSheet={closeSidebarSheet}
isNewChat={isNewChat}
/>
);
};
return (
<FormProvider {...formReturn}>
<SheetContent className="w-[400px] overflow-y-auto px-0 sm:w-[540px]">
<div className="pt-4">
<div className="px-4">{renderControlSidebar()}</div>
{!isNewChat && (
<div className="mt-6">
<ChatMembers data={chatMembers} isChatHost={isChatHost} />
</div>
)}
</div>
</SheetContent>
<div className="size-0 overflow-x-hidden transition-[width] lg:h-auto lg:max-h-[calc(100vh_-_65px)] lg:w-[450px] lg:border-x">
{isNewChat ? (
renderControlSidebar()
) : (
<ResizablePanelGroup direction="vertical" onLayout={onLayout}>
<ResizablePanel defaultSize={defaultMemberSidebarLayout[0]}>
<div className="h-full overflow-y-auto">
{renderControlSidebar()}
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={defaultMemberSidebarLayout[1]}
minSize={MIN_CHAT_MEMBER_SIDEBAR_SIZE}
maxSize={MAX_CHAT_MEMBER_SIDEBAR_SIZE}
>
<ChatMembers data={chatMembers} isChatHost={isChatHost} />
</ResizablePanel>
</ResizablePanelGroup>
)}
</div>
</FormProvider>
);
});
================================================
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<ChatParams>();
return (
<div className="grid gap-2 pt-4">
<HoverCard openDelay={200}>
<HoverCardTrigger>
<SliderField
name="frequencyPenalty"
label="Word Variation"
defaultValue={getValues("frequencyPenalty")}
min={-2}
max={2}
step={0.1}
className="[&_[role=slider]]:size-4"
control={control}
formState={formState}
aria-label="Word Variation"
/>
</HoverCardTrigger>
<HoverCardContent
align="start"
className="hidden w-[260px] text-sm lg:block"
side="left"
>
<p>
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.
</p>
</HoverCardContent>
</HoverCard>
</div>
);
}
================================================
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<ChatParams>();
return (
<div className="grid gap-2 pt-4">
<HoverCard openDelay={200}>
<HoverCardTrigger>
<SliderField
name="maxTokens"
label="Length Limit"
defaultValue={getValues("maxTokens")}
min={0}
max={1000}
step={10}
className="[&_[role=slider]]:size-4"
control={control}
formState={formState}
aria-label="Length Limit"
/>
</HoverCardTrigger>
<HoverCardContent
align="start"
className="hidden w-[260px] text-sm lg:block"
side="left"
>
{`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.`}
</HoverCardContent>
</HoverCard>
</div>
);
}
================================================
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<Model>(models[0]);
const [peekedModel, setPeekedModel] = React.useState<Model>(models[0]);
const { getValues, setValue } = useFormContext<ChatParams>();
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 (
<div className="grid gap-2">
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Label className="text-left" htmlFor="model">
Model
</Label>
</HoverCardTrigger>
<HoverCardContent
align="start"
className="w-[260px] text-sm"
side="left"
>
{`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.`}
</HoverCardContent>
</HoverCard>
<Popover open={open} onOpenChange={setOpen} {...props}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
aria-label="Select a model"
className="w-full justify-between"
>
{selectedModel ? selectedModel.name : "Select a model..."}
<CaretSortIcon className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-[250px] p-0">
<HoverCard>
<HoverCardContent
side="left"
align="start"
forceMount
className="hidden min-h-[265px] lg:block"
>
<div className="grid gap-2">
<h4 className="font-medium leading-none">{peekedModel.name}</h4>
<div className="text-sm text-muted-foreground">
{peekedModel.description}
</div>
</div>
</HoverCardContent>
<Command loop>
<CommandList className="max-h-[500px]">
<CommandInput placeholder="Search Models..." />
<CommandEmpty>No Models found.</CommandEmpty>
<HoverCardTrigger />
{types.map((type) => (
<CommandGroup key={type} heading={type}>
{models
.filter((model) => model.type === type)
.map((model) => (
<ModelItem
key={model.id}
model={model}
isSelected={selectedModel?.id === model.id}
onPeek={onModelPeek}
onSelect={onModelSelect(model)}
/>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</HoverCard>
</PopoverContent>
</Popover>
</div>
);
}
interface ModelItemProps {
model: Model;
isSelected: boolean;
onSelect: () => void;
onPeek: (model: Model) => void;
}
function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
const ref = React.useRef<HTMLDivElement>(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 (
<CommandItem
key={model.id}
onSelect={onSelect}
ref={ref}
className="aria-selected:bg-primary aria-selected:text-primary-foreground"
>
{model.name}
<CheckIcon
className={cn(
"ml-auto size-4",
isSelected ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
);
}
================================================
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<ChatParams>();
return (
<div className="grid gap-2 pt-4">
<HoverCard openDelay={200}>
<HoverCardTrigger>
<SliderField
name="presencePenalty"
label="Topic Relevance"
defaultValue={getValues("presencePenalty")}
min={-2}
max={2}
step={0.1}
className="[&_[role=slider]]:size-4"
control={control}
formState={formState}
aria-label="Topic Relevance"
/>
</HoverCardTrigger>
<HoverCardContent
align="start"
className="hidden w-[260px] text-sm lg:block"
side="left"
>
<p>
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.
</p>
</HoverCardContent>
</HoverCard>
</div>
);
}
================================================
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<ChatParams>();
return (
<div className="grid gap-2 pt-4">
<HoverCard openDelay={200}>
<HoverCardTrigger>
<SliderField
name="temperature"
label="Creativity"
defaultValue={getValues("temperature")}
min={0}
max={2}
step={0.1}
className="[&_[role=slider]]:size-4"
control={control}
formState={formState}
aria-label="Creativity"
/>
</HoverCardTrigger>
<HoverCardContent
align="start"
className="hidden w-[260px] text-sm lg:block"
side="left"
>
{`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.`}
</HoverCardContent>
</HoverCard>
</div>
);
}
================================================
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<ChatParams>();
return (
<div className="grid gap-2 pt-4">
<HoverCard openDelay={200}>
<HoverCardTrigger>
<SliderField
name="topP"
label="Focus"
defaultValue={getValues("topP")}
min={0}
max={1}
step={0.1}
className="[&_[role=slider]]:size-4"
control={control}
formState={formState}
aria-label="Focus"
/>
</HoverCardTrigger>
<HoverCardContent
align="start"
className="hidden w-[260px] text-sm lg:block"
side="left"
>
<p>
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.
</p>
<p>
We generally recommend altering this or Creativity but not both.
</p>
</HoverCardContent>
</HoverCard>
</div>
);
}
================================================
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<Type = string> {
id: string;
name: string;
description: string;
type: Type;
}
export const models: Model<ModelType>[] = [
{
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<typeof ChatParamSchema>;
export type ChatActionProps = {
chat: Chat;
} & Omit<AlertDialogProps, "children">;
================================================
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 (
<form action="/api/auth/logout" method="post">
<Button size="sm" variant="ghost" className="w-full justify-start">
<LogOut className="mr-2 size-4" />
<span>Log out</span>
</Button>
</form>
);
}
================================================
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<boolean>(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 (
<Button
variant="outline"
type="button"
disabled={isLoading}
onClick={signIn}
{...rest}
>
{isLoading ? <Loader className="animate-spin" /> : children}
</Button>
);
};
================================================
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 (
<div className="grid gap-2 lg:grid-cols-3">
<SocialLoginButton provider="google">
<CustomIcon.google />
</SocialLoginButton>
<SocialLoginButton provider="twitter">
<CustomIcon.x />
</SocialLoginButton>
<SocialLoginButton provider="github">
<Github />
</SocialLoginButton>
</div>
);
};
================================================
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<HTMLDivElement>;
type FormData = z.infer<typeof credentialAuthSchema>;
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const supabase = createClient();
const { register, formState, handleSubmit } = useForm<FormData>({
mode: "onChange",
resolver: zodResolver(credentialAuthSchema),
});
const [isLoading, setIsLoading] = React.useState<boolean>(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 (
<div
className={cn(
"grid gap-6 rounded-lg p-4 backdrop-blur-3xl lg:rounded-none lg:p-0 lg:backdrop-blur-none",
className
)}
{...props}
>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-1">
<InputField
name="email"
label="Email"
placeholder="name@example.com"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading}
{...fieldProps}
/>
<InputField
name="password"
label="Password"
placeholder="********"
type="password"
autoCapitalize="none"
autoCorrect="off"
disabled={isLoading}
{...fieldProps}
/>
<Button disabled={isLoading} className="mt-2">
{isLoading && <Loader className="mr-2 size-4 animate-spin" />}
Sign In
</Button>
</div>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<SocialLoginOptions />
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
{`Don't have an account yet?`}
</span>
<Link href="/signup" className="text-primary">
Sign Up
</Link>
</div>
</div>
);
}
================================================
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<HTMLDivElement>;
type FormData = z.infer<typeof registerProfileSchema>;
export function UserSignupForm({ className, ...props }: UserSignupFormProps) {
const supabase = createClient();
const { replace } = useRouter();
const { register, formState, handleSubmit } = useForm<FormData>({
mode: "onChange",
resolver: zodResolver(registerProfileSchema),
});
const [isLoading, setIsLoading] = React.useState<boolean>(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 (
<div
className={cn(
"grid gap-6 rounded-lg p-4 backdrop-blur-3xl lg:rounded-none lg:p-0 lg:backdrop-blur-none",
className
)}
{...props}
>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-2">
<div className="grid gap-1">
<InputField
name="email"
label="Email"
placeholder="name@example.com"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading}
{...fieldProps}
/>
</div>
<div className="grid gap-1">
<InputField
name="fullName"
label="Full Name"
placeholder="John Doe"
autoCapitalize="none"
autoCorrect="off"
disabled={isLoading}
{...fieldProps}
/>
</div>
<div className="grid gap-1">
<InputField
name="password"
label="Password"
placeholder="********"
type="password"
autoCapitalize="none"
autoCorrect="off"
disabled={isLoading}
{...fieldProps}
/>
</div>
<div className="grid gap-1">
<InputField
name="confirmPassword"
label="Confirm Password"
placeholder="********"
type="password"
autoCapitalize="none"
autoCorrect="off"
disabled={isLoading}
{...fieldProps}
/>
</div>
<Button disabled={isLoading}>
{isLoading && <Loader className="mr-2 size-4 animate-spin" />}
Sign up
</Button>
</div>
</form>
<div className="relative flex justify-center text-xs uppercase">
<span className="px-2 text-muted-foreground">
Already have an account?
</span>
<Link href="/signin" className="text-primary">
Sign in
</Link>
</div>
</div>
);
}
================================================
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 (
<div className="px-4">
<motion.span
className="h-16 max-w-2xl leading-normal text-muted-foreground sm:text-xl sm:leading-8"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 2 }}
>
{displayedText
? displayedText
: "A feature-rich, highly customizable AI Chatbot Template, empowered by Next.js x Supabase."}
</motion.span>
<motion.span
className="ml-1 inline-flex h-[22px] w-[2px] animate-blink rounded-full bg-current align-sub opacity-75 delay-1000"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 2 }}
/>
</div>
);
}
================================================
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 (
<motion.section
id="features"
className="container space-y-6 bg-slate-50 py-8 dark:bg-transparent md:py-12 lg:py-24"
initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
>
<div className="mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center">
<h2 className="font-heading bg-gradient-to-r from-gray-900 to-gray-500 bg-clip-text text-3xl font-bold leading-[1.1] tracking-tighter text-transparent dark:from-white dark:to-gray-500 sm:text-4xl">
Modern Web Tools for Chatbot App
</h2>
<p className="leading-normal text-muted-foreground sm:text-lg sm:leading-7">
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.
</p>
</div>
<div className="mx-auto grid justify-center gap-4 sm:grid-cols-2 md:max-w-5xl md:grid-cols-3">
<Card>
<CardHeader>
<svg viewBox="0 0 24 24" className="mb-2 size-12 fill-current">
<path d="M11.572 0c-.176 0-.31.001-.358.007a19.76 19.76 0 0 1-.364.033C7.443.346 4.25 2.185 2.228 5.012a11.875 11.875 0 0 0-2.119 5.243c-.096.659-.108.854-.108 1.747s.012 1.089.108 1.748c.652 4.506 3.86 8.292 8.209 9.695.779.25 1.6.422 2.534.525.363.04 1.935.04 2.299 0 1.611-.178 2.977-.577 4.323-1.264.207-.106.247-.134.219-.158-.02-.013-.9-1.193-1.955-2.62l-1.919-2.592-2.404-3.558a338.739 338.739 0 0 0-2.422-3.556c-.009-.002-.018 1.579-.023 3.51-.007 3.38-.01 3.515-.052 3.595a.426.426 0 0 1-.206.214c-.075.037-.14.044-.495.044H7.81l-.108-.068a.438.438 0 0 1-.157-.171l-.05-.106.006-4.703.007-4.705.072-.092a.645.645 0 0 1 .174-.143c.096-.047.134-.051.54-.051.478 0 .558.018.682.154.035.038 1.337 1.999 2.895 4.361a10760.433 10760.433 0 0 0 4.735 7.17l1.9 2.879.096-.063a12.317 12.317 0 0 0 2.466-2.163 11.944 11.944 0 0 0 2.824-6.134c.096-.66.108-.854.108-1.748 0-.893-.012-1.088-.108-1.747-.652-4.506-3.859-8.292-8.208-9.695a12.597 12.597 0 0 0-2.499-.523A33.119 33.119 0 0 0 11.573 0zm4.069 7.217c.347 0 .408.005.486.047a.473.473 0 0 1 .237.277c.018.06.023 1.365.018 4.304l-.006 4.218-.744-1.14-.746-1.14v-3.066c0-1.982.01-3.097.023-3.15a.478.478 0 0 1 .233-.296c.096-.05.13-.054.5-.054z" />
</svg>
<CardTitle>Next.js 14</CardTitle>
<CardDescription>
App Router, Routing, Layouts, Route Handlers and Server Actions.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<svg viewBox="0 0 24 24" className="mb-2 size-12 fill-[#0866FF]">
<path d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38a2.167 2.167 0 0 0-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44a23.476 23.476 0 0 0-3.107-.534A23.892 23.892 0 0 0 12.769 4.7c1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442a22.73 22.73 0 0 0-3.113.538 15.02 15.02 0 0 1-.254-1.42c-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87a25.64 25.64 0 0 1-4.412.005 26.64 26.64 0 0 1-1.183-1.86c-.372-.64-.71-1.29-1.018-1.946a25.17 25.17 0 0 1 1.013-1.954c.38-.66.773-1.286 1.18-1.868A25.245 25.245 0 0 1 12 8.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933a25.952 25.952 0 0 0-1.345-2.32zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493a23.966 23.966 0 0 0-1.1-2.98c.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98a23.142 23.142 0 0 0-1.086 2.964c-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39a25.819 25.819 0 0 0 1.341-2.338zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143a22.005 22.005 0 0 1-2.006-.386c.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295a1.185 1.185 0 0 1-.553-.132c-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z" />
</svg>
<CardTitle>React 18</CardTitle>
<CardDescription>
Server and Client Components. New Hooks.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<svg
className="mb-2 size-12 fill-current"
width="109"
height="113"
viewBox="0 0 109 113"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint0_linear)"
/>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
fill="url(#paint1_linear)"
fillOpacity="0.2"
/>
<path
d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
fill="#3ECF8E"
/>
<defs>
<linearGradient
id="paint0_linear"
x1="53.9738"
y1="54.974"
x2="94.1635"
y2="71.8295"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#249361" />
<stop offset="1" stopColor="#3ECF8E" />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="36.1558"
y1="30.578"
x2="54.4844"
y2="65.0806"
gradientUnits="userSpaceOnUse"
>
<stop />
<stop offset="1" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
<CardTitle className="font-bold">Database</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
Secure and high-performance Postgres backends with Supabase.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<svg viewBox="0 0 24 24" className="mb-2 size-12 fill-[#38BDF8]">
<path d="M12.001 4.8c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624C13.666 10.618 15.027 12 18.001 12c3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624C16.337 6.182 14.976 4.8 12.001 4.8zm-6 7.2c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624 1.177 1.194 2.538 2.576 5.512 2.576 3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624C10.337 13.382 8.976 12 6.001 12z" />
</svg>
<CardTitle className="font-bold">Components</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
Shadcn UI, React Hook Form and styled with Tailwind CSS.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="#249361"
strokeWidth="1"
className="mb-2 size-12 fill-[#3ECF8E]"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
<CardTitle className="font-bold">Authentication</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
Authenticate and authorize your users with Supabase Auth.
</CardDescription>
</CardHeader>
</Card>
<Card>
<CardHeader>
<svg
fill="currentColor"
viewBox="0 0 24 24"
role="img"
xmlns="http://www.w3.org/2000/svg"
className="mb-2 size-12 fill-current"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g
id="SVGRepo_tracerCarrier"
strokeLinecap="round"
strokeLinejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
<title>OpenAI icon</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"></path>
</g>
</svg>
<CardTitle className="font-bold">Open AI</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
Using Open AI and Vercel AI SDK for building conversational
streaming UI.
</CardDescription>
</CardHeader>
</Card>
</div>
</motion.section>
);
};
================================================
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 <div className="h-[673px]" />;
const getImageSrc = () => {
if (theme === "dark") {
return "/featured-dark.jpg";
}
return "/featured.jpg";
};
return (
<motion.section
className="flex justify-center px-4 "
initial={{ opacity: 0, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<Image
src={getImageSrc()}
height={673}
width={1280}
alt="Resume GPT"
className="rounded-lg border-8"
/>
</motion.section>
);
};
================================================
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<boolean>();
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 (
<Button variant="ghost" className="h-14">
<Loader className="animate-spin" size={24} />
</Button>
);
}
if (!profile) {
return (
<Link
href="/signin"
className={cn(
buttonVariants({
variant: "outline",
}),
"ml-2"
)}
>
Signin
</Link>
);
}
const { username, avatar_url } = profile;
const nameLabel = username || userEmail || "";
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-14">
<div className="flex items-center">
<UserAvatar
username={username}
avatarUrl={avatar_url}
email={userEmail}
/>
<p className="ml-2 hidden md:block">{nameLabel}</p>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href="/profile">
<User className="mr-2 size-4" />
<span>Profile</span>
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<LogoutButton />
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
================================================
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 (
<div className="px-4 pt-4">
<UserAvatar
username={username}
avatarUrl={avatarUrl}
email={email}
className="size-24 border-2"
/>
<div className="mt-6">
<div className="flex items-end">
{fullName ? <p className="text-2xl">{fullName}</p> : null}
{username ? (
<p className="ml-1 text-muted-foreground">(@{username})</p>
) : null}
</div>
{email ? <Subtle className="mt-2">{email}</Subtle> : null}
</div>
</div>
);
};
================================================
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<HTMLDivElement>;
type FormData = z.infer<typeof profileSchema>;
export function ProfileForm({
className,
formValues,
...props
}: ProfileFormProps) {
const [isPendingUpdate, startUpdate] = React.useTransition();
const { register, formState, handleSubmit, reset } = useForm<FormData>({
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 (
<div className={cn("grid gap-2 p-4", className)} {...props}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-2">
<div className="grid gap-1">
<InputField
name="username"
label="Username"
autoCapitalize="none"
autoCorrect="off"
disabled={isPendingUpdate}
{...fieldProps}
/>
</div>
<div className="grid gap-1">
<InputField
name="fullName"
label="Full Name"
placeholder="John Doe"
autoCapitalize="none"
autoCorrect="off"
disabled={isPendingUpdate}
{...fieldProps}
/>
</div>
<div className="grid gap-1">
<InputField
name="website"
label="Website"
autoCapitalize="none"
autoCorrect="off"
disabled={isPendingUpdate}
{...fieldProps}
/>
</div>
</div>
<div className="mt-4 grid grid-cols-1 gap-4 lg:flex lg:justify-end">
<Link
href="/"
className={cn(
buttonVariants({ variant: "outline" }),
"mr-2 w-full lg:w-auto"
)}
>
Cancel
</Link>
<Button className="w-full lg:w-auto" disabled={isPendingUpdate}>
{isPendingUpdate && <Loader className="mr-2 size-4 animate-spin" />}
Save
</Button>
</div>
</form>
</div>
);
}
================================================
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<typeof profileSchema>;
================================================
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 (
<div className="fixed top-0 z-50 w-full bg-background shadow-md dark:border-b">
<div className="flex h-16 items-center justify-between px-4">
<div className="flex items-center">
<label
htmlFor="my-drawer"
className={cn(
buttonVariants({
variant: "ghost",
size: "sm",
}),
"mr-2 lg:hidden"
)}
>
<Menu />
</label>
<div className="shrink-0 md:mr-6">
<AppLogo />
</div>
<NavigationMainMenu />
</div>
<div>
<div className="flex items-center">
<div className="hidden lg:block">
<ThemeToggle />
</div>
<div>
<AccountDropdownMenu userEmail={user?.email} />
</div>
</div>
</div>
</div>
</div>
);
};
================================================
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 (
<NavigationMenu className="hidden pl-4 md:pl-0 lg:block">
<NavigationMenuList>
<NavigationMenuItem asChild>
<NavigationMenuLink
href={siteConfig.links.github}
target="_blank"
className={buttonVariants({
variant: "ghost",
size: "sm",
})}
>
<Github size={16} />
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem asChild>
<NavigationMenuLink
href={siteConfig.links.x}
target="_blank"
className={buttonVariants({
variant: "ghost",
size: "sm",
})}
>
<CustomIcon.x width={12} height={12} />
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);
};
================================================
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 (
<div className="h-screen w-80 border-r bg-background">
<div className="flex items-center justify-between px-4 pt-2">
<AppLogo />
<ThemeToggle />
</div>
<Separator className="my-2" />
<ul className="menu py-0 pl-12">
<li>
<Link
href="#"
className="group flex items-center rounded-lg p-2 text-lg"
>
Explore
</Link>
</li>
<li>
<a
href={siteConfig.links.github}
className="group flex items-center rounded-lg p-2 text-lg"
>
Github
</a>
</li>
<li>
<a
href={siteConfig.links.x}
className="group flex items-center rounded-lg p-2 text-lg"
>
Follow on
<CustomIcon.x width={16} height={16} />
</a>
</li>
</ul>
</div>
);
};
================================================
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 (
<Button variant="ghost" size="sm" onClick={toggleTheme}>
<Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}
================================================
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<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="size-4 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className
)}
{...props}
>
<div className="pb-4 pt-0">{children}</div>
</AccordionPrimitive.Content>
));
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
) => <AlertDialogPrimitive.Portal {...props} />;
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
));
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<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square size-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
));
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<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
isLoading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
variant,
size,
isLoading,
children,
asChild = false,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{isLoading ? <Loader size={16} className="animate-spin" /> : children}
</Comp>
);
}
);
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<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
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<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex size-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
);
};
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) => (
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="24"
height="24"
viewBox="0,0,256,256"
{...props}
>
<g
className="fill-black dark:fill-white"
fill-rule="nonzero"
stroke="none"
strokeWidth="1"
strokeLinecap="butt"
strokeLinejoin="miter"
stroke-miterlimit="10"
stroke-dasharray=""
stroke-dashoffset="0"
font-family="none"
font-weight="none"
font-size="none"
text-anchor="none"
style={{ mixBlendMode: "normal" }}
>
<g transform="scale(8.53333,8.53333)">
<path d="M15.00391,3c-6.629,0 -12.00391,5.373 -12.00391,12c0,6.627 5.37491,12 12.00391,12c10.01,0 12.26517,-9.293 11.32617,-14h-1.33008h-2.26758h-7.73242v4h7.73828c-0.88958,3.44825 -4.01233,6 -7.73828,6c-4.418,0 -8,-3.582 -8,-8c0,-4.418 3.582,-8 8,-8c2.009,0 3.83914,0.74575 5.24414,1.96875l2.8418,-2.83984c-2.134,-1.944 -4.96903,-3.12891 -8.08203,-3.12891z"></path>
</g>
</g>
</svg>
),
x: (props: LucideProps) => (
<svg
className="fill-current"
height="20"
viewBox="0 0 1200 1227"
width="20"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"></path>
</svg>
),
};
================================================
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) => (
<DialogPrimitive.Portal {...props} />
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
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<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
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<HTMLDivElement>,
VariantProps<typeof flexVariants> {}
export const Flex = ({
className,
direction,
justify,
align,
wrap,
children,
...props
}: FlexProps) => {
return (
<div
className={cn(
flexVariants({ direction, justify, align, wrap, className })
)}
{...props}
>
{children}
</div>
);
};
================================================
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<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
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<HTMLInputElement> {
label?: string;
helperText?: string;
containerClassName?: string;
}
const Input = React.forwardRef<
HTMLInputElement,
InputProps & VariantProps<typeof inputVariants>
>(
(
{ className, label, helperText, containerClassName, isError, ...props },
ref
) => {
return (
<div
className={cn(
`relative grid w-full items-center gap-1.5`,
containerClassName
)}
>
{label ? <Label htmlFor={props.name}>{label}</Label> : null}
<input
className={cn(inputVariants({ isError, className }))}
ref={ref}
{...props}
/>
{helperText ? (
<p
className={cn("text-xs text-stone-500", isError && "text-red-500")}
>
{helperText}
</p>
) : null}
</div>
);
}
);
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<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
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<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
));
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<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] size-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
const NavigationListItem = React.forwardRef<
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
>(({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
ref={ref}
className={cn(
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
);
});
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<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
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<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex size-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.P
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
SYMBOL INDEX (200 symbols across 108 files)
FILE: app/(auth)/auth-code-error/page.tsx
function AuthCodeError (line 10) | async function AuthCodeError() {
FILE: app/(auth)/layout.tsx
type AuthLayoutProps (line 3) | type AuthLayoutProps = {
function AuthLayout (line 7) | function AuthLayout({ children }: AuthLayoutProps) {
FILE: app/(auth)/signin/page.tsx
function LoginPage (line 19) | async function LoginPage() {
FILE: app/(auth)/signup/page.tsx
function LoginPage (line 19) | async function LoginPage() {
FILE: app/api/auth/callback/route.ts
function GET (line 6) | async function GET(request: Request) {
FILE: app/api/auth/logout/route.ts
function POST (line 8) | async function POST(req: NextRequest) {
FILE: app/api/chat/route.ts
constant POST (line 27) | const POST = withAxiom(async (req: AxiomRequest) => {
FILE: app/apps/chat/[id]/page.tsx
function ChatPage (line 29) | async function ChatPage({ params }: { params: { id: string } }) {
FILE: app/apps/chat/loading.tsx
function Page (line 7) | function Page() {
FILE: app/apps/chat/page.tsx
function NewChatPage (line 16) | async function NewChatPage() {
FILE: app/apps/layout.tsx
type AppLayoutProps (line 10) | interface AppLayoutProps {
function AppLayout (line 14) | async function AppLayout({ children }: AppLayoutProps) {
FILE: app/apps/page.tsx
function Apps (line 5) | function Apps() {
FILE: app/docs/page.tsx
function Docs (line 1) | async function Docs() {
FILE: app/layout.tsx
type RootLayoutProps (line 34) | interface RootLayoutProps {
function RootLayout (line 38) | function RootLayout({ children }: RootLayoutProps) {
FILE: app/page.tsx
function Home (line 21) | async function Home() {
FILE: app/profile/layout.tsx
type AppLayoutProps (line 3) | interface AppLayoutProps {
function AppLayout (line 7) | function AppLayout({ children }: AppLayoutProps) {
FILE: app/profile/page.tsx
function Profile (line 14) | async function Profile() {
FILE: components/modules/apps/app-side-bar/AppSideBarItem.tsx
type AppSideBarItemProps (line 7) | type AppSideBarItemProps = Pick<
FILE: components/modules/apps/app-side-bar/AppSideBarList.tsx
type AppSideBarListProps (line 5) | type AppSideBarListProps = {
FILE: components/modules/apps/app-side-bar/AppSidebarSection.tsx
type AppSidebarSectionProps (line 5) | type AppSidebarSectionProps = {
FILE: components/modules/apps/chat/ChatForm.tsx
type ChatFormProps (line 13) | type ChatFormProps = {
FILE: components/modules/apps/chat/ChatHistory.tsx
type ChatHistoryProps (line 13) | type ChatHistoryProps = {
FILE: components/modules/apps/chat/ChatHistoryDrawer.tsx
type ChatHistoryDrawerProps (line 13) | type ChatHistoryDrawerProps = {
FILE: components/modules/apps/chat/ChatHistoryItem.tsx
type ChatHistoryItemProps (line 11) | type ChatHistoryItemProps = {
FILE: components/modules/apps/chat/ChatLayout.tsx
type ChatLayoutProps (line 3) | interface ChatLayoutProps {
FILE: components/modules/apps/chat/ChatPanel.tsx
type ChatPanelProps (line 42) | type ChatPanelProps = {
FILE: components/modules/apps/chat/CodeBlock.tsx
type Props (line 18) | interface Props {
type languageMap (line 23) | interface languageMap {
FILE: components/modules/apps/chat/MobileDrawerControls.tsx
type MobileDrawerControlProps (line 10) | type MobileDrawerControlProps = {
FILE: components/modules/apps/chat/NewChatButton.tsx
type NewChatButtonProps (line 10) | type NewChatButtonProps = {
FILE: components/modules/apps/chat/SystemPromptControl.tsx
type SystemPromptControlProps (line 17) | type SystemPromptControlProps = Pick<
FILE: components/modules/apps/chat/chat-members/AddMembersForm.tsx
type AddMembersFormProps (line 14) | type AddMembersFormProps = {
type AddMembersFormParams (line 18) | type AddMembersFormParams = z.infer<typeof addMemberSchema>;
FILE: components/modules/apps/chat/chat-members/ChatMemberItem.tsx
type ChatMemberItemProps (line 8) | type ChatMemberItemProps = {
FILE: components/modules/apps/chat/chat-members/ChatMembers.tsx
type ChatMembersProps (line 21) | type ChatMembersProps = {
FILE: components/modules/apps/chat/chat-members/DeleteMemberAction.tsx
type ChatActionProps (line 23) | type ChatActionProps = {
FILE: components/modules/apps/chat/control-side-bar/ControlSidebar.tsx
type ControlSidebarProps (line 27) | type ControlSidebarProps = Pick<UseChatHelpers, "setMessages" | "message...
FILE: components/modules/apps/chat/control-side-bar/ControlSidebarSheet.tsx
type ControlSidebarSheetProps (line 23) | type ControlSidebarSheetProps = {
FILE: components/modules/apps/chat/control-side-bar/FrequencyPenaltySelector.tsx
function FrequencyPenaltySelector (line 15) | function FrequencyPenaltySelector() {
FILE: components/modules/apps/chat/control-side-bar/MaxLengthSelector.tsx
function MaxLengthSelector (line 15) | function MaxLengthSelector() {
FILE: components/modules/apps/chat/control-side-bar/ModelSelector.tsx
type ModelSelectorProps (line 34) | interface ModelSelectorProps extends PopoverProps {
function ModelSelector (line 39) | function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
type ModelItemProps (line 141) | interface ModelItemProps {
function ModelItem (line 148) | function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemPro...
FILE: components/modules/apps/chat/control-side-bar/PresencePenaltySelector.tsx
function PresencePenaltySelector (line 15) | function PresencePenaltySelector() {
FILE: components/modules/apps/chat/control-side-bar/TemperatureSelector.tsx
function TemperatureSelector (line 15) | function TemperatureSelector() {
FILE: components/modules/apps/chat/control-side-bar/TopPSelector.tsx
function TopPSelector (line 15) | function TopPSelector() {
FILE: components/modules/apps/chat/control-side-bar/data/models.ts
type ModelType (line 6) | type ModelType = (typeof types)[number];
type Model (line 8) | interface Model<Type = string> {
FILE: components/modules/apps/chat/types.ts
type ChatParams (line 8) | type ChatParams = z.infer<typeof ChatParamSchema>;
type ChatActionProps (line 10) | type ChatActionProps = {
FILE: components/modules/auth/LogoutButton.tsx
function LogoutButton (line 8) | function LogoutButton() {
FILE: components/modules/auth/SocialLoginButton.tsx
type SocialLoginButtonProps (line 10) | type SocialLoginButtonProps = ButtonProps & {
FILE: components/modules/auth/UserAuthForm.tsx
type UserAuthFormProps (line 19) | type UserAuthFormProps = React.HTMLAttributes<HTMLDivElement>;
type FormData (line 21) | type FormData = z.infer<typeof credentialAuthSchema>;
function UserAuthForm (line 23) | function UserAuthForm({ className, ...props }: UserAuthFormProps) {
FILE: components/modules/auth/UserSignupForm.tsx
type UserSignupFormProps (line 19) | type UserSignupFormProps = React.HTMLAttributes<HTMLDivElement>;
type FormData (line 21) | type FormData = z.infer<typeof registerProfileSchema>;
function UserSignupForm (line 23) | function UserSignupForm({ className, ...props }: UserSignupFormProps) {
FILE: components/modules/home/DescriptionHeadingText.tsx
function DescriptionHeadingText (line 6) | function DescriptionHeadingText() {
FILE: components/modules/profile/AccountDropdownMenu.tsx
type AccountDropdownMenuProps (line 24) | type AccountDropdownMenuProps = {
FILE: components/modules/profile/Header.tsx
type HeaderProps (line 8) | type HeaderProps = {
FILE: components/modules/profile/ProfileForm.tsx
type ProfileFormProps (line 19) | type ProfileFormProps = {
type FormData (line 23) | type FormData = z.infer<typeof profileSchema>;
function ProfileForm (line 25) | function ProfileForm({
FILE: components/modules/profile/action.ts
function updateProfile (line 11) | async function updateProfile({
FILE: components/modules/profile/type.ts
type ProfileFormValues (line 5) | type ProfileFormValues = z.infer<typeof profileSchema>;
FILE: components/theme/ThemeToggle.tsx
function ThemeToggle (line 9) | function ThemeToggle() {
FILE: components/ui/Badge.tsx
type BadgeProps (line 26) | interface BadgeProps
function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: components/ui/Button.tsx
type ButtonProps (line 36) | interface ButtonProps
FILE: components/ui/Command.tsx
type CommandDialogProps (line 26) | type CommandDialogProps = DialogProps;
FILE: components/ui/Flex.tsx
type FlexProps (line 37) | interface FlexProps
FILE: components/ui/Input.tsx
type InputProps (line 19) | interface InputProps
FILE: components/ui/Section.tsx
type SectionProps (line 21) | type SectionProps = {
FILE: components/ui/Sheet.tsx
type SheetContentProps (line 55) | interface SheetContentProps
FILE: components/ui/Skeleton.tsx
function Skeleton (line 3) | function Skeleton({
FILE: components/ui/Slider.tsx
type SliderProps (line 10) | type SliderProps = React.ComponentPropsWithoutRef<
FILE: components/ui/TextArea.tsx
type TextAreaProps (line 11) | interface TextAreaProps
FILE: components/ui/Toast.tsx
type ToastProps (line 113) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement (line 115) | type ToastActionElement = React.ReactElement<typeof ToastAction>;
FILE: components/ui/Toaster.tsx
function Toaster (line 14) | function Toaster() {
FILE: components/ui/chat/ChatBubble.tsx
type ChatBubbleProps (line 17) | type ChatBubbleProps = {
method p (line 122) | p({ children }) {
method a (line 125) | a({ children, node: { properties } }) {
method code (line 166) | code({ inline, className, children, ...props }) {
FILE: components/ui/chat/ChatInput.tsx
type ChatTextAreaProps (line 16) | type ChatTextAreaProps = Omit<MentionsInputProps, "children"> & {
FILE: components/ui/chat/ChatList.tsx
type ChatListProps (line 21) | type ChatListProps = {
FILE: components/ui/chat/ChatProfileHoverCard.tsx
type ProfileHoverCardProps (line 13) | type ProfileHoverCardProps = {
FILE: components/ui/common/ChatScrollAnchor.tsx
type ChatScrollAnchorProps (line 8) | interface ChatScrollAnchorProps {
function ChatScrollAnchor (line 13) | function ChatScrollAnchor({
FILE: components/ui/common/MainLayout.tsx
type MainLayoutProps (line 4) | interface MainLayoutProps {
FILE: components/ui/common/UserAvatar.tsx
type UserAvatarProps (line 9) | type UserAvatarProps = {
FILE: components/ui/form/form-fields/InputField/InputField.tsx
type InputFieldProps (line 9) | type InputFieldProps<T extends FieldValues> = FormFieldProps<T> &
function InputField (line 14) | function InputField<T extends FieldValues>(props: InputFieldProps<T>) {
FILE: components/ui/form/form-fields/SliderField/SliderField.tsx
type SliderFieldProps (line 8) | type SliderFieldProps<T extends FieldValues> = ControlledFormFieldProps<...
function SliderField (line 11) | function SliderField<T extends FieldValues>(props: SliderFieldProps<T>) {
FILE: components/ui/form/form-fields/TextAreaField/TextAreaField.tsx
type TextAreaFieldProps (line 10) | type TextAreaFieldProps<T extends FieldValues> = FormFieldProps<T> &
function TextAreaField (line 14) | function TextAreaField<T extends FieldValues>(
FILE: components/ui/form/form-fields/types.ts
type FormFieldProps (line 9) | type FormFieldProps<T extends FieldValues = FieldValues> = {
type ControlledFormFieldProps (line 15) | type ControlledFormFieldProps<T extends FieldValues = FieldValues> = {
FILE: components/ui/typography/Blockquote.tsx
function Blockquote (line 5) | function Blockquote({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/Heading1.tsx
function Heading1 (line 5) | function Heading1({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/Heading2.tsx
function Heading2 (line 5) | function Heading2({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/Heading3.tsx
function Heading3 (line 5) | function Heading3({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/Heading4.tsx
function Heading4 (line 5) | function Heading4({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/Heading5.tsx
function Heading5 (line 5) | function Heading5({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/Paragraph.tsx
function Paragraph (line 5) | function Paragraph({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/Subtle.tsx
function Subtle (line 5) | function Subtle({ text, className, children }: TypographyProps) {
FILE: components/ui/typography/types.ts
type TypographyProps (line 1) | type TypographyProps = {
FILE: components/ui/use-toast.ts
constant TOAST_LIMIT (line 6) | const TOAST_LIMIT = 1;
constant TOAST_REMOVE_DELAY (line 7) | const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast (line 9) | type ToasterToast = ToastProps & {
function genId (line 25) | function genId() {
type ActionType (line 30) | type ActionType = typeof actionTypes;
type Action (line 32) | type Action =
type State (line 50) | interface State {
function dispatch (line 131) | function dispatch(action: Action) {
type Toast (line 138) | type Toast = Omit<ToasterToast, "id">;
function toast (line 140) | function toast({ ...props }: Toast) {
function useToast (line 169) | function useToast() {
FILE: config/site.ts
type SiteConfig (line 3) | type SiteConfig = typeof siteConfig;
FILE: hooks/useAtBottom.tsx
function useAtBottom (line 3) | function useAtBottom(offset = 0, element?: HTMLElement) {
FILE: hooks/useCopyToClipboard.tsx
type useCopyToClipboardProps (line 5) | interface useCopyToClipboardProps {
function useCopyToClipboard (line 9) | function useCopyToClipboard({
FILE: hooks/useEnterSubmit.tsx
type KeyboardEvent (line 3) | type KeyboardEvent =
function useEnterSubmit (line 7) | function useEnterSubmit(): {
FILE: hooks/useSubscribeChatMessages.ts
type ChatMemberStatus (line 8) | type ChatMemberStatus = {
type RealtimeChatMemberStatus (line 13) | type RealtimeChatMemberStatus = RealtimePresenceState<ChatMemberStatus>;
type UseSubscribeChatMessagesParams (line 15) | type UseSubscribeChatMessagesParams = {
function useSubscribeChatMessages (line 25) | function useSubscribeChatMessages({
FILE: lib/cache.ts
constant CACHE_TTL (line 1) | const CACHE_TTL = 3600;
constant CACHE_KEYS (line 3) | const CACHE_KEYS = {
FILE: lib/contants.ts
constant APP_SLUGS (line 5) | const APP_SLUGS = {
constant CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE (line 9) | const CHAT_MEMBER_SIDEBAR_LAYOUT_COOKIE =
constant DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT (line 12) | const DEFAULT_CHAT_MEMBER_SIDEBAR_LAYOUT = [70, 30];
constant MIN_CHAT_MEMBER_SIDEBAR_SIZE (line 13) | const MIN_CHAT_MEMBER_SIDEBAR_SIZE = 8;
constant MAX_CHAT_MEMBER_SIDEBAR_SIZE (line 14) | const MAX_CHAT_MEMBER_SIDEBAR_SIZE = 70;
constant CHAT_BOT_TRIGGER_WHITE_LIST (line 15) | const CHAT_BOT_TRIGGER_WHITE_LIST = ["assistant"];
constant MENTION_MARKUP (line 16) | const MENTION_MARKUP = "@[__display__](user:__id__)";
constant MENTION_TRIGGER (line 17) | const MENTION_TRIGGER = "@";
constant MAX_CHAT_INPUT_HEIGHT (line 18) | const MAX_CHAT_INPUT_HEIGHT = 210;
constant AI_ASSISTANT_PROFILE (line 19) | const AI_ASSISTANT_PROFILE: Profile = {
FILE: lib/db/database.types.ts
type Json (line 1) | type Json =
type Database (line 9) | type Database = {
type PublicSchema (line 690) | type PublicSchema = Database[Extract<keyof Database, "public">];
type Tables (line 692) | type Tables<
type TablesInsert (line 717) | type TablesInsert<
type TablesUpdate (line 738) | type TablesUpdate<
type Enums (line 759) | type Enums<
FILE: lib/db/index.ts
type Profile (line 4) | type Profile = Tables<"profiles">;
type App (line 5) | type App = Tables<"apps">;
type Chat (line 6) | type Chat = Tables<"chats">;
type ChatMember (line 7) | type ChatMember = Tables<"chat_members">;
type Message (line 8) | type Message = Tables<"messages">;
type ChatMemberProfile (line 9) | type ChatMemberProfile = {
type MessageAdditionalData (line 15) | type MessageAdditionalData = Pick<Message, "profile_id" | "chat_id"> & {
FILE: lib/db/message.ts
type CreateNewMessageParams (line 7) | type CreateNewMessageParams = Pick<
FILE: lib/stores/profile.ts
type Profile (line 5) | interface Profile {
FILE: lib/supabase/middleware.ts
method get (line 18) | get(name: string) {
method set (line 21) | set(name: string, value: string, options: CookieOptions) {
method remove (line 39) | remove(name: string, options: CookieOptions) {
FILE: lib/supabase/server.ts
method get (line 11) | get(name: string) {
method set (line 14) | set(name: string, value: string, options: CookieOptions) {
method remove (line 23) | remove(name: string, options: CookieOptions) {
FILE: lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: middleware.ts
function middleware (line 6) | async function middleware(req: NextRequest) {
FILE: supabase/migrations/20240402103717_init_schema.sql
type "public" (line 9) | create table "public"."apps" (
type "public" (line 20) | create table "public"."chats" (
type "public" (line 31) | create table "public"."customers" (
type "public" (line 39) | create table "public"."messages" (
type "public" (line 50) | create table "public"."prices" (
type "public" (line 67) | create table "public"."products" (
type "public" (line 79) | create table "public"."profiles" (
type "public" (line 93) | create table "public"."subscription_plans" (
type "public" (line 104) | create table "public"."subscriptions" (
type "public" (line 125) | create table "public"."token_usage" (
type apps_pkey (line 137) | CREATE UNIQUE INDEX apps_pkey ON public.apps USING btree (id)
type apps_slug_key (line 139) | CREATE UNIQUE INDEX apps_slug_key ON public.apps USING btree (slug)
type chat_sessions_pkey (line 141) | CREATE UNIQUE INDEX chat_sessions_pkey ON public.chats USING btree (id)
type messages_pkey (line 143) | CREATE UNIQUE INDEX messages_pkey ON public.messages USING btree (id)
type prices_pkey (line 145) | CREATE UNIQUE INDEX prices_pkey ON public.prices USING btree (id)
type products_pkey (line 147) | CREATE UNIQUE INDEX products_pkey ON public.products USING btree (id)
type profiles_pkey (line 149) | CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id)
type profiles_username_key (line 151) | CREATE UNIQUE INDEX profiles_username_key ON public.profiles USING btree...
type stripe_customers_pkey (line 153) | CREATE UNIQUE INDEX stripe_customers_pkey ON public.customers USING btre...
type subscription_plans_pkey (line 155) | CREATE UNIQUE INDEX subscription_plans_pkey ON public.subscription_plans...
type subscriptions_pkey (line 157) | CREATE UNIQUE INDEX subscriptions_pkey ON public.subscriptions USING btr...
type token_usage_pkey (line 159) | CREATE UNIQUE INDEX token_usage_pkey ON public.token_usage USING btree (id)
function public (line 243) | CREATE OR REPLACE FUNCTION public.handle_new_user()
function public (line 256) | CREATE OR REPLACE FUNCTION public.supabase_url()
FILE: supabase/migrations/20240420162835_chat_members.sql
type "public" (line 5) | create table "public"."chat_members" (
type chat_members_pkey (line 15) | CREATE UNIQUE INDEX chat_members_pkey ON public.chat_members USING btree...
FILE: supabase/migrations/20240609070425_handle_new_user_update.sql
function public (line 1) | CREATE OR REPLACE FUNCTION public.handle_new_user()
FILE: supabase/migrations/20240626065226_update_handle_new_user.sql
function public (line 1) | CREATE OR REPLACE FUNCTION public.handle_new_user()
FILE: tailwind.config.js
function addVariablesForColors (line 129) | function addVariablesForColors({ addBase, theme }) {
About this extraction
This page contains the full source code of the nphivu414/ai-fusion-kit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 190 files (329.0 KB), approximately 90.6k tokens, and a symbol index with 200 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.