- {/*
-
-
-
- Chat with PDF
-
- by Makify ✨
-
-
-
- {!!chats.length && (
- <>
-
-
-
-
-
-
- >
- )}
-
- */}
-
- Chat with PDF
-
- by Makify ✨
-
-
-
+
+
+
+
+
+
+
+
+ Toogle sidebar ⌘B
+
+
+
- {!!chats.length && }
-
-
-
-
-
- Feedback
-
- }
- />
-
-
-
+
+
);
diff --git a/apps/chat-with-pdf/app/components/header/user-nav.tsx b/apps/chat-with-pdf/app/components/header/user-nav.tsx
deleted file mode 100644
index 1df9179..0000000
--- a/apps/chat-with-pdf/app/components/header/user-nav.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
-} from "@makify/ui/components/avatar";
-import { Button } from "@makify/ui/components/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuTrigger,
-} from "@makify/ui/components/dropdown-menu";
-
-export function UserNav() {
- return (
-
-
-
-
-
-
-
-
shadcn
-
- m@example.com
-
-
-
-
-
-
- Profile
- ⇧⌘P
-
-
- Billing
- ⌘B
-
-
- Settings
- ⌘S
-
- New Team
-
-
-
- Log out
- ⇧⌘Q
-
-
-
- );
-}
diff --git a/apps/chat-with-pdf/app/components/header/user-nav/user-nav-menu-items.tsx b/apps/chat-with-pdf/app/components/header/user-nav/user-nav-menu-items.tsx
new file mode 100644
index 0000000..11263f5
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/header/user-nav/user-nav-menu-items.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { createClient } from "@/lib/supabase/client";
+import {
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ ToggleGroup,
+ ToggleGroupItem,
+} from "@makify/ui";
+import { LaptopMinimalIcon, MoonIcon, SunIcon } from "lucide-react";
+import { useTheme } from "next-themes";
+import { useRouter } from "next/navigation";
+
+export function UserNavMenuItems() {
+ const router = useRouter();
+ const { theme, setTheme } = useTheme();
+ console.log(theme);
+
+ async function handleLogout() {
+ const supabase = createClient();
+
+ await supabase.auth.signOut();
+
+ router.push("/login");
+ }
+
+ return (
+ <>
+
+
+ Profile
+ ⇧⌘P
+
+
+ Billing
+ ⌘B
+
+
+ Theme
+ setTheme(value)}
+ onClick={(event) => event.stopPropagation()}
+ className="border-border bg-background rounded-md border"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Settings
+ ⌘S
+
+
+
+
+ Log out
+ ⇧⌘Q
+ {" "}
+ >
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/header/user-nav/user-nav.tsx b/apps/chat-with-pdf/app/components/header/user-nav/user-nav.tsx
new file mode 100644
index 0000000..008bf05
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/header/user-nav/user-nav.tsx
@@ -0,0 +1,46 @@
+import { Button } from "@makify/ui/components/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@makify/ui/components/dropdown-menu";
+import { UserAvatar } from "@/components/ui/user-avatar";
+import { UserNavMenuItems } from "./user-nav-menu-items";
+import { createClient } from "@/lib/supabase/server";
+
+export async function UserNav() {
+ const supabase = createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ const userMetadata = user?.user_metadata;
+
+ return (
+
+
+
+
+
+
+
+
+ {userMetadata?.full_name || userMetadata?.name}
+
+
+ {user?.email}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/pages-containers/chat-id-container.tsx b/apps/chat-with-pdf/app/components/pages-containers/chat-id-container.tsx
index 44d57b6..677569a 100644
--- a/apps/chat-with-pdf/app/components/pages-containers/chat-id-container.tsx
+++ b/apps/chat-with-pdf/app/components/pages-containers/chat-id-container.tsx
@@ -1,6 +1,6 @@
import { ChatProvider } from "@/app/context/chat-context";
import { ChatScreen } from "../chat/chat-screen";
-import { Chat } from "@prisma/client";
+import { Tables } from "database.types";
type ChatIdContainerProps =
| {
@@ -9,7 +9,7 @@ type ChatIdContainerProps =
}
| {
loading?: false;
- chatData: Chat;
+ chatData: Tables<"Chat">;
};
export function ChatIdContainer({ chatData, loading }: ChatIdContainerProps) {
diff --git a/apps/chat-with-pdf/app/components/pages-containers/chats-container.tsx b/apps/chat-with-pdf/app/components/pages-containers/chats-container.tsx
index 9854521..7c1c42b 100644
--- a/apps/chat-with-pdf/app/components/pages-containers/chats-container.tsx
+++ b/apps/chat-with-pdf/app/components/pages-containers/chats-container.tsx
@@ -13,27 +13,30 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@makify/ui";
-import { Chat } from "@prisma/client";
import { FileTextIcon, PlusCircleIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { Container } from "../ui/container";
import { Heading } from "../ui/heading";
import { SadFaceIcon } from "icons/sad-face";
-import { NewDocumentDialog } from "../header/document-switcher/new-document-dialog/new-document-dialog";
+import { NewDocumentDialog } from "../header/document-title/new-document-dialog/new-document-dialog";
+import { Tables } from "database.types";
type ChatsContainerProps =
| {
loading: true;
chats?: never;
+ documents?: never;
}
| {
loading?: false;
- chats: Chat[];
+ chats: Tables<"Chat">[];
+ documents: Tables<"Document">[];
};
export function ChatsContainer({
chats,
+ documents,
loading = false,
}: ChatsContainerProps) {
const [isNewChatDialogOpen, setIsNewChatDialogOpen] = useState(false);
@@ -42,10 +45,14 @@ export function ChatsContainer({
setIsNewChatDialogOpen(!isNewChatDialogOpen);
}
+ function getChatData(chatId: string) {
+ return chats?.find((chat) => chat.id === chatId);
+ }
+
const fakeChatsList = Array.from({ length: 6 }).fill(null);
return (
-
+
- {chats?.length === 5 && (
+ {documents?.length === 5 && (
You have reached the maximum number of documents.
@@ -81,51 +88,52 @@ export function ChatsContainer({
{!loading &&
- chats?.map((chat) => (
+ documents?.map((document) => (
-
- {
- (chat?.documentMetadata as Record)
- ?.title
- }
-
+ {document?.name}
{
- (chat?.documentMetadata as Record)
- ?.numPages
+ (
+ getChatData(document.chatId || "")
+ ?.documentMetadata as Record
+ )?.numPages
}{" "}
page
- {(chat?.documentMetadata as Record)
- ?.numPages > 1
+ {(
+ getChatData(document.chatId || "")
+ ?.documentMetadata as Record
+ )?.numPages > 1
? "s"
: ""}{" "}
- {(chat?.documentMetadata as Record)?.size
- .mb > 1
- ? `${(chat?.documentMetadata as Record)?.size.mb} MB`
- : `${(chat?.documentMetadata as Record)?.size.kb} KB`}
+ {(
+ getChatData(document.chatId || "")
+ ?.documentMetadata as Record
+ )?.size.mb > 1
+ ? `${(getChatData(document.chatId || "")?.documentMetadata as Record)?.size.mb} MB`
+ : `${(getChatData(document.chatId || "")?.documentMetadata as Record)?.size.kb} KB`}
))}
{loading &&
fakeChatsList.map((_, index) => (
-
+
))}
{!loading && chats?.length === 0 && (
diff --git a/apps/chat-with-pdf/app/components/pages-containers/login/login-container.tsx b/apps/chat-with-pdf/app/components/pages-containers/login/login-container.tsx
new file mode 100644
index 0000000..db1ead0
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/pages-containers/login/login-container.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import Image from "next/image";
+import { Social } from "../signup/social";
+import { LoginForm } from "./login-form";
+import Logo from "@/public/logo.svg";
+
+export function LoginContainer() {
+ const queryString =
+ typeof window !== "undefined" ? window?.location.search : "";
+ const urlParams = new URLSearchParams(queryString);
+
+ // Get the value of the 'next' parameter
+ const next = urlParams.get("next");
+
+ return (
+
+
+
+
+
+
+
Sign in to Makify
+
Welcome back! Please sign in to continue
+
+
+
+
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/pages-containers/login/login-form.tsx b/apps/chat-with-pdf/app/components/pages-containers/login/login-form.tsx
new file mode 100644
index 0000000..d0252c3
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/pages-containers/login/login-form.tsx
@@ -0,0 +1,134 @@
+"use client";
+import { useRouter } from "next/navigation";
+import { useState, useTransition } from "react";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { AiOutlineLoading3Quarters } from "react-icons/ai";
+import { FaRegEye, FaRegEyeSlash } from "react-icons/fa6";
+import { z } from "zod";
+
+import { createClient } from "@/lib/supabase/client";
+import {
+ Button,
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ Input,
+ useToast,
+} from "@makify/ui";
+import { cn } from "@makify/ui/lib/utils";
+import Link from "next/link";
+
+const FormSchema = z.object({
+ email: z.string().email({ message: "Invalid Email Address" }),
+ password: z.string().min(6, { message: "Password is too short" }),
+});
+
+export function LoginForm({ redirectTo }: { redirectTo: string }) {
+ const [passwordReveal, setPasswordReveal] = useState(false);
+ const [isPending, startTransition] = useTransition();
+ const router = useRouter();
+
+ const { toast } = useToast();
+
+ const form = useForm
>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ });
+
+ function onSubmit(data: z.infer) {
+ const supabase = createClient();
+ if (!isPending) {
+ startTransition(async () => {
+ const { error } = await supabase.auth.signInWithPassword({
+ email: data.email,
+ password: data.password,
+ });
+ if (error) {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ } else {
+ router.push(redirectTo);
+ }
+ });
+ }
+ }
+
+ return (
+
+
+ Don't have an account?{" "}
+
+ Sign up
+
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/pages-containers/signup/sign-up-container.tsx b/apps/chat-with-pdf/app/components/pages-containers/signup/sign-up-container.tsx
new file mode 100644
index 0000000..df14fb0
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/pages-containers/signup/sign-up-container.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import Image from "next/image";
+import { SignUpForm } from "./signup-form";
+import { Social } from "./social";
+import Logo from "@/public/logo.svg";
+
+export function SignUpContainer() {
+ const queryString =
+ typeof window !== "undefined" ? window?.location.search : "";
+ const urlParams = new URLSearchParams(queryString);
+
+ // Get the value of the 'next' parameter
+ const next = urlParams.get("next");
+ const verify = urlParams.get("verify");
+
+ return (
+
+
+
+
+
+
+
Create Account
+
+ Welcome! Please fill in the details to get started.
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/pages-containers/signup/signup-form.tsx b/apps/chat-with-pdf/app/components/pages-containers/signup/signup-form.tsx
new file mode 100644
index 0000000..d8e4594
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/pages-containers/signup/signup-form.tsx
@@ -0,0 +1,353 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { FaRegEye, FaRegEyeSlash } from "react-icons/fa6";
+import { RiArrowRightSFill, RiArrowDropLeftFill } from "react-icons/ri";
+import { AiOutlineLoading3Quarters } from "react-icons/ai";
+import { SiMinutemailer } from "react-icons/si";
+import { REGEXP_ONLY_DIGITS } from "input-otp";
+import { useState, useTransition } from "react";
+import Link from "next/link";
+import { usePathname, useRouter } from "next/navigation";
+import {
+ FormMessage,
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ Input,
+ useToast,
+ Button,
+ InputOTP,
+ InputOTPGroup,
+ InputOTPSlot,
+ InputOTPSeparator,
+} from "@makify/ui";
+import { cn } from "@makify/ui/lib/utils";
+import { verifyOtp } from "@/app/actions/auth";
+
+const FormSchema = z
+ .object({
+ email: z.string().email({ message: "Invalid Email Address" }),
+ password: z.string().min(6, { message: "Password is too short" }),
+ "confirm-pass": z.string().min(6, { message: "Password is too short" }),
+ })
+ .refine(
+ (data) => {
+ if (data["confirm-pass"] !== data.password) {
+ console.log("running");
+ return false;
+ } else {
+ return true;
+ }
+ },
+ { message: "Password does't match", path: ["confirm-pass"] },
+ );
+
+export function SignUpForm({ redirectTo }: { redirectTo: string }) {
+ const queryString =
+ typeof window !== "undefined" ? window.location.search : "";
+ const urlParams = new URLSearchParams(queryString);
+
+ const verify = urlParams.get("verify");
+ const existEmail = urlParams.get("email");
+
+ const [passwordReveal, setPasswordReveal] = useState(false);
+ const [isConfirmed, setIsConfirmed] = useState(verify === "true");
+ const [verifyStatus, setVerifyStatus] = useState("");
+ const [isPending, startTransition] = useTransition();
+ const [isSendAgain, startSendAgain] = useTransition();
+ const pathname = usePathname();
+ const router = useRouter();
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ "confirm-pass": "",
+ },
+ });
+
+ const { toast } = useToast();
+
+ async function postEmail({
+ email,
+ password,
+ }: {
+ email: string;
+ password: string;
+ }) {
+ const requestOptions = {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email, password }),
+ };
+ // Send the POST request
+ const res = await fetch("/api/auth/signup", requestOptions);
+ return res.json();
+ }
+
+ async function sendVerifyEmail(data: z.infer) {
+ const json = await postEmail({
+ email: data.email,
+ password: data.password,
+ });
+ if (!json.error) {
+ router.replace(
+ (pathname || "/") + "?verify=true&email=" + form.getValues("email"),
+ );
+ setIsConfirmed(true);
+ } else {
+ if (json.error?.code) {
+ toast({
+ title: json.error.code,
+ variant: "destructive",
+ });
+ } else if (json.error?.message) {
+ toast({
+ title: json.error.message,
+ variant: "destructive",
+ });
+ }
+ }
+ }
+
+ const inputOptClass = cn({
+ " border-green-500": verifyStatus === "success",
+ " border-red-500": verifyStatus === "failed",
+ });
+
+ function onSubmit(data: z.infer) {
+ if (!isPending) {
+ startTransition(async () => {
+ await sendVerifyEmail(data);
+ });
+ }
+ }
+
+ return (
+
+
+
+ {/* verify email */}
+
+
+
+
+
Verify email
+
+ {" A verification code has been sent to "}
+
+ {verify === "true" ? existEmail : form.getValues("email")}
+
+
+
+
{
+ if (value.length === 6) {
+ document.getElementById("input-otp")?.blur();
+ const res = await verifyOtp({
+ email: form.getValues("email"),
+ otp: value,
+ type: "email",
+ });
+ const { error } = JSON.parse(res);
+ if (error) {
+ setVerifyStatus("failed");
+ } else {
+ setVerifyStatus("success");
+ router.push(redirectTo);
+ }
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{"Didn't work?"}
+
{
+ if (!isSendAgain) {
+ startSendAgain(async () => {
+ if (!form.getValues("password")) {
+ const json = await postEmail({
+ email: form.getValues("email"),
+ password: form.getValues("password"),
+ });
+
+ if (json.error) {
+ toast({
+ title: "Fail to resend email",
+ variant: "destructive",
+ });
+ } else {
+ toast({
+ title: "Please check your email.",
+ });
+ }
+ } else {
+ router.replace(pathname || "/register");
+ form.setValue("email", existEmail || "");
+ form.setValue("password", "");
+ setIsConfirmed(false);
+ }
+ });
+ }
+ }}
+ >
+
+ Send me another code.
+
+
+
+
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/pages-containers/signup/social.tsx b/apps/chat-with-pdf/app/components/pages-containers/signup/social.tsx
new file mode 100644
index 0000000..8ae1926
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/pages-containers/signup/social.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import React from "react";
+import { FcGoogle } from "react-icons/fc";
+import { IoLogoGithub } from "react-icons/io5";
+import { createClient } from "@/lib/supabase/client";
+import { Button, useToast } from "@makify/ui";
+
+export function Social({ redirectTo }: { redirectTo: string }) {
+ const { toast } = useToast();
+
+ const loginWithProvider = async (provider: "github" | "google") => {
+ const supbase = createClient();
+ const { error, data } = await supbase.auth.signInWithOAuth({
+ provider,
+ options: {
+ redirectTo:
+ window.location.origin + `/auth/callback?next=` + redirectTo,
+ },
+ });
+ console.log({ data });
+ if (error) {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+
+
+ {/* */}
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/pdf/pdf-viewer.tsx b/apps/chat-with-pdf/app/components/pdf/pdf-viewer.tsx
index fdf2992..7db0a64 100644
--- a/apps/chat-with-pdf/app/components/pdf/pdf-viewer.tsx
+++ b/apps/chat-with-pdf/app/components/pdf/pdf-viewer.tsx
@@ -23,7 +23,7 @@ import { useTheme } from "next-themes";
pdfjs.GlobalWorkerOptions.workerSrc = `/api/pdf-helper?url=unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
-const documentOptions = {
+export const documentOptions = {
cMapUrl: `/api/pdf-helper?url=unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
};
@@ -51,11 +51,10 @@ export function PdfViewer({ className }: { className?: string }) {
const popoverRef = useRef(null);
const [pdfData, setPdfData] = useState(null);
const {
- globalContext: { chatData, setExtraData },
+ globalContext: { chatData, setExtraData, documentState, setDocumentState },
} = useGlobalChat();
useOnClickOutside(popoverRef, () => setSelectedTextOptions(null));
/* Tools */
- const [currentPage, setCurrentPage] = useState(1);
const [currentZoom, setCurrentZoom] = useState(1);
const [enableChangePageOnScroll, setEnableChangePageOnScroll] =
useState(true);
@@ -120,16 +119,19 @@ export function PdfViewer({ className }: { className?: string }) {
if (canGoNextPage || canGoPrevPage) {
const safeNextPage = Math.min(
pdfData?.numPages as number,
- Math.max(1, currentPage + (scrollDirection === "down" ? 1 : -1)),
+ Math.max(
+ 1,
+ documentState.currentPage + (scrollDirection === "down" ? 1 : -1),
+ ),
);
- setCurrentPage(safeNextPage);
- }
+ setDocumentState({ currentPage: safeNextPage });
- return null;
+ return null;
+ }
}
function handlePageNumberChange(pageNumber: number) {
- setCurrentPage(pageNumber);
+ setDocumentState({ currentPage: pageNumber });
pdfPagesRef.current[pageNumber - 1]?.scrollIntoView();
}
@@ -170,7 +172,7 @@ export function PdfViewer({ className }: { className?: string }) {
setSelectedTextOptions(null);
setExtraData({
quotedText: selectedTextOptions?.selectedText,
- page: currentPage,
+ page: documentState.currentPage,
});
}
@@ -180,7 +182,7 @@ export function PdfViewer({ className }: { className?: string }) {
name[0])
+ .join("");
+ }
+
+ async function handleLogout() {
+ const supabase = createClient();
+
+ await supabase.auth.signOut();
+
+ router.push("/login");
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ Chat with PDF
+
+ by Makify ✨
+
+
+
+
+
+
+
+
+
+
+
+
+ New conversation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {getAvatarFallback()}
+
+
+
+
+ {userInfo?.user_metadata?.full_name ||
+ userInfo?.user_metadata?.name ||
+ "Unknown User"}
+
+
+ {userInfo?.email}
+
+
+
+
+
+
+
+
+
+
+
+ {getAvatarFallback()}
+
+
+
+
+ {userInfo?.user_metadata?.full_name ||
+ userInfo?.user_metadata?.name ||
+ "Unknown User"}
+
+
+ {userInfo?.email}
+
+
+
+
+
+
+
+
+ Upgrade to Pro
+
+
+
+
+
+
+ Account
+
+
+
+ Billing
+
+
+
+ Notifications
+
+
+
+
+
+ Log out
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/sidebar/recent-conversation-sidebar-group.tsx b/apps/chat-with-pdf/app/components/sidebar/recent-conversation-sidebar-group.tsx
new file mode 100644
index 0000000..7381b76
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/sidebar/recent-conversation-sidebar-group.tsx
@@ -0,0 +1,99 @@
+import { createClient } from "@/lib/supabase/client";
+import {
+ Button,
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@makify/ui";
+import { Tables } from "database.types";
+import { ChevronRight, MessageSquareIcon, MoreHorizontal } from "lucide-react";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+
+type RecentConversation = Record<
+ "id" | "name",
+ Tables<"Chat">["id"] | Tables<"Document">["name"]
+>;
+
+export default function RecentConversationsSidebarGroup() {
+ const [recentConversations, setRecentConversations] = useState<
+ RecentConversation[]
+ >([]);
+
+ useEffect(() => {
+ getRecentConversations();
+ }, []);
+
+ async function getRecentConversations() {
+ const supabase = createClient();
+
+ const { data, error } = await supabase
+ .from("Chat")
+ .select("id, updatedAt")
+ .order("updatedAt", { ascending: false })
+ .limit(3);
+
+ if (error) {
+ console.error(error);
+ return [];
+ }
+
+ if (data) {
+ const { data: documents, error } = await supabase
+ .from("Document")
+ .select("name, chatId")
+ .in(
+ "chatId",
+ data.map((chat) => chat.id),
+ );
+
+ if (error) {
+ console.error(error);
+ return [];
+ }
+
+ const recentConversations = data.map((chat, index) => ({
+ id: chat.id,
+ name:
+ documents.find((document) => document.chatId === chat.id)?.name ?? "",
+ }));
+
+ setRecentConversations(recentConversations);
+ }
+ }
+
+ return (
+
+ Recent conversations
+
+ {recentConversations.map((conversation) => (
+
+
+
+
+ {conversation.name}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/sidebar/secondary-sidebar-menu.tsx b/apps/chat-with-pdf/app/components/sidebar/secondary-sidebar-menu.tsx
new file mode 100644
index 0000000..42fb482
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/sidebar/secondary-sidebar-menu.tsx
@@ -0,0 +1,93 @@
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuItem,
+ SidebarMenuButton,
+ ToggleGroup,
+ ToggleGroupItem,
+ SidebarMenuAction,
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from "@makify/ui";
+import { cn } from "@makify/ui/lib/utils";
+import {
+ LaptopMinimalIcon,
+ MessageSquareIcon,
+ MoonIcon,
+ MoreHorizontal,
+ Plus,
+ Settings2Icon,
+ SunIcon,
+ SunMoonIcon,
+} from "lucide-react";
+import { useTheme } from "next-themes";
+import { FeedbackDialog } from "../header/feedback-dialog";
+
+const ThemeIconsMap = {
+ system: LaptopMinimalIcon,
+ light: SunIcon,
+ dark: MoonIcon,
+};
+
+const themeIconList = Object.keys(ThemeIconsMap);
+
+export function SecondarySidebarMenu() {
+ const { theme, setTheme } = useTheme();
+
+ const CurrentThemeIcon = ThemeIconsMap[theme as keyof typeof ThemeIconsMap];
+
+ return (
+
+
+
+
+
+
+ Feedback
+
+ }
+ />
+
+
+
+
+
+
+
+ Theme
+
+
+
+
+
+
+
+ {themeIconList.map((themeIcon) => {
+ const ThemeIcon =
+ ThemeIconsMap[themeIcon as keyof typeof ThemeIconsMap];
+ return (
+ setTheme(themeIcon)}
+ >
+
+ {themeIcon}
+
+ );
+ })}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/ui/logo.tsx b/apps/chat-with-pdf/app/components/ui/logo.tsx
new file mode 100644
index 0000000..82ff616
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/ui/logo.tsx
@@ -0,0 +1,21 @@
+import { cn } from "@makify/ui/lib/utils";
+
+type LogoProps = {
+ className?: string;
+};
+
+export function Logo({ className }: LogoProps) {
+ return (
+
+ Chat with PDF
+
+ by Makify ✨
+
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/components/ui/user-avatar.tsx b/apps/chat-with-pdf/app/components/ui/user-avatar.tsx
new file mode 100644
index 0000000..9ddb087
--- /dev/null
+++ b/apps/chat-with-pdf/app/components/ui/user-avatar.tsx
@@ -0,0 +1,48 @@
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@makify/ui/components/avatar";
+import { createClient } from "@/lib/supabase/server";
+import { cn } from "@makify/ui/lib/utils";
+
+interface UserAvatarProps {
+ className?: string;
+}
+
+export async function UserAvatar({ className }: UserAvatarProps) {
+ const supabase = createClient();
+ const {
+ data: { user },
+ error,
+ } = await supabase.auth.getUser();
+
+ function getAvatarFallback() {
+ if (!user) return "";
+ const userFullName =
+ user.user_metadata?.full_name || user.user_metadata?.name;
+ const userEmail = user.email;
+ const userFallback = (userFullName || userEmail)?.toUpperCase();
+
+ return userFallback
+ ?.split(" ")
+ .map((name: string) => name[0])
+ .join("");
+ }
+
+ if (error) {
+ console.error(error);
+ return (
+
+ !
+
+ );
+ }
+
+ return (
+
+
+ {getAvatarFallback()}
+
+ );
+}
diff --git a/apps/chat-with-pdf/app/context/chat-context.tsx b/apps/chat-with-pdf/app/context/chat-context.tsx
index a27249a..8dda933 100644
--- a/apps/chat-with-pdf/app/context/chat-context.tsx
+++ b/apps/chat-with-pdf/app/context/chat-context.tsx
@@ -1,7 +1,6 @@
"use client";
import { MESSAGE_TYPE } from "@/components/chat/constants/message-type";
-import { Chat } from "@prisma/client";
import { Message, useChat, UseChatOptions } from "ai/react";
import { useParams } from "next/navigation";
import {
@@ -13,8 +12,11 @@ import {
useState,
} from "react";
import { updateChatMessages } from "../actions/update-chat-messages";
+import { Tables } from "database.types";
+import { createClient } from "@/lib/supabase/client";
+import { generateDocumentTitle as generateDocumentTitleAction } from "../actions/generate-document-title";
-const EMPTY_CHAT_DATA: Partial = {
+const EMPTY_CHAT_DATA: Partial> = {
id: "",
documentMetadata: "",
documentUrl: "",
@@ -29,6 +31,12 @@ export const ChatContext = createContext({
setExtraData: (() => null) as Dispatch<
SetStateAction>
>,
+ documentState: {
+ currentPage: 1,
+ },
+ setDocumentState: (() => null) as Dispatch<
+ SetStateAction<{ currentPage: number }>
+ >,
},
initOptions: {} as UseChatOptions,
useChatReturn: {} as ReturnType,
@@ -36,12 +44,15 @@ export const ChatContext = createContext({
type ChatProviderProps = {
children: React.ReactNode;
- chatData: Partial;
+ chatData: Partial>;
};
export function ChatProvider({ children, chatData }: ChatProviderProps) {
const [isLoading, setIsLoading] = useState(true);
const [extraData, setExtraData] = useState>({});
+ const [documentState, setDocumentState] = useState({
+ currentPage: 1,
+ });
const params = useParams();
const initOptions = {
id: chatData.id,
@@ -57,20 +68,19 @@ export function ChatProvider({ children, chatData }: ChatProviderProps) {
const preloadPrompts = useRef([
{
message:
- "Introduce yourself without mention your name and summarize the document.",
+ "Introduce yourself and explain your purpose here. Mention that you're here to assist with the provided document. Avoid mentioning your name. Make your message friendly and professional.",
type: MESSAGE_TYPE.INTRODUCTION,
},
- {
- message:
- "Give me a list of a few questions that are already answered by the document content. Give me the questions as a list. Say those questions are suggestions to start and don't mention the questions are already answered by the document content.",
- type: MESSAGE_TYPE.SUGGESTION_MESSAGES,
- },
]);
useEffect(() => {
fetchChatData();
}, []);
+ useEffect(() => {
+ generateDocumentTitle();
+ }, [chatData.id]);
+
useEffect(sendPreloadedPrompts, [isLoading]);
useEffect(storeChatMessages, [
@@ -87,6 +97,32 @@ export function ChatProvider({ children, chatData }: ChatProviderProps) {
useChatReturn.setMessages(chatData?.messages as unknown as Message[]);
}
+ async function generateDocumentTitle() {
+ if (!chatData.id) return;
+ const supabase = createClient();
+
+ const chatId = chatData.id as string;
+
+ // Check if the document title is already set
+ const { data, error } = await supabase
+ .from("Document")
+ .select("name")
+ .eq("chatId", chatId)
+ .single();
+
+ const documentTitle = data?.name;
+ if (!documentTitle) {
+ const { title: generatedTitle } =
+ await generateDocumentTitleAction(chatId);
+ console.log({ generatedTitle });
+
+ await supabase
+ .from("Document")
+ .update({ name: generatedTitle })
+ .eq("chatId", chatId);
+ }
+ }
+
function sendPreloadedPrompts() {
const preloadPromptsArr = preloadPrompts.current;
@@ -130,7 +166,8 @@ export function ChatProvider({ children, chatData }: ChatProviderProps) {
if (hasAddedMessages && !useChatReturn.isLoading)
updateChatMessages({
documentId: params.documentId as string,
- messages: useChatReturn.messages as unknown as Chat["messages"],
+ messages:
+ useChatReturn.messages as unknown as Tables<"Chat">["messages"],
});
}
@@ -142,6 +179,8 @@ export function ChatProvider({ children, chatData }: ChatProviderProps) {
isLoading,
extraData,
setExtraData,
+ documentState,
+ setDocumentState,
},
useChatReturn,
initOptions,
diff --git a/apps/chat-with-pdf/app/favicon.ico b/apps/chat-with-pdf/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/apps/chat-with-pdf/app/favicon.ico and /dev/null differ
diff --git a/apps/chat-with-pdf/app/globals.css b/apps/chat-with-pdf/app/globals.css
index 3887641..12fb3f1 100644
--- a/apps/chat-with-pdf/app/globals.css
+++ b/apps/chat-with-pdf/app/globals.css
@@ -3,9 +3,21 @@
@tailwind utilities;
@layer base {
- input[type="number"]::-webkit-inner-spin-button,
- input[type="number"]::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
- }
-}
\ No newline at end of file
+ input[type="number"]::-webkit-inner-spin-button,
+ input[type="number"]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ ::-webkit-scrollbar {
+ @apply h-2.5 w-2.5;
+ }
+
+ ::-webkit-scrollbar-track {
+ @apply bg-transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ @apply bg-border rounded-full border-[1px] border-solid border-transparent bg-clip-padding;
+ }
+}
diff --git a/apps/chat-with-pdf/app/layout.tsx b/apps/chat-with-pdf/app/layout.tsx
index 8317f90..14430ac 100644
--- a/apps/chat-with-pdf/app/layout.tsx
+++ b/apps/chat-with-pdf/app/layout.tsx
@@ -2,15 +2,36 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import "@makify/ui/globals.css";
-import { Toaster } from "@makify/ui";
+import { SidebarProvider, Toaster } from "@makify/ui";
import { ThemeProvider } from "./components/theme-provider";
+import { cn } from "@makify/ui/lib/utils";
import PlausibleProvider from "next-plausible";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Chat with PDF",
+ description: "Get insights from your PDFs in seconds",
+ icons: {
+ icon: [
+ {
+ rel: "icon",
+ url: "/icon1.svg",
+ media: "(prefers-color-scheme: dark)",
+ type: "image/svg+xml",
+ },
+ {
+ rel: "icon",
+ url: "/icon2.svg",
+ media: "(prefers-color-scheme: light)",
+ type: "image/svg+xml",
+ },
+ {
+ rel: "apple-touch-icon",
+ url: "/icon1.svg",
+ },
+ ],
+ },
};
export default function RootLayout({
@@ -23,7 +44,7 @@ export default function RootLayout({
-
+
Hello {data.user.email};
+}
diff --git a/apps/chat-with-pdf/database.types.ts b/apps/chat-with-pdf/database.types.ts
new file mode 100644
index 0000000..ecf2812
--- /dev/null
+++ b/apps/chat-with-pdf/database.types.ts
@@ -0,0 +1,575 @@
+export type Json =
+ | string
+ | number
+ | boolean
+ | null
+ | { [key: string]: Json | undefined }
+ | Json[]
+
+export type Database = {
+ graphql_public: {
+ Tables: {
+ [_ in never]: never
+ }
+ Views: {
+ [_ in never]: never
+ }
+ Functions: {
+ graphql: {
+ Args: {
+ operationName?: string
+ query?: string
+ variables?: Json
+ extensions?: Json
+ }
+ Returns: Json
+ }
+ }
+ Enums: {
+ [_ in never]: never
+ }
+ CompositeTypes: {
+ [_ in never]: never
+ }
+ }
+ public: {
+ Tables: {
+ _prisma_migrations: {
+ Row: {
+ applied_steps_count: number
+ checksum: string
+ finished_at: string | null
+ id: string
+ logs: string | null
+ migration_name: string
+ rolled_back_at: string | null
+ started_at: string
+ }
+ Insert: {
+ applied_steps_count?: number
+ checksum: string
+ finished_at?: string | null
+ id: string
+ logs?: string | null
+ migration_name: string
+ rolled_back_at?: string | null
+ started_at?: string
+ }
+ Update: {
+ applied_steps_count?: number
+ checksum?: string
+ finished_at?: string | null
+ id?: string
+ logs?: string | null
+ migration_name?: string
+ rolled_back_at?: string | null
+ started_at?: string
+ }
+ Relationships: []
+ }
+ Chat: {
+ Row: {
+ createdAt: string
+ documentMetadata: Json | null
+ documentUrl: string | null
+ id: string
+ messages: Json | null
+ suggestedQuestions: string[] | null
+ updatedAt: string
+ userId: string
+ }
+ Insert: {
+ createdAt?: string
+ documentMetadata?: Json | null
+ documentUrl?: string | null
+ id?: string
+ messages?: Json | null
+ suggestedQuestions?: string[] | null
+ updatedAt?: string
+ userId?: string
+ }
+ Update: {
+ createdAt?: string
+ documentMetadata?: Json | null
+ documentUrl?: string | null
+ id?: string
+ messages?: Json | null
+ suggestedQuestions?: string[] | null
+ updatedAt?: string
+ userId?: string
+ }
+ Relationships: []
+ }
+ Document: {
+ Row: {
+ chatId: string | null
+ createdAt: string
+ id: string
+ metadata: Json | null
+ name: string | null
+ updatedAt: string
+ url: string | null
+ userId: string
+ }
+ Insert: {
+ chatId?: string | null
+ createdAt?: string
+ id?: string
+ metadata?: Json | null
+ name?: string | null
+ updatedAt?: string
+ url?: string | null
+ userId?: string
+ }
+ Update: {
+ chatId?: string | null
+ createdAt?: string
+ id?: string
+ metadata?: Json | null
+ name?: string | null
+ updatedAt?: string
+ url?: string | null
+ userId?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "Document_chatId_fkey"
+ columns: ["chatId"]
+ isOneToOne: false
+ referencedRelation: "Chat"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ DocumentSections: {
+ Row: {
+ chatId: string | null
+ documentId: string | null
+ embedding: string | null
+ id: string
+ pageNumber: number | null
+ text: string | null
+ textChunk: string | null
+ userId: string
+ }
+ Insert: {
+ chatId?: string | null
+ documentId?: string | null
+ embedding?: string | null
+ id?: string
+ pageNumber?: number | null
+ text?: string | null
+ textChunk?: string | null
+ userId?: string
+ }
+ Update: {
+ chatId?: string | null
+ documentId?: string | null
+ embedding?: string | null
+ id?: string
+ pageNumber?: number | null
+ text?: string | null
+ textChunk?: string | null
+ userId?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "DocumentSections_chatId_fkey"
+ columns: ["chatId"]
+ isOneToOne: false
+ referencedRelation: "Chat"
+ referencedColumns: ["id"]
+ },
+ {
+ foreignKeyName: "DocumentSections_documentId_fkey"
+ columns: ["documentId"]
+ isOneToOne: false
+ referencedRelation: "Document"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ Feedback: {
+ Row: {
+ createdAt: string
+ id: string
+ message: string
+ type: string
+ updatedAt: string
+ userId: string | null
+ }
+ Insert: {
+ createdAt?: string
+ id: string
+ message: string
+ type: string
+ updatedAt: string
+ userId?: string | null
+ }
+ Update: {
+ createdAt?: string
+ id?: string
+ message?: string
+ type?: string
+ updatedAt?: string
+ userId?: string | null
+ }
+ Relationships: []
+ }
+ profiles: {
+ Row: {
+ avatarUrl: string | null
+ email: string | null
+ firstName: string | null
+ id: string
+ lastName: string | null
+ role: string | null
+ }
+ Insert: {
+ avatarUrl?: string | null
+ email?: string | null
+ firstName?: string | null
+ id: string
+ lastName?: string | null
+ role?: string | null
+ }
+ Update: {
+ avatarUrl?: string | null
+ email?: string | null
+ firstName?: string | null
+ id?: string
+ lastName?: string | null
+ role?: string | null
+ }
+ Relationships: [
+ {
+ foreignKeyName: "profiles_id_fkey"
+ columns: ["id"]
+ isOneToOne: true
+ referencedRelation: "users"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ }
+ Views: {
+ [_ in never]: never
+ }
+ Functions: {
+ binary_quantize:
+ | {
+ Args: {
+ "": string
+ }
+ Returns: unknown
+ }
+ | {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ halfvec_avg: {
+ Args: {
+ "": number[]
+ }
+ Returns: unknown
+ }
+ halfvec_out: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ halfvec_send: {
+ Args: {
+ "": unknown
+ }
+ Returns: string
+ }
+ halfvec_typmod_in: {
+ Args: {
+ "": unknown[]
+ }
+ Returns: number
+ }
+ hnsw_bit_support: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ hnsw_halfvec_support: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ hnsw_sparsevec_support: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ hnswhandler: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ ivfflat_bit_support: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ ivfflat_halfvec_support: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ ivfflathandler: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ l2_norm:
+ | {
+ Args: {
+ "": unknown
+ }
+ Returns: number
+ }
+ | {
+ Args: {
+ "": unknown
+ }
+ Returns: number
+ }
+ l2_normalize:
+ | {
+ Args: {
+ "": string
+ }
+ Returns: string
+ }
+ | {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ | {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ match_documents:
+ | {
+ Args: {
+ query_embedding: string
+ match_threshold: number
+ document_id: string
+ }
+ Returns: {
+ chatId: string | null
+ documentId: string | null
+ embedding: string | null
+ id: string
+ pageNumber: number | null
+ text: string | null
+ textChunk: string | null
+ userId: string
+ }[]
+ }
+ | {
+ Args: {
+ query_embedding: string
+ match_threshold: number
+ match_count: number
+ }
+ Returns: {
+ chatId: string | null
+ documentId: string | null
+ embedding: string | null
+ id: string
+ pageNumber: number | null
+ text: string | null
+ textChunk: string | null
+ userId: string
+ }[]
+ }
+ | {
+ Args: {
+ query_embedding: string
+ match_threshold: number
+ match_count: number
+ document_id: string
+ }
+ Returns: {
+ chatId: string | null
+ documentId: string | null
+ embedding: string | null
+ id: string
+ pageNumber: number | null
+ text: string | null
+ textChunk: string | null
+ userId: string
+ }[]
+ }
+ sparsevec_out: {
+ Args: {
+ "": unknown
+ }
+ Returns: unknown
+ }
+ sparsevec_send: {
+ Args: {
+ "": unknown
+ }
+ Returns: string
+ }
+ sparsevec_typmod_in: {
+ Args: {
+ "": unknown[]
+ }
+ Returns: number
+ }
+ vector_avg: {
+ Args: {
+ "": number[]
+ }
+ Returns: string
+ }
+ vector_dims:
+ | {
+ Args: {
+ "": string
+ }
+ Returns: number
+ }
+ | {
+ Args: {
+ "": unknown
+ }
+ Returns: number
+ }
+ vector_norm: {
+ Args: {
+ "": string
+ }
+ Returns: number
+ }
+ vector_out: {
+ Args: {
+ "": string
+ }
+ Returns: unknown
+ }
+ vector_send: {
+ Args: {
+ "": string
+ }
+ Returns: string
+ }
+ vector_typmod_in: {
+ Args: {
+ "": unknown[]
+ }
+ Returns: number
+ }
+ }
+ Enums: {
+ [_ in never]: never
+ }
+ CompositeTypes: {
+ [_ in never]: never
+ }
+ }
+}
+
+type PublicSchema = Database[Extract]
+
+export type Tables<
+ PublicTableNameOrOptions extends
+ | keyof (PublicSchema["Tables"] & PublicSchema["Views"])
+ | { schema: keyof Database },
+ TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
+ ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
+ Database[PublicTableNameOrOptions["schema"]]["Views"])
+ : never = never,
+> = PublicTableNameOrOptions extends { schema: keyof Database }
+ ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
+ Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
+ Row: infer R
+ }
+ ? R
+ : never
+ : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
+ PublicSchema["Views"])
+ ? (PublicSchema["Tables"] &
+ PublicSchema["Views"])[PublicTableNameOrOptions] extends {
+ Row: infer R
+ }
+ ? R
+ : never
+ : never
+
+export type TablesInsert<
+ PublicTableNameOrOptions extends
+ | keyof PublicSchema["Tables"]
+ | { schema: keyof Database },
+ TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
+ ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
+ : never = never,
+> = PublicTableNameOrOptions extends { schema: keyof Database }
+ ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
+ Insert: infer I
+ }
+ ? I
+ : never
+ : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
+ ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
+ Insert: infer I
+ }
+ ? I
+ : never
+ : never
+
+export type TablesUpdate<
+ PublicTableNameOrOptions extends
+ | keyof PublicSchema["Tables"]
+ | { schema: keyof Database },
+ TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
+ ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
+ : never = never,
+> = PublicTableNameOrOptions extends { schema: keyof Database }
+ ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
+ Update: infer U
+ }
+ ? U
+ : never
+ : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
+ ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
+ Update: infer U
+ }
+ ? U
+ : never
+ : never
+
+export type Enums<
+ PublicEnumNameOrOptions extends
+ | keyof PublicSchema["Enums"]
+ | { schema: keyof Database },
+ EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
+ ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
+ : never = never,
+> = PublicEnumNameOrOptions extends { schema: keyof Database }
+ ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
+ : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
+ ? PublicSchema["Enums"][PublicEnumNameOrOptions]
+ : never
+
diff --git a/apps/chat-with-pdf/lib/constants/index.ts b/apps/chat-with-pdf/lib/constants/index.ts
new file mode 100644
index 0000000..678ece4
--- /dev/null
+++ b/apps/chat-with-pdf/lib/constants/index.ts
@@ -0,0 +1,2 @@
+export const protectedPaths = ["/chat"];
+export const authPaths = ["/login", "/signup"];
diff --git a/apps/chat-with-pdf/lib/supabase/admin.ts b/apps/chat-with-pdf/lib/supabase/admin.ts
new file mode 100644
index 0000000..1de1aff
--- /dev/null
+++ b/apps/chat-with-pdf/lib/supabase/admin.ts
@@ -0,0 +1,14 @@
+import { createClient } from "@supabase/supabase-js";
+
+export function supabaseAdmin() {
+ return createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_ADMIN!,
+ {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false,
+ },
+ },
+ );
+}
diff --git a/apps/chat-with-pdf/lib/supabase/middleware.ts b/apps/chat-with-pdf/lib/supabase/middleware.ts
new file mode 100644
index 0000000..c416248
--- /dev/null
+++ b/apps/chat-with-pdf/lib/supabase/middleware.ts
@@ -0,0 +1,81 @@
+import { createServerClient, type CookieOptions } from "@supabase/ssr";
+import { protectedPaths, authPaths } from "lib/constants";
+import { NextResponse, type NextRequest } from "next/server";
+
+export async function updateSession(request: NextRequest) {
+ let response = NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ return request.cookies.get(name)?.value;
+ },
+ set(name: string, value: string, options: CookieOptions) {
+ request.cookies.set({
+ name,
+ value,
+ ...options,
+ });
+ response = NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+ response.cookies.set({
+ name,
+ value,
+ ...options,
+ });
+ },
+ remove(name: string, options: CookieOptions) {
+ request.cookies.set({
+ name,
+ value: "",
+ ...options,
+ });
+ response = NextResponse.next({
+ request: {
+ headers: request.headers,
+ },
+ });
+ response.cookies.set({
+ name,
+ value: "",
+ ...options,
+ });
+ },
+ },
+ },
+ );
+
+ const user = await supabase.auth.getUser();
+ const url = new URL(request.url);
+ const next = url.searchParams.get("next");
+
+ if (user.error && !authPaths.includes(url.pathname)) {
+ return NextResponse.redirect(
+ new URL("/login?next=" + (next || url.pathname), request.url),
+ );
+ }
+
+ if (user.data.user?.id) {
+ if (authPaths.includes(url.pathname)) {
+ return NextResponse.redirect(new URL("/", request.url));
+ }
+ return response;
+ } else {
+ if (protectedPaths.includes(url.pathname)) {
+ return NextResponse.redirect(
+ new URL("/login?next=" + (next || url.pathname), request.url),
+ );
+ }
+ return response;
+ }
+}
diff --git a/apps/chat-with-pdf/middleware.ts b/apps/chat-with-pdf/middleware.ts
new file mode 100644
index 0000000..526eb0e
--- /dev/null
+++ b/apps/chat-with-pdf/middleware.ts
@@ -0,0 +1,20 @@
+import { updateSession } from "lib/supabase/middleware";
+import { type NextRequest } from "next/server";
+
+export async function middleware(request: NextRequest) {
+ return await updateSession(request);
+}
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except for the ones starting with:
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ * - api/auth (API authentication routes)
+ * - Image file extensions like svg, png, jpg, etc.
+ */
+ "/((?!_next/static|_next/image|favicon.ico|auth|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
+ ],
+};
diff --git a/apps/chat-with-pdf/next.config.js b/apps/chat-with-pdf/next.config.js
index 7b35d89..78b6b64 100644
--- a/apps/chat-with-pdf/next.config.js
+++ b/apps/chat-with-pdf/next.config.js
@@ -12,6 +12,11 @@ const nextConfig = {
'.js': ['.js', '.ts', '.tsx'],
};
+ config.module.rules.push({
+ test: /\.svg$/,
+ use: ['@svgr/webpack'],
+ });
+
return config;
},
transpilePackages: ['@makify/ui'],
diff --git a/apps/chat-with-pdf/package.json b/apps/chat-with-pdf/package.json
index b3d7f30..a429f6a 100644
--- a/apps/chat-with-pdf/package.json
+++ b/apps/chat-with-pdf/package.json
@@ -3,16 +3,21 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev --experimental-https",
- "prisma:generate": "prisma generate",
+ "dev": "NODE_ENV=development next dev --experimental-https",
+ "prisma:dev": "dotenv -e .env.development.local -- prisma",
+ "prisma:generate:dev": "dotenv -e .env.development.local -- prisma generate",
+ "prisma:generate": "prisma generate dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
+ "@ai-sdk/anthropic": "0.0.50",
"@makify/ui": "workspace:*",
"@prisma/client": "5.16.2",
+ "@react-email/components": "0.0.25",
"@react-pdf/renderer": "3.4.4",
+ "@supabase/ssr": "0.5.1",
"@supabase/supabase-js": "2.44.4",
"@upstash/ratelimit": "2.0.1",
"@vercel/kv": "2.0.0",
@@ -25,16 +30,20 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-dropzone": "14.2.3",
+ "react-email": "3.0.1",
"react-hook-form": "7.52.1",
+ "react-icons": "5.3.0",
"react-pdf": "9.1.0",
"react-to-pdf": "1.0.1",
+ "rehype-raw": "7.0.0",
"remark-directive": "3.0.0",
"remark-directive-rehype": "0.4.2",
"remark-gfm": "4.0.0",
+ "resend": "4.0.0",
"usehooks-ts": "3.1.0"
},
"devDependencies": {
- "@ai-sdk/google": "0.0.14",
+ "@ai-sdk/google": "0.0.48",
"@ai-sdk/openai": "0.0.13",
"@google/generative-ai": "0.14.1",
"@langchain/community": "0.2.16",
@@ -45,18 +54,20 @@
"@makify/eslint-config": "workspace:*",
"@makify/typescript-config": "workspace:*",
"@pinecone-database/pinecone": "2.2.2",
+ "@svgr/webpack": "^8.1.0",
"@types/node": "20",
"@types/react": "18.2.61",
"@types/react-dom": "18.2.19",
- "ai": "3.2.37",
- "autoprefixer": "10.0.1",
+ "ai": "3.4.30",
+ "autoprefixer": "10.4.20",
+ "dotenv-cli": "7.4.2",
"js-md5": "0.8.3",
"langchain": "0.2.8",
"pdf-parse": "1.1.1",
- "postcss": "8",
+ "postcss": "8.4.47",
"prisma": "5.16.2",
"react-markdown": "9.0.1",
- "tailwindcss": "3.3.0",
+ "tailwindcss": "3.4.14",
"typescript": "5.3.3",
"zod": "3.23.8"
}
diff --git a/apps/chat-with-pdf/prisma/migrations/20240818222106_add_document/migration.sql b/apps/chat-with-pdf/prisma/migrations/20240818222106_add_document/migration.sql
new file mode 100644
index 0000000..5f57289
--- /dev/null
+++ b/apps/chat-with-pdf/prisma/migrations/20240818222106_add_document/migration.sql
@@ -0,0 +1,26 @@
+/*
+ Warnings:
+
+ - Added the required column `documentId` to the `Chat` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "Chat" ADD COLUMN "documentId" TEXT NOT NULL;
+
+-- CreateTable
+CREATE TABLE "Document" (
+ "id" TEXT NOT NULL,
+ "name" TEXT,
+ "url" TEXT,
+ "chatId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Document_id_key" ON "Document"("id");
+
+-- AddForeignKey
+ALTER TABLE "Chat" ADD CONSTRAINT "Chat_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/apps/chat-with-pdf/prisma/migrations/20240818230228_add_optional_fields/migration.sql b/apps/chat-with-pdf/prisma/migrations/20240818230228_add_optional_fields/migration.sql
new file mode 100644
index 0000000..30990c2
--- /dev/null
+++ b/apps/chat-with-pdf/prisma/migrations/20240818230228_add_optional_fields/migration.sql
@@ -0,0 +1,14 @@
+-- DropForeignKey
+ALTER TABLE "Chat" DROP CONSTRAINT "Chat_documentId_fkey";
+
+-- AlterTable
+ALTER TABLE "Chat" ALTER COLUMN "documentId" DROP NOT NULL;
+
+ -- Example: enable the "vector" extension on supabase.
+create extension vector with schema public;
+
+-- AlterTable
+ALTER TABLE "Document" ADD COLUMN "embedding" vector(768);
+
+-- AddForeignKey
+ALTER TABLE "Chat" ADD CONSTRAINT "Chat_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/apps/chat-with-pdf/prisma/migrations/20240818233331_add_doument_metadata/migration.sql b/apps/chat-with-pdf/prisma/migrations/20240818233331_add_doument_metadata/migration.sql
new file mode 100644
index 0000000..4938f4e
--- /dev/null
+++ b/apps/chat-with-pdf/prisma/migrations/20240818233331_add_doument_metadata/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "Document" ADD COLUMN "metadata" JSONB,
+ALTER COLUMN "chatId" DROP NOT NULL;
diff --git a/apps/chat-with-pdf/prisma/migrations/20240819225259_add_document_sections/migration.sql b/apps/chat-with-pdf/prisma/migrations/20240819225259_add_document_sections/migration.sql
new file mode 100644
index 0000000..f2585c1
--- /dev/null
+++ b/apps/chat-with-pdf/prisma/migrations/20240819225259_add_document_sections/migration.sql
@@ -0,0 +1,40 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `documentId` on the `Chat` table. All the data in the column will be lost.
+ - You are about to drop the column `embedding` on the `Document` table. All the data in the column will be lost.
+ - A unique constraint covering the columns `[chatId]` on the table `Document` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- DropForeignKey
+ALTER TABLE "Chat" DROP CONSTRAINT "Chat_documentId_fkey";
+
+-- AlterTable
+ALTER TABLE "Chat" DROP COLUMN "documentId";
+
+-- AlterTable
+ALTER TABLE "Document" DROP COLUMN "embedding";
+
+-- CreateTable
+CREATE TABLE "DocumentSections" (
+ "id" TEXT NOT NULL,
+ "embedding" vector(768),
+ "chatId" TEXT,
+ "text" TEXT,
+ "pageNumber" INTEGER,
+ "documentId" TEXT,
+
+ CONSTRAINT "DocumentSections_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DocumentSections_id_key" ON "DocumentSections"("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Document_chatId_key" ON "Document"("chatId");
+
+-- AddForeignKey
+ALTER TABLE "Document" ADD CONSTRAINT "Document_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DocumentSections" ADD CONSTRAINT "DocumentSections_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/apps/chat-with-pdf/prisma/migrations/20240821023002_add_the_text_chunk_column/migration.sql b/apps/chat-with-pdf/prisma/migrations/20240821023002_add_the_text_chunk_column/migration.sql
new file mode 100644
index 0000000..a03bcf0
--- /dev/null
+++ b/apps/chat-with-pdf/prisma/migrations/20240821023002_add_the_text_chunk_column/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "DocumentSections" ADD COLUMN "textChunk" TEXT;
diff --git a/apps/chat-with-pdf/prisma/migrations/20240823013715_generate_random_uuid_on_document_sections_table/migration.sql b/apps/chat-with-pdf/prisma/migrations/20240823013715_generate_random_uuid_on_document_sections_table/migration.sql
new file mode 100644
index 0000000..211e651
--- /dev/null
+++ b/apps/chat-with-pdf/prisma/migrations/20240823013715_generate_random_uuid_on_document_sections_table/migration.sql
@@ -0,0 +1,15 @@
+/*
+ Warnings:
+
+ - The primary key for the `DocumentSections` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - The `id` column on the `DocumentSections` table would be dropped and recreated. This will lead to data loss if there is data in the column.
+
+*/
+-- AlterTable
+ALTER TABLE "DocumentSections" DROP CONSTRAINT "DocumentSections_pkey",
+DROP COLUMN "id",
+ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
+ADD CONSTRAINT "DocumentSections_pkey" PRIMARY KEY ("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DocumentSections_id_key" ON "DocumentSections"("id");
diff --git a/apps/chat-with-pdf/prisma/migrations/20240824221542_match_documents_function/migration.sql b/apps/chat-with-pdf/prisma/migrations/20240824221542_match_documents_function/migration.sql
new file mode 100644
index 0000000..83d86a8
--- /dev/null
+++ b/apps/chat-with-pdf/prisma/migrations/20240824221542_match_documents_function/migration.sql
@@ -0,0 +1,15 @@
+-- Match documents using negative inner product (<#>)
+create or replace function match_documents (
+ query_embedding vector(768),
+ match_threshold float,
+ document_id uuid
+)
+returns setof "DocumentSections"
+language sql
+as $$
+ select *
+ from "DocumentSections"
+ where "DocumentSections".embedding <#> query_embedding < -match_threshold
+ and "DocumentSections"."chatId"::text = document_id::text
+ order by "DocumentSections"."pageNumber" asc
+$$;
\ No newline at end of file
diff --git a/apps/chat-with-pdf/prisma/schema.prisma b/apps/chat-with-pdf/prisma/schema.prisma
index fabd1fb..52e0a96 100644
--- a/apps/chat-with-pdf/prisma/schema.prisma
+++ b/apps/chat-with-pdf/prisma/schema.prisma
@@ -1,26 +1,50 @@
generator client {
- provider = "prisma-client-js"
+ provider = "prisma-client-js"
}
datasource db {
- provider = "postgresql"
- url = env("DATABASE_URL")
- directUrl = env("DIRECT_URL")
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+ directUrl = env("DIRECT_URL")
}
model Chat {
- id String @id @unique @default(uuid())
- documentMetadata Json?
- documentUrl String?
- messages Json?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ id String @id @unique @default(uuid())
+ documentMetadata Json?
+ documentUrl String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ messages Json?
+ document Document?
+}
+
+model Document {
+ id String @id @unique @default(uuid())
+ name String?
+ url String?
+ metadata Json?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ chatId String? @unique
+ chat Chat? @relation(fields: [chatId], references: [id])
+ documentSections DocumentSections[]
+}
+
+model DocumentSections {
+ id String @id @unique @default(dbgenerated("extensions.gen_random_uuid()")) @db.Uuid
+ embedding Unsupported("vector(768)")?
+ chatId String?
+ text String?
+ textChunk String?
+ pageNumber Int?
+ document Document? @relation(fields: [documentId], references: [id])
+ documentId String?
}
model Feedback {
- id String @id @unique @default(uuid())
- type String
- message String
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ id String @id @unique @default(uuid())
+ type String
+ message String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
}
diff --git a/apps/chat-with-pdf/public/genesis-cv.pdf b/apps/chat-with-pdf/public/genesis-cv.pdf
deleted file mode 100644
index 5f8c9b5..0000000
Binary files a/apps/chat-with-pdf/public/genesis-cv.pdf and /dev/null differ
diff --git a/apps/chat-with-pdf/public/icon1.svg b/apps/chat-with-pdf/public/icon1.svg
new file mode 100644
index 0000000..b14f9b9
--- /dev/null
+++ b/apps/chat-with-pdf/public/icon1.svg
@@ -0,0 +1,34 @@
+
diff --git a/apps/chat-with-pdf/public/icon2.svg b/apps/chat-with-pdf/public/icon2.svg
new file mode 100644
index 0000000..a674b7b
--- /dev/null
+++ b/apps/chat-with-pdf/public/icon2.svg
@@ -0,0 +1,34 @@
+
diff --git a/apps/chat-with-pdf/public/logo.svg b/apps/chat-with-pdf/public/logo.svg
new file mode 100644
index 0000000..5b2c77c
--- /dev/null
+++ b/apps/chat-with-pdf/public/logo.svg
@@ -0,0 +1,45 @@
+
+
+
diff --git a/apps/chat-with-pdf/supabase/.gitignore b/apps/chat-with-pdf/supabase/.gitignore
new file mode 100644
index 0000000..a3ad880
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/.gitignore
@@ -0,0 +1,4 @@
+# Supabase
+.branches
+.temp
+.env
diff --git a/apps/chat-with-pdf/supabase/config.toml b/apps/chat-with-pdf/supabase/config.toml
new file mode 100644
index 0000000..128c0c8
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/config.toml
@@ -0,0 +1,245 @@
+# A string used to distinguish different Supabase projects on the same host. Defaults to the
+# working directory name when running `supabase init`.
+project_id = "chat-with-pdf"
+
+[api]
+enabled = true
+# Port to use for the API URL.
+port = 54321
+# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
+# endpoints. `public` is always included.
+schemas = ["public", "graphql_public"]
+# Extra schemas to add to the search_path of every request. `public` is always included.
+extra_search_path = ["public", "extensions"]
+# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
+# for accidental or malicious requests.
+max_rows = 1000
+
+[api.tls]
+enabled = false
+
+[db]
+# Port to use for the local database URL.
+port = 54322
+# Port used by db diff command to initialize the shadow database.
+shadow_port = 54320
+# The database major version to use. This has to be the same as your remote database's. Run `SHOW
+# server_version;` on the remote database to check.
+major_version = 15
+
+[db.pooler]
+enabled = false
+# Port to use for the local connection pooler.
+port = 54329
+# Specifies when a server connection can be reused by other clients.
+# Configure one of the supported pooler modes: `transaction`, `session`.
+pool_mode = "transaction"
+# How many server connections to allow per user/database pair.
+default_pool_size = 20
+# Maximum number of client connections allowed.
+max_client_conn = 100
+
+[realtime]
+enabled = true
+# Bind realtime via either IPv4 or IPv6. (default: IPv4)
+# ip_version = "IPv6"
+# The maximum length in bytes of HTTP request headers. (default: 4096)
+# max_header_length = 4096
+
+[studio]
+enabled = true
+# Port to use for Supabase Studio.
+port = 54323
+# External URL of the API server that frontend connects to.
+api_url = "http://127.0.0.1"
+# OpenAI API Key to use for Supabase AI in the Supabase Studio.
+openai_api_key = "env(OPENAI_API_KEY)"
+
+# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
+# are monitored, and you can view the emails that would have been sent from the web interface.
+[inbucket]
+enabled = true
+# Port to use for the email testing server web interface.
+port = 54324
+# Uncomment to expose additional ports for testing user applications that send emails.
+# smtp_port = 54325
+# pop3_port = 54326
+
+[storage]
+enabled = true
+# The maximum file size allowed (e.g. "5MB", "500KB").
+file_size_limit = "50MiB"
+
+[storage.image_transformation]
+enabled = true
+
+# Uncomment to configure local storage buckets
+# [storage.buckets.images]
+# public = false
+# file_size_limit = "50MiB"
+# allowed_mime_types = ["image/png", "image/jpeg"]
+# objects_path = "./images"
+
+[auth]
+enabled = true
+# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
+# in emails.
+site_url = "https://localhost:3000/"
+# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
+additional_redirect_urls = ["https://localhost:3000/**"]
+# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
+jwt_expiry = 3600
+# If disabled, the refresh token will never expire.
+enable_refresh_token_rotation = true
+# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
+# Requires enable_refresh_token_rotation = true.
+refresh_token_reuse_interval = 10
+# Allow/disallow new user signups to your project.
+enable_signup = true
+# Allow/disallow anonymous sign-ins to your project.
+enable_anonymous_sign_ins = false
+# Allow/disallow testing manual linking of accounts
+enable_manual_linking = false
+
+[auth.email]
+# Allow/disallow new user signups via email to your project.
+enable_signup = true
+# If enabled, a user will be required to confirm any email change on both the old, and new email
+# addresses. If disabled, only the new email is required to confirm.
+double_confirm_changes = true
+# If enabled, users need to confirm their email address before signing in.
+enable_confirmations = true
+# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
+max_frequency = "1s"
+
+# Use a production-ready SMTP server
+# [auth.email.smtp]
+# host = "smtp.sendgrid.net"
+# port = 587
+# user = "apikey"
+# pass = "env(SENDGRID_API_KEY)"
+# admin_email = "admin@email.com"
+# sender_name = "Admin"
+
+# Uncomment to customize email template
+# [auth.email.template.invite]
+# subject = "You have been invited"
+# content_path = "./supabase/templates/invite.html"
+
+[auth.sms]
+# Allow/disallow new user signups via SMS to your project.
+enable_signup = true
+# If enabled, users need to confirm their phone number before signing in.
+enable_confirmations = false
+# Template for sending OTP to users
+template = "Your code is {{ .Code }} ."
+# Controls the minimum amount of time that must pass before sending another sms otp.
+max_frequency = "5s"
+
+[auth.external.github]
+# Allow/disallow new user signups via GitHub to your project.
+enabled = true
+# Set the GitHub OAuth client ID and secret.
+client_id = "env(SUPABASE_AUTH_GITHUB_CLIENT_ID)"
+secret = "env(SUPABASE_AUTH_GITHUB_SECRET)"
+redirect_uri = "http://127.0.0.1:54321/auth/v1/callback"
+
+# Use pre-defined map of phone number to OTP for testing.
+# [auth.sms.test_otp]
+# 4152127777 = "123456"
+
+# Configure logged in session timeouts.
+# [auth.sessions]
+# Force log out after the specified duration.
+# timebox = "24h"
+# Force log out if the user has been inactive longer than the specified duration.
+# inactivity_timeout = "8h"
+
+# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
+# [auth.hook.custom_access_token]
+# enabled = true
+# uri = "pg-functions:////"
+
+# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
+[auth.sms.twilio]
+enabled = false
+account_sid = ""
+message_service_sid = ""
+# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
+auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
+
+[auth.mfa]
+# Control how many MFA factors can be enrolled at once per user.
+max_enrolled_factors = 10
+
+# Control use of MFA via App Authenticator (TOTP)
+[auth.mfa.totp]
+enroll_enabled = true
+verify_enabled = true
+
+# Configure Multi-factor-authentication via Phone Messaging
+# [auth.mfa.phone]
+# enroll_enabled = true
+# verify_enabled = true
+# otp_length = 6
+# template = "Your code is {{ .Code }} ."
+# max_frequency = "10s"
+
+# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
+# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
+# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
+[auth.external.apple]
+enabled = false
+client_id = ""
+# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
+secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
+# Overrides the default auth redirectUrl.
+redirect_uri = ""
+# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
+# or any other third-party OIDC providers.
+url = ""
+# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
+skip_nonce_check = false
+
+# Use Firebase Auth as a third-party provider alongside Supabase Auth.
+[auth.third_party.firebase]
+enabled = false
+# project_id = "my-firebase-project"
+
+# Use Auth0 as a third-party provider alongside Supabase Auth.
+[auth.third_party.auth0]
+enabled = false
+# tenant = "my-auth0-tenant"
+# tenant_region = "us"
+
+# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
+[auth.third_party.aws_cognito]
+enabled = false
+# user_pool_id = "my-user-pool-id"
+# user_pool_region = "us-east-1"
+
+[edge_runtime]
+enabled = true
+# Configure one of the supported request policies: `oneshot`, `per_worker`.
+# Use `oneshot` for hot reload, or `per_worker` for load testing.
+policy = "oneshot"
+inspector_port = 8083
+
+[analytics]
+enabled = true
+port = 54327
+# Configure one of the supported backends: `postgres`, `bigquery`.
+backend = "postgres"
+
+# Experimental features may be deprecated any time
+[experimental]
+# Configures Postgres storage engine to use OrioleDB (S3)
+orioledb_version = ""
+# Configures S3 bucket URL, eg. .s3-.amazonaws.com
+s3_host = "env(S3_HOST)"
+# Configures S3 bucket region, eg. us-east-1
+s3_region = "env(S3_REGION)"
+# Configures AWS_ACCESS_KEY_ID for S3 bucket
+s3_access_key = "env(S3_ACCESS_KEY)"
+# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
+s3_secret_key = "env(S3_SECRET_KEY)"
diff --git a/apps/chat-with-pdf/supabase/migrations/20240818213221_remote_schema.sql b/apps/chat-with-pdf/supabase/migrations/20240818213221_remote_schema.sql
new file mode 100644
index 0000000..384e696
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/migrations/20240818213221_remote_schema.sql
@@ -0,0 +1,114 @@
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
+
+COMMENT ON SCHEMA "public" IS 'standard public schema';
+
+CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
+
+CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
+
+CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
+
+CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
+
+CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
+
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
+
+SET default_tablespace = '';
+
+SET default_table_access_method = "heap";
+
+CREATE TABLE IF NOT EXISTS "public"."Chat" (
+ "id" "text" NOT NULL,
+ "documentMetadata" "jsonb",
+ "documentUrl" "text",
+ "createdAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ "updatedAt" timestamp(3) without time zone NOT NULL,
+ "messages" "jsonb"
+);
+
+ALTER TABLE "public"."Chat" OWNER TO "postgres";
+
+CREATE TABLE IF NOT EXISTS "public"."Feedback" (
+ "id" "text" NOT NULL,
+ "type" "text" NOT NULL,
+ "message" "text" NOT NULL,
+ "createdAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ "updatedAt" timestamp(3) without time zone NOT NULL
+);
+
+ALTER TABLE "public"."Feedback" OWNER TO "postgres";
+
+CREATE TABLE IF NOT EXISTS "public"."_prisma_migrations" (
+ "id" character varying(36) NOT NULL,
+ "checksum" character varying(64) NOT NULL,
+ "finished_at" timestamp with time zone,
+ "migration_name" character varying(255) NOT NULL,
+ "logs" "text",
+ "rolled_back_at" timestamp with time zone,
+ "started_at" timestamp with time zone DEFAULT "now"() NOT NULL,
+ "applied_steps_count" integer DEFAULT 0 NOT NULL
+);
+
+ALTER TABLE "public"."_prisma_migrations" OWNER TO "postgres";
+
+ALTER TABLE ONLY "public"."Chat"
+ ADD CONSTRAINT "Chat_pkey" PRIMARY KEY ("id");
+
+ALTER TABLE ONLY "public"."Feedback"
+ ADD CONSTRAINT "Feedback_pkey" PRIMARY KEY ("id");
+
+ALTER TABLE ONLY "public"."_prisma_migrations"
+ ADD CONSTRAINT "_prisma_migrations_pkey" PRIMARY KEY ("id");
+
+CREATE UNIQUE INDEX "Chat_id_key" ON "public"."Chat" USING "btree" ("id");
+
+CREATE UNIQUE INDEX "Feedback_id_key" ON "public"."Feedback" USING "btree" ("id");
+
+ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres";
+
+GRANT USAGE ON SCHEMA "public" TO "postgres";
+GRANT USAGE ON SCHEMA "public" TO "anon";
+GRANT USAGE ON SCHEMA "public" TO "authenticated";
+GRANT USAGE ON SCHEMA "public" TO "service_role";
+
+GRANT ALL ON TABLE "public"."Chat" TO "anon";
+GRANT ALL ON TABLE "public"."Chat" TO "authenticated";
+GRANT ALL ON TABLE "public"."Chat" TO "service_role";
+
+GRANT ALL ON TABLE "public"."Feedback" TO "anon";
+GRANT ALL ON TABLE "public"."Feedback" TO "authenticated";
+GRANT ALL ON TABLE "public"."Feedback" TO "service_role";
+
+GRANT ALL ON TABLE "public"."_prisma_migrations" TO "anon";
+GRANT ALL ON TABLE "public"."_prisma_migrations" TO "authenticated";
+GRANT ALL ON TABLE "public"."_prisma_migrations" TO "service_role";
+
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";
+
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";
+
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
+ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";
+
+RESET ALL;
diff --git a/apps/chat-with-pdf/supabase/migrations/20240818213851_remote_schema.sql b/apps/chat-with-pdf/supabase/migrations/20240818213851_remote_schema.sql
new file mode 100644
index 0000000..73a90dc
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/migrations/20240818213851_remote_schema.sql
@@ -0,0 +1,33 @@
+create policy "Allow all flreew_0"
+on "storage"."objects"
+as permissive
+for select
+to public
+using ((bucket_id = 'documents'::text));
+
+
+create policy "Allow all flreew_1"
+on "storage"."objects"
+as permissive
+for delete
+to public
+using ((bucket_id = 'documents'::text));
+
+
+create policy "Allow all flreew_2"
+on "storage"."objects"
+as permissive
+for insert
+to public
+with check ((bucket_id = 'documents'::text));
+
+
+create policy "Allow all flreew_3"
+on "storage"."objects"
+as permissive
+for update
+to public
+using ((bucket_id = 'documents'::text));
+
+
+
diff --git a/apps/chat-with-pdf/supabase/migrations/20240902160033_remote_schema.sql b/apps/chat-with-pdf/supabase/migrations/20240902160033_remote_schema.sql
new file mode 100644
index 0000000..cf3c4a7
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/migrations/20240902160033_remote_schema.sql
@@ -0,0 +1,288 @@
+create extension vector with schema extensions;
+
+create table "public"."Document" (
+ "id" text not null default gen_random_uuid(),
+ "name" text,
+ "url" text,
+ "chatId" text,
+ "createdAt" timestamp with time zone not null default now(),
+ "updatedAt" timestamp with time zone not null default now(),
+ "metadata" jsonb
+);
+
+
+create table "public"."DocumentSections" (
+ "embedding" vector(768),
+ "chatId" text,
+ "text" text,
+ "pageNumber" integer,
+ "documentId" text,
+ "textChunk" text,
+ "id" uuid not null default extensions.gen_random_uuid()
+);
+
+
+alter table "public"."DocumentSections" enable row level security;
+
+create table "public"."profiles" (
+ "id" uuid not null,
+ "firstName" text,
+ "lastName" text,
+ "email" text,
+ "avatarUrl" text,
+ "role" text
+);
+
+
+alter table "public"."profiles" enable row level security;
+
+alter table "public"."Chat" alter column "createdAt" set default now();
+
+alter table "public"."Chat" alter column "createdAt" set data type timestamp with time zone using "createdAt"::timestamp with time zone;
+
+alter table "public"."Chat" alter column "id" set default gen_random_uuid();
+
+alter table "public"."Chat" alter column "updatedAt" set default now();
+
+alter table "public"."Chat" alter column "updatedAt" set data type timestamp with time zone using "updatedAt"::timestamp with time zone;
+
+CREATE UNIQUE INDEX "DocumentSections_id_key" ON public."DocumentSections" USING btree (id);
+
+CREATE UNIQUE INDEX "DocumentSections_pkey" ON public."DocumentSections" USING btree (id);
+
+CREATE UNIQUE INDEX "Document_chatId_key" ON public."Document" USING btree ("chatId");
+
+CREATE UNIQUE INDEX "Document_id_key" ON public."Document" USING btree (id);
+
+CREATE UNIQUE INDEX "Document_pkey" ON public."Document" USING btree (id);
+
+CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id);
+
+alter table "public"."Document" add constraint "Document_pkey" PRIMARY KEY using index "Document_pkey";
+
+alter table "public"."DocumentSections" add constraint "DocumentSections_pkey" PRIMARY KEY using index "DocumentSections_pkey";
+
+alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey";
+
+alter table "public"."Document" add constraint "Document_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
+
+alter table "public"."Document" validate constraint "Document_chatId_fkey";
+
+alter table "public"."DocumentSections" add constraint "DocumentSections_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"(id) ON DELETE CASCADE not valid;
+
+alter table "public"."DocumentSections" validate constraint "DocumentSections_chatId_fkey";
+
+alter table "public"."DocumentSections" add constraint "DocumentSections_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"(id) ON UPDATE CASCADE ON DELETE SET NULL not valid;
+
+alter table "public"."DocumentSections" validate constraint "DocumentSections_documentId_fkey";
+
+alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
+
+alter table "public"."profiles" validate constraint "profiles_id_fkey";
+
+set check_function_bodies = off;
+
+CREATE OR REPLACE FUNCTION public.handle_new_user()
+ RETURNS trigger
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO ''
+AS $function$
+BEGIN
+ INSERT INTO public.profiles (id, email, role)
+ VALUES (
+ NEW.id,
+ NEW.email,
+ NEW.role
+ );
+ RETURN NEW;
+END;
+$function$
+;
+
+CREATE OR REPLACE FUNCTION public.match_documents(query_embedding vector, match_threshold double precision, document_id uuid)
+ RETURNS SETOF "DocumentSections"
+ LANGUAGE sql
+AS $function$
+ select *
+ from "DocumentSections"
+ where "DocumentSections".embedding <#> query_embedding < -match_threshold
+ and "DocumentSections"."chatId"::text = document_id::text
+ order by "DocumentSections"."pageNumber" asc
+$function$
+;
+
+CREATE OR REPLACE FUNCTION public.match_documents(query_embedding vector, match_threshold double precision, match_count integer)
+ RETURNS SETOF "DocumentSections"
+ LANGUAGE sql
+AS $function$
+ select *
+ from "DocumentSections"
+ where "DocumentSections".embedding <#> query_embedding < -match_threshold
+ order by "DocumentSections"."pageNumber" asc
+ limit least(match_count, 200);
+$function$
+;
+
+CREATE OR REPLACE FUNCTION public.match_documents(query_embedding vector, match_threshold double precision, match_count integer, document_id uuid)
+ RETURNS SETOF "DocumentSections"
+ LANGUAGE sql
+AS $function$
+ select *
+ from "DocumentSections"
+ where "DocumentSections".embedding <#> query_embedding < -match_threshold
+ and "DocumentSections"."chatId"::text = document_id::text
+ order by "DocumentSections"."pageNumber" asc
+ limit least(match_count, 200);
+$function$
+;
+
+grant delete on table "public"."Document" to "anon";
+
+grant insert on table "public"."Document" to "anon";
+
+grant references on table "public"."Document" to "anon";
+
+grant select on table "public"."Document" to "anon";
+
+grant trigger on table "public"."Document" to "anon";
+
+grant truncate on table "public"."Document" to "anon";
+
+grant update on table "public"."Document" to "anon";
+
+grant delete on table "public"."Document" to "authenticated";
+
+grant insert on table "public"."Document" to "authenticated";
+
+grant references on table "public"."Document" to "authenticated";
+
+grant select on table "public"."Document" to "authenticated";
+
+grant trigger on table "public"."Document" to "authenticated";
+
+grant truncate on table "public"."Document" to "authenticated";
+
+grant update on table "public"."Document" to "authenticated";
+
+grant delete on table "public"."Document" to "service_role";
+
+grant insert on table "public"."Document" to "service_role";
+
+grant references on table "public"."Document" to "service_role";
+
+grant select on table "public"."Document" to "service_role";
+
+grant trigger on table "public"."Document" to "service_role";
+
+grant truncate on table "public"."Document" to "service_role";
+
+grant update on table "public"."Document" to "service_role";
+
+grant delete on table "public"."DocumentSections" to "anon";
+
+grant insert on table "public"."DocumentSections" to "anon";
+
+grant references on table "public"."DocumentSections" to "anon";
+
+grant select on table "public"."DocumentSections" to "anon";
+
+grant trigger on table "public"."DocumentSections" to "anon";
+
+grant truncate on table "public"."DocumentSections" to "anon";
+
+grant update on table "public"."DocumentSections" to "anon";
+
+grant delete on table "public"."DocumentSections" to "authenticated";
+
+grant insert on table "public"."DocumentSections" to "authenticated";
+
+grant references on table "public"."DocumentSections" to "authenticated";
+
+grant select on table "public"."DocumentSections" to "authenticated";
+
+grant trigger on table "public"."DocumentSections" to "authenticated";
+
+grant truncate on table "public"."DocumentSections" to "authenticated";
+
+grant update on table "public"."DocumentSections" to "authenticated";
+
+grant delete on table "public"."DocumentSections" to "dashboard_user";
+
+grant insert on table "public"."DocumentSections" to "dashboard_user";
+
+grant references on table "public"."DocumentSections" to "dashboard_user";
+
+grant select on table "public"."DocumentSections" to "dashboard_user";
+
+grant trigger on table "public"."DocumentSections" to "dashboard_user";
+
+grant truncate on table "public"."DocumentSections" to "dashboard_user";
+
+grant update on table "public"."DocumentSections" to "dashboard_user";
+
+grant delete on table "public"."DocumentSections" to "service_role";
+
+grant insert on table "public"."DocumentSections" to "service_role";
+
+grant references on table "public"."DocumentSections" to "service_role";
+
+grant select on table "public"."DocumentSections" to "service_role";
+
+grant trigger on table "public"."DocumentSections" to "service_role";
+
+grant truncate on table "public"."DocumentSections" to "service_role";
+
+grant update on table "public"."DocumentSections" to "service_role";
+
+grant delete on table "public"."profiles" to "anon";
+
+grant insert on table "public"."profiles" to "anon";
+
+grant references on table "public"."profiles" to "anon";
+
+grant select on table "public"."profiles" to "anon";
+
+grant trigger on table "public"."profiles" to "anon";
+
+grant truncate on table "public"."profiles" to "anon";
+
+grant update on table "public"."profiles" to "anon";
+
+grant delete on table "public"."profiles" to "authenticated";
+
+grant insert on table "public"."profiles" to "authenticated";
+
+grant references on table "public"."profiles" to "authenticated";
+
+grant select on table "public"."profiles" to "authenticated";
+
+grant trigger on table "public"."profiles" to "authenticated";
+
+grant truncate on table "public"."profiles" to "authenticated";
+
+grant update on table "public"."profiles" to "authenticated";
+
+grant delete on table "public"."profiles" to "service_role";
+
+grant insert on table "public"."profiles" to "service_role";
+
+grant references on table "public"."profiles" to "service_role";
+
+grant select on table "public"."profiles" to "service_role";
+
+grant trigger on table "public"."profiles" to "service_role";
+
+grant truncate on table "public"."profiles" to "service_role";
+
+grant update on table "public"."profiles" to "service_role";
+
+create policy "enabled for all"
+on "public"."DocumentSections"
+as permissive
+for all
+to public
+using (true);
+
+
+
diff --git a/apps/chat-with-pdf/supabase/migrations/20241102160258_authentication-and-roles.sql b/apps/chat-with-pdf/supabase/migrations/20241102160258_authentication-and-roles.sql
new file mode 100644
index 0000000..80552db
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/migrations/20241102160258_authentication-and-roles.sql
@@ -0,0 +1,154 @@
+create extension if not exists "moddatetime" with schema "extensions";
+
+
+create extension if not exists "vector" with schema "public" version '0.7.0';
+
+drop policy "enabled for all" on "public"."DocumentSections";
+
+alter table "public"."Chat" add column "suggestedQuestions" text[];
+
+alter table "public"."Chat" add column "userId" uuid;
+
+update "public"."Chat" set "userId" = auth.uid();
+
+alter table "public"."Chat" enable row level security;
+
+alter table "public"."Document" add column "userId" uuid;
+
+update "public"."Document" set "userId" = auth.uid();
+
+alter table "public"."Document" enable row level security;
+
+alter table "public"."DocumentSections" add column "userId" uuid;
+
+update "public"."DocumentSections" set "userId" = auth.uid();
+
+alter table "public"."DocumentSections" enable row level security;
+
+alter table "public"."Feedback" add column "userId" uuid;
+
+update "public"."Feedback" set "userId" = auth.uid();
+
+alter table "public"."Feedback" enable row level security;
+
+do $$
+begin
+ if exists (
+ select 1 from pg_roles where rolname = 'supabase_functions_admin'
+ ) then
+ grant delete on table "public"."DocumentSections" to "supabase_functions_admin";
+ grant insert on table "public"."DocumentSections" to "supabase_functions_admin";
+ grant references on table "public"."DocumentSections" to "supabase_functions_admin";
+ grant select on table "public"."DocumentSections" to "supabase_functions_admin";
+ grant trigger on table "public"."DocumentSections" to "supabase_functions_admin";
+ grant truncate on table "public"."DocumentSections" to "supabase_functions_admin";
+ grant update on table "public"."DocumentSections" to "supabase_functions_admin";
+ end if;
+end $$;
+
+create policy "Authenticated users can delete a chat."
+on "public"."Chat"
+as permissive
+for delete
+to authenticated
+using ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can insert a chat."
+on "public"."Chat"
+as permissive
+for insert
+to authenticated
+with check ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can update a chat."
+on "public"."Chat"
+as permissive
+for update
+to authenticated
+using ((( SELECT auth.uid() AS uid) = "userId"))
+with check ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can view their own chats."
+on "public"."Chat"
+as permissive
+for select
+to public
+using ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can delete a chat."
+on "public"."Document"
+as permissive
+for delete
+to authenticated
+using ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can insert a chat."
+on "public"."Document"
+as permissive
+for insert
+to authenticated
+with check ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can update a chat."
+on "public"."Document"
+as permissive
+for update
+to authenticated
+using ((( SELECT auth.uid() AS uid) = "userId"))
+with check ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can view their own chats."
+on "public"."Document"
+as permissive
+for select
+to public
+using ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can delete a chat."
+on "public"."DocumentSections"
+as permissive
+for delete
+to authenticated
+using ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can insert a chat."
+on "public"."DocumentSections"
+as permissive
+for insert
+to authenticated
+with check ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can update a chat."
+on "public"."DocumentSections"
+as permissive
+for update
+to authenticated
+using ((( SELECT auth.uid() AS uid) = "userId"))
+with check ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+create policy "Authenticated users can view their own chats."
+on "public"."DocumentSections"
+as permissive
+for select
+to public
+using ((( SELECT auth.uid() AS uid) = "userId"));
+
+
+CREATE TRIGGER handle_updated_at BEFORE UPDATE ON public."Chat" FOR EACH ROW EXECUTE FUNCTION moddatetime('updatedAt');
+
+CREATE TRIGGER handle_updated_at BEFORE UPDATE ON public."Document" FOR EACH ROW EXECUTE FUNCTION moddatetime('updatedAt');
+
+CREATE TRIGGER handle_updated_at BEFORE UPDATE ON public."DocumentSections" FOR EACH ROW EXECUTE FUNCTION moddatetime('updatedAt');
+
+
diff --git a/apps/chat-with-pdf/supabase/migrations/20241102160420_set-userid-default-uuid.sql b/apps/chat-with-pdf/supabase/migrations/20241102160420_set-userid-default-uuid.sql
new file mode 100644
index 0000000..4010213
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/migrations/20241102160420_set-userid-default-uuid.sql
@@ -0,0 +1,22 @@
+-- add the createdAt and updatedAt columns to the DocumentSections table
+alter table "public"."DocumentSections" add column "createdAt" timestamp with time zone not null default now();
+alter table "public"."DocumentSections" add column "updatedAt" timestamp with time zone not null default now();
+
+-- Update existing null records with a default UUID to replace it with the auth.uid()
+UPDATE "public"."Chat" SET "userId" = '00000000-0000-0000-0000-000000000000' WHERE "userId" IS NULL;
+UPDATE "public"."Document" SET "userId" = '00000000-0000-0000-0000-000000000000' WHERE "userId" IS NULL;
+UPDATE "public"."DocumentSections" SET "userId" = '00000000-0000-0000-0000-000000000000' WHERE "userId" IS NULL;
+UPDATE "public"."Feedback" SET "userId" = '00000000-0000-0000-0000-000000000000' WHERE "userId" IS NULL;
+
+-- Set the default value and make it not null
+alter table "public"."Chat" alter column "userId" set default auth.uid();
+alter table "public"."Chat" alter column "userId" set not null;
+
+alter table "public"."Document" alter column "userId" set default auth.uid();
+alter table "public"."Document" alter column "userId" set not null;
+
+alter table "public"."DocumentSections" alter column "userId" set default auth.uid();
+alter table "public"."DocumentSections" alter column "userId" set not null;
+
+alter table "public"."Feedback" alter column "userId" set default auth.uid();
+alter table "public"."Feedback" alter column "userId" set not null;
\ No newline at end of file
diff --git a/apps/chat-with-pdf/supabase/migrations/20241109155454_auto-remove-document-file.sql b/apps/chat-with-pdf/supabase/migrations/20241109155454_auto-remove-document-file.sql
new file mode 100644
index 0000000..09c5d62
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/migrations/20241109155454_auto-remove-document-file.sql
@@ -0,0 +1,16 @@
+create extension if not exists "vector" with schema "public" version '0.7.0';
+
+set check_function_bodies = off;
+
+CREATE OR REPLACE FUNCTION public.remove_document_file_after_removing_chats()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$BEGIN
+ DELETE FROM storage.objects
+ WHERE bucket_id = 'documents' AND name = OLD.id || '.pdf';
+
+ RETURN OLD;
+END;$function$
+;
+
+
diff --git a/apps/chat-with-pdf/supabase/queries/get-chat.ts b/apps/chat-with-pdf/supabase/queries/get-chat.ts
new file mode 100644
index 0000000..16eb449
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/queries/get-chat.ts
@@ -0,0 +1,70 @@
+import { generateSuggestedQuestions } from "@/app/actions/generate-suggested-questions";
+import { createClient } from "@/lib/supabase/server";
+import { SupabaseClient } from "@supabase/supabase-js";
+import { unstable_cache } from "next/cache";
+
+async function retrieveChat(supabase: SupabaseClient, id: string) {
+ const { data: chatData, error: chatError } = await supabase
+ .from("Chat")
+ .select("*")
+ .eq("id", id)
+ .single();
+
+ if (chatError) {
+ throw chatError;
+ }
+
+ return chatData;
+}
+
+async function generateAndUpdateSuggestedQuestions(
+ supabase: SupabaseClient,
+ id: string,
+) {
+ const result = await generateSuggestedQuestions(id);
+
+ if (!result?.questions) {
+ return null;
+ }
+
+ const { error: updateError } = await supabase
+ .from("Chat")
+ .update({ suggestedQuestions: result.questions })
+ .eq("id", id)
+ .select();
+
+ if (updateError) {
+ throw updateError;
+ }
+
+ return result.questions;
+}
+
+export async function getChat(id: string) {
+ const supabase = createClient();
+ const { data, error: errorOnFetchingSession } = await supabase.auth.getUser();
+
+ if (errorOnFetchingSession) {
+ throw errorOnFetchingSession;
+ }
+
+ const chat = await unstable_cache(
+ (supabase: SupabaseClient) => retrieveChat(supabase, id),
+ [data.user.id || "", id],
+ {
+ revalidate: 60 * 60,
+ tags: ["chat", data.user.id || "", id],
+ },
+ )(supabase);
+
+ console.log({ chat, id });
+ if (!chat?.suggestedQuestions) {
+ const suggestedQuestions = await generateAndUpdateSuggestedQuestions(
+ supabase,
+ id,
+ );
+ return { ...chat, suggestedQuestions };
+ }
+
+ return chat;
+}
diff --git a/apps/chat-with-pdf/supabase/queries/get-chats.ts b/apps/chat-with-pdf/supabase/queries/get-chats.ts
new file mode 100644
index 0000000..d9abe4b
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/queries/get-chats.ts
@@ -0,0 +1,29 @@
+import { SupabaseClient } from "@supabase/supabase-js";
+import { createClient } from "@/lib/supabase/server";
+import { unstable_cache } from "next/cache";
+
+async function retrieveChats(supabase: SupabaseClient) {
+ const { data, error } = await supabase.from("Chat").select("*");
+
+ if (error) {
+ throw error;
+ }
+
+ return data;
+}
+
+export async function getChats() {
+ const supabase = createClient();
+ const { data, error: errorOnFetchingSession } = await supabase.auth.getUser();
+
+ if (errorOnFetchingSession) {
+ throw errorOnFetchingSession;
+ }
+
+ const chats = unstable_cache(retrieveChats, [data.user.id || ""], {
+ revalidate: 60 * 60,
+ tags: ["chats", data.user.id || ""],
+ })(supabase);
+
+ return chats;
+}
diff --git a/apps/chat-with-pdf/supabase/queries/get-documents.ts b/apps/chat-with-pdf/supabase/queries/get-documents.ts
new file mode 100644
index 0000000..b9e079c
--- /dev/null
+++ b/apps/chat-with-pdf/supabase/queries/get-documents.ts
@@ -0,0 +1,31 @@
+import { createClient } from "@/lib/supabase/server";
+import { SupabaseClient } from "@supabase/supabase-js";
+import { unstable_cache } from "next/cache";
+
+async function retrieveDocuments(supabase: SupabaseClient) {
+ const { data: documents, error: errorOnFetchingDocuments } = await supabase
+ .from("Document")
+ .select("*");
+
+ if (errorOnFetchingDocuments) {
+ throw errorOnFetchingDocuments;
+ }
+
+ return documents;
+}
+
+export async function getDocuments() {
+ const supabase = createClient();
+ const { data, error: errorOnFetchingSession } = await supabase.auth.getUser();
+
+ if (errorOnFetchingSession) {
+ throw errorOnFetchingSession;
+ }
+
+ const documents = unstable_cache(retrieveDocuments, [data.user.id || ""], {
+ revalidate: 60 * 60,
+ tags: ["documents", data.user.id || ""],
+ })(supabase);
+
+ return documents;
+}
diff --git a/apps/chat-with-pdf/supabase/seed.sql b/apps/chat-with-pdf/supabase/seed.sql
new file mode 100644
index 0000000..e69de29
diff --git a/apps/chat-with-pdf/tailwind.config.ts b/apps/chat-with-pdf/tailwind.config.ts
index ddcddbf..4e06c8c 100644
--- a/apps/chat-with-pdf/tailwind.config.ts
+++ b/apps/chat-with-pdf/tailwind.config.ts
@@ -14,12 +14,10 @@ const config: TailwindConfig = {
"components/**/*.{ts,tsx}",
),
],
+ presets: [uiTailwindConfig],
theme: {
- ...uiTailwindConfig.theme,
extend: {
- ...uiTailwindConfig.theme.extend,
keyframes: {
- ...uiTailwindConfig.theme.extend.keyframes,
shake: {
"0%, 100%": {
transform: "translateX(0)",
@@ -33,12 +31,11 @@ const config: TailwindConfig = {
},
},
animation: {
- ...uiTailwindConfig.theme.extend.animation,
shake: "shake 0.6s ease-in-out 0.25s 1",
},
},
},
- plugins: [...uiTailwindConfig.plugins],
+ plugins: [],
};
export default config;
diff --git a/apps/chat-with-pdf/tsconfig.json b/apps/chat-with-pdf/tsconfig.json
index 4020a16..b3b4a6f 100644
--- a/apps/chat-with-pdf/tsconfig.json
+++ b/apps/chat-with-pdf/tsconfig.json
@@ -11,6 +11,9 @@
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
+ "@/public/*": [
+ "public/*"
+ ],
"@/components/*": [
"app/components/*"
],
diff --git a/apps/chat-with-pdf/turbo.json b/apps/chat-with-pdf/turbo.json
index 62314d0..d6d9e80 100644
--- a/apps/chat-with-pdf/turbo.json
+++ b/apps/chat-with-pdf/turbo.json
@@ -4,16 +4,10 @@
],
"tasks": {
"build": {
- "dependsOn": [
- "prisma:generate"
- ],
"outputs": [
".next/**",
"!.next/cache/**"
]
- },
- "prisma:generate": {
- "cache": false
}
}
}
\ No newline at end of file
diff --git a/apps/chat-with-pdf/utils/chunked-upsert.ts b/apps/chat-with-pdf/utils/chunked-upsert.ts
deleted file mode 100644
index 7aab622..0000000
--- a/apps/chat-with-pdf/utils/chunked-upsert.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { Index, PineconeRecord } from '@pinecone-database/pinecone';
-import { getPineconeClient } from './pinecone.client';
-
-const sliceIntoChunks = (arr: T[], chunkSize: number) => {
- return Array.from({ length: Math.ceil(arr.length / chunkSize) }, (_, i) =>
- arr.slice(i * chunkSize, (i + 1) * chunkSize)
- );
-};
-
-export async function chunkedUpsert(
- vectors: Array,
- namespace: string,
- chunkSize = 10
-) {
- // Split the vectors into chunks
- const chunks = sliceIntoChunks(vectors, chunkSize);
-
- try {
- // Upsert each chunk of vectors into the index
- await Promise.allSettled(
- chunks.map(async (chunk) => {
- try {
- const pineconeClient = await getPineconeClient(namespace);
- await pineconeClient.upsert(chunk);
- } catch (e) {
- console.log('Error upserting chunk', e);
- }
- })
- );
-
- return true;
- } catch (e) {
- throw new Error(`Error upserting vectors into index: ${e}`);
- }
-};
\ No newline at end of file
diff --git a/apps/chat-with-pdf/utils/context.ts b/apps/chat-with-pdf/utils/context.ts
index b53dbb2..68f3e1e 100644
--- a/apps/chat-with-pdf/utils/context.ts
+++ b/apps/chat-with-pdf/utils/context.ts
@@ -1,65 +1,43 @@
-import { getPineconeClient } from "./pinecone.client";
+import { Tables } from "database.types";
import { getEmbeddings } from "./vector-store";
-
-async function getMatchesFromEmbeddings(
- embeddings: number[],
- documentId: string,
-) {
- const pineconeClient = await getPineconeClient(documentId);
-
- try {
- const queryResult = await pineconeClient.query({
- topK: 5,
- vector: embeddings,
- includeMetadata: true,
- });
-
- return queryResult.matches || [];
- } catch (error) {
- console.log("error querying embeddings", error);
- throw error;
- }
-}
+import { createClient } from "@/lib/supabase/server";
export async function getContext(query: string, documentId: string) {
// User query embeddings
const userQueryEmbeddings = await getEmbeddings(query);
- // Get matches from Pinecone
- const matches = await getMatchesFromEmbeddings(
- userQueryEmbeddings,
- documentId,
- );
-
- const qualifiedMatches = matches.filter((match) => match?.score ?? 0 > 0.7);
-
- const textContent = qualifiedMatches.reduce((acc, match) => {
- const { metadata } = match;
- const pageNumber = metadata?.pageNumber;
- const text = metadata?.text;
- // Return the accumulator in this format:
- // START PAGE 1 BLOCK
- // Text extracted from page 1
-
- if (!acc.includes(`START PAGE ${pageNumber} BLOCK`)) {
- acc += `START PAGE ${pageNumber} BLOCK\n`;
- }
- acc += `${text}\n`;
+ const supabase = createClient();
- return acc;
- }, "");
+ const { data: documentSections, error } = await supabase.rpc(
+ "match_documents",
+ {
+ query_embedding: userQueryEmbeddings,
+ match_threshold: 0.7,
+ match_count: 200,
+ document_id: documentId,
+ },
+ );
- const docs = qualifiedMatches.map((match) => match.metadata?.text);
- const pageNumbers = qualifiedMatches
- .map((match) => match.metadata?.pageNumber)
- .filter((pageNumber, index, self) => self.indexOf(pageNumber) === index);
+ const supabaseTextContent = documentSections.reduce(
+ (acc: string, currentDocSections: Tables<"DocumentSections">) => {
+ const { pageNumber, textChunk } = currentDocSections;
+ // Return the accumulator in this format:
+ // START PAGE 1 BLOCK
+ // Text extracted from page 1
+
+ if (!acc.includes(`START PAGE ${pageNumber} BLOCK`)) {
+ acc += `START PAGE ${pageNumber} BLOCK\n`;
+ }
+ acc += `${textChunk}\n`;
+
+ return acc;
+ },
+ "",
+ );
- const context = {
- text: docs.join("\n").substring(0, 3000),
- pageNumbers,
- };
+ const textContentNormalized = supabaseTextContent.replace(/\n/g, " ");
// Limit the block text to 3000 characters
- return textContent.substring(0, 3000);
+ return textContentNormalized.substring(0, 3000);
}
diff --git a/apps/chat-with-pdf/utils/embed-document.ts b/apps/chat-with-pdf/utils/embed-document.ts
index 1505dae..239fb04 100644
--- a/apps/chat-with-pdf/utils/embed-document.ts
+++ b/apps/chat-with-pdf/utils/embed-document.ts
@@ -38,6 +38,8 @@ export async function embedDocument(doc: Document>) {
const embeddings = await getEmbeddings(doc.pageContent);
const hash = md5(doc.pageContent);
+ const textChunk = doc.pageContent;
+
return {
id: hash,
values: embeddings,
@@ -45,6 +47,7 @@ export async function embedDocument(doc: Document>) {
chatId: doc.metadata.chatId,
text: doc.metadata.text,
pageNumber: doc.metadata.pageNumber,
+ textChunk,
},
};
} catch (error) {}
diff --git a/apps/chat-with-pdf/utils/get-loading-messages.ts b/apps/chat-with-pdf/utils/get-loading-messages.ts
index 4dfa001..0cd4e59 100644
--- a/apps/chat-with-pdf/utils/get-loading-messages.ts
+++ b/apps/chat-with-pdf/utils/get-loading-messages.ts
@@ -1,7 +1,8 @@
import {
loadingPdfFileMessages,
loadingPdfLinkMessages,
-} from "@/components/header/document-switcher/constants/loading-messages";
+} from "@/components/header/document-title/constants/loading-messages";
+import { PostgrestError } from "@supabase/supabase-js";
let currentActiveIndex = -1;
let loadingMessagesCopy = [] as
@@ -16,7 +17,7 @@ export function resetLoadingMessages() {
type GetLoadingMessagesProps = {
isViaLink: boolean;
chatId: string | null;
- errorMessage?: string;
+ errorMessage?: string | PostgrestError;
friendlyError?: string;
};
@@ -34,8 +35,8 @@ export function getLoadingMessages({
: loadingPdfFileMessages
: loadingMessagesCopy;
const loadingMessagesNewCopy = structuredClone(loadingMessagesToClone);
- loadingMessagesNewCopy[currentActiveIndex]!.error =
- errorMessage || friendlyError;
+ loadingMessagesNewCopy[currentActiveIndex]!.error = (errorMessage ||
+ friendlyError) as string;
loadingMessagesNewCopy[currentActiveIndex]!.friendlyError =
friendlyError ||
loadingMessagesNewCopy[currentActiveIndex]!.friendlyError;
diff --git a/apps/chat-with-pdf/utils/oauth-redirect-url.ts b/apps/chat-with-pdf/utils/oauth-redirect-url.ts
new file mode 100644
index 0000000..901a6b3
--- /dev/null
+++ b/apps/chat-with-pdf/utils/oauth-redirect-url.ts
@@ -0,0 +1,15 @@
+import { ReadonlyURLSearchParams } from "next/navigation";
+
+export function getOAuthRedirectUrl(searchParams: ReadonlyURLSearchParams) {
+ const protocol = "https";
+ const host = process.env.SUPABASE_AUTH_REDIRECT_URL || process.env.VERCEL_URL;
+ const path = "/api/auth/callback";
+ const queryParams = new URLSearchParams(searchParams).toString();
+
+ const redirectUrl = new URL(
+ `${path}?${queryParams}`,
+ `${protocol}://${host}`,
+ ).toString();
+
+ return redirectUrl;
+}
diff --git a/apps/chat-with-pdf/utils/supabase/client.ts b/apps/chat-with-pdf/utils/supabase/client.ts
new file mode 100644
index 0000000..2ad2591
--- /dev/null
+++ b/apps/chat-with-pdf/utils/supabase/client.ts
@@ -0,0 +1,8 @@
+import { createBrowserClient } from "@supabase/ssr";
+
+export function createClient() {
+ return createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ );
+}
diff --git a/apps/chat-with-pdf/utils/supabase/middleware.ts b/apps/chat-with-pdf/utils/supabase/middleware.ts
new file mode 100644
index 0000000..450e3a5
--- /dev/null
+++ b/apps/chat-with-pdf/utils/supabase/middleware.ts
@@ -0,0 +1,66 @@
+import { createServerClient } from "@supabase/ssr";
+import { NextResponse, type NextRequest } from "next/server";
+
+export async function updateSession(request: NextRequest) {
+ let supabaseResponse = NextResponse.next({
+ request,
+ });
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll();
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value, options }) =>
+ request.cookies.set(name, value),
+ );
+ supabaseResponse = NextResponse.next({
+ request,
+ });
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options),
+ );
+ },
+ },
+ },
+ );
+
+ // IMPORTANT: Avoid writing any logic between createServerClient and
+ // supabase.auth.getUser(). A simple mistake could make it very hard to debug
+ // issues with users being randomly logged out.
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ if (
+ !user &&
+ !request.nextUrl.pathname.startsWith("/login") &&
+ !request.nextUrl.pathname.startsWith("/signup")
+ ) {
+ // no user, potentially respond by redirecting the user to the login page
+ const url = request.nextUrl.clone();
+ url.pathname = "/login";
+ url.searchParams.set("next", request.nextUrl.pathname.toString());
+ return NextResponse.redirect(url);
+ }
+
+ // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
+ // creating a new response object with NextResponse.next() make sure to:
+ // 1. Pass the request in it, like so:
+ // const myNewResponse = NextResponse.next({ request })
+ // 2. Copy over the cookies, like so:
+ // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
+ // 3. Change the myNewResponse object to fit your needs, but avoid changing
+ // the cookies!
+ // 4. Finally:
+ // return myNewResponse
+ // If this is not done, you may be causing the browser and server to go out
+ // of sync and terminate the user's session prematurely!
+
+ return supabaseResponse;
+}
diff --git a/apps/chat-with-pdf/utils/supabase/server.ts b/apps/chat-with-pdf/utils/supabase/server.ts
new file mode 100644
index 0000000..8e62c13
--- /dev/null
+++ b/apps/chat-with-pdf/utils/supabase/server.ts
@@ -0,0 +1,29 @@
+import { createServerClient } from "@supabase/ssr";
+import { cookies } from "next/headers";
+
+export function createClient() {
+ const cookieStore = cookies();
+
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll();
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) =>
+ cookieStore.set(name, value, options),
+ );
+ } catch {
+ // The `setAll` method was called from a Server Component.
+ // This can be ignored if you have middleware refreshing
+ // user sessions.
+ }
+ },
+ },
+ },
+ );
+}
diff --git a/apps/chat-with-pdf/utils/vector-store.ts b/apps/chat-with-pdf/utils/vector-store.ts
index 777763a..11a19cd 100644
--- a/apps/chat-with-pdf/utils/vector-store.ts
+++ b/apps/chat-with-pdf/utils/vector-store.ts
@@ -1,55 +1,22 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
-import { openai } from "@ai-sdk/openai";
-import { OpenAIEmbeddings } from "@langchain/openai";
-import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
-import { PineconeStore } from "@langchain/pinecone";
-import { Index } from "@pinecone-database/pinecone";
-import { embed } from "ai";
import { TaskType } from "@google/generative-ai";
-
-// Instantiate a new Pinecone client, which will automatically read the
-// env vars: PINECONE_API_KEY and PINECONE_ENVIRONMENT which come from
-// the Pinecone dashboard at https://app.pinecone.io
-
-
-
-export async function embedAndStoreDocs(pineconeIndex: Index, docs: any) {
- // const embeddings = new OpenAIEmbeddings()
- const embeddings = new GoogleGenerativeAIEmbeddings({
- model: 'embedding-001',
- taskType: TaskType.RETRIEVAL_DOCUMENT,
- title: 'Document title',
- apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
- })
-
- await PineconeStore.fromDocuments(docs, embeddings, {
- pineconeIndex,
- maxConcurrency: 5, // Maximum number of batch requests to allow at once. Each batch is 1000 vectors.
- });
-}
+import { GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
export async function getEmbeddings(value: string) {
- try {
- // Embed the text using the Google Generative AI API
- const googleGenerativeAIEmbeddings = new GoogleGenerativeAIEmbeddings({
- model: 'embedding-001',
- taskType: TaskType.RETRIEVAL_DOCUMENT,
- title: 'Document title',
- apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
- })
-
- const embedding = await googleGenerativeAIEmbeddings.embedQuery(value)
-
- // Embed the text using the OpenAI API
-
- // const { embedding } = await embed({
- // model: openai.embedding('text-embedding-ada-002'),
- // value: value.replace(/\n/g, ' '),
- // })
-
- return embedding
- } catch (error) {
- console.log('Error in getEmbeddings', error)
- throw new Error(`Error in getEmbeddings: ${error}`)
- }
-}
\ No newline at end of file
+ try {
+ // Embed the text using the Google Generative AI API
+ const googleGenerativeAIEmbeddings = new GoogleGenerativeAIEmbeddings({
+ model: "embedding-001",
+ taskType: TaskType.RETRIEVAL_DOCUMENT,
+ title: "Document title",
+ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
+ });
+
+ const embedding = await googleGenerativeAIEmbeddings.embedQuery(value);
+
+ return embedding;
+ } catch (error) {
+ console.log("Error in getEmbeddings", error);
+ throw new Error(`Error in getEmbeddings: ${error}`);
+ }
+}
diff --git a/apps/landing/app/globals.css b/apps/landing/app/globals.css
index bd6213e..b5c61c9 100644
--- a/apps/landing/app/globals.css
+++ b/apps/landing/app/globals.css
@@ -1,3 +1,3 @@
@tailwind base;
@tailwind components;
-@tailwind utilities;
\ No newline at end of file
+@tailwind utilities;
diff --git a/apps/landing/package.json b/apps/landing/package.json
index aed8ff2..bae2a68 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -55,7 +55,7 @@
"eslint-config-next": "14.2.5",
"postcss": "8.4.31",
"prisma": "4.16.2",
- "tailwindcss": "3.3.3",
+ "tailwindcss": "3.4.14",
"tailwindcss-animate": "1.0.7"
}
}
\ No newline at end of file
diff --git a/bun.lockb b/bun.lockb
index 50af2a4..fd38789 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 7ad94fd..ad91357 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,10 @@
"scripts": {
"build": "turbo build",
"dev": "turbo dev --env-mode=loose",
+ "dev:landing": "turbo dev --filter=@makify/landing --env-mode=loose",
+ "dev:chat-with-pdf": "turbo dev --filter=@makify/chat-with-pdf --env-mode=loose",
+ "build:landing": "turbo build --filter=@makify/landing --env-mode=loose",
+ "build:chat-with-pdf": "turbo build --filter=@makify/chat-with-pdf --env-mode=loose",
"lint": "turbo lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"commit": "sui-mono commit"
@@ -15,7 +19,6 @@
"eslint-config-next": "14.2.5",
"prettier": "3.2.5",
"prettier-plugin-tailwindcss": "0.5.14",
- "tailwindcss": "3.4.3",
"tsx": "4.16.0",
"turbo": "2.0.12"
},
diff --git a/packages/ui/components.json b/packages/ui/components.json
index 3b734b5..69d558a 100644
--- a/packages/ui/components.json
+++ b/packages/ui/components.json
@@ -4,7 +4,7 @@
"rsc": true,
"tsx": true,
"tailwind": {
- "config": "tailwind.config.js",
+ "config": "tailwind.config.ts",
"css": "globals.css",
"baseColor": "slate",
"cssVariables": true,
diff --git a/packages/ui/components/button.tsx b/packages/ui/components/button.tsx
index 53e43d5..33b5ae1 100644
--- a/packages/ui/components/button.tsx
+++ b/packages/ui/components/button.tsx
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@makify/ui/lib/utils";
const buttonVariants = cva(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
diff --git a/packages/ui/components/collapsible.tsx b/packages/ui/components/collapsible.tsx
new file mode 100644
index 0000000..9fa4894
--- /dev/null
+++ b/packages/ui/components/collapsible.tsx
@@ -0,0 +1,11 @@
+"use client"
+
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/packages/ui/components/drawer.tsx b/packages/ui/components/drawer.tsx
new file mode 100644
index 0000000..855ba81
--- /dev/null
+++ b/packages/ui/components/drawer.tsx
@@ -0,0 +1,118 @@
+"use client"
+
+import * as React from "react"
+import { Drawer as DrawerPrimitive } from "vaul"
+
+import { cn } from "@makify/ui/lib/utils"
+
+const Drawer = ({
+ shouldScaleBackground = true,
+ ...props
+}: React.ComponentProps) => (
+
+)
+Drawer.displayName = "Drawer"
+
+const DrawerTrigger = DrawerPrimitive.Trigger
+
+const DrawerPortal = DrawerPrimitive.Portal
+
+const DrawerClose = DrawerPrimitive.Close
+
+const DrawerOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
+
+const DrawerContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+))
+DrawerContent.displayName = "DrawerContent"
+
+const DrawerHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DrawerHeader.displayName = "DrawerHeader"
+
+const DrawerFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DrawerFooter.displayName = "DrawerFooter"
+
+const DrawerTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName
+
+const DrawerDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+}
diff --git a/packages/ui/components/dropdown-menu.tsx b/packages/ui/components/dropdown-menu.tsx
index 73b22c6..b4e9020 100644
--- a/packages/ui/components/dropdown-menu.tsx
+++ b/packages/ui/components/dropdown-menu.tsx
@@ -1,48 +1,48 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
-} from "@radix-ui/react-icons"
+} from "@radix-ui/react-icons";
-import { cn } from "@makify/ui/lib/utils"
+import { cn } from "@makify/ui/lib/utils";
-const DropdownMenu = DropdownMenuPrimitive.Root
+const DropdownMenu = DropdownMenuPrimitive.Root;
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
-const DropdownMenuGroup = DropdownMenuPrimitive.Group
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
-const DropdownMenuSub = DropdownMenuPrimitive.Sub
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
{children}
-))
+));
DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName
+ DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
@@ -51,14 +51,14 @@ const DropdownMenuSubContent = React.forwardRef<
-))
+));
DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName
+ DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
@@ -69,33 +69,33 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
+ "bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 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
+ className,
)}
{...props}
/>
-))
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
@@ -104,8 +104,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children}
-))
+));
DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName
+ DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
@@ -128,8 +128,8 @@ const DropdownMenuRadioItem = React.forwardRef<
@@ -140,13 +140,13 @@ const DropdownMenuRadioItem = React.forwardRef<
{children}
-))
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
@@ -167,11 +167,11 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
-))
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
@@ -182,9 +182,9 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
- )
-}
-DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
@@ -202,4 +202,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
-}
+};
diff --git a/packages/ui/components/form.tsx b/packages/ui/components/form.tsx
index e3d8c89..c550b43 100644
--- a/packages/ui/components/form.tsx
+++ b/packages/ui/components/form.tsx
@@ -109,7 +109,6 @@ const FormControl = React.forwardRef<
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
-
return (
);
});
diff --git a/packages/ui/components/index.ts b/packages/ui/components/index.ts
index f0bab60..36377cf 100644
--- a/packages/ui/components/index.ts
+++ b/packages/ui/components/index.ts
@@ -3,20 +3,26 @@ export * from "./breadcrumb";
export * from "./button";
export * from "./card";
export * from "./carousel";
+export * from "./collapsible";
export * from "./command";
export * from "./dialog";
export * from "./dropdown-menu";
+export * from "./drawer";
export * from "./navigation-menu";
export * from "./input";
export * from "./form";
export * from "./avatar";
export * from "./label";
export * from "./skeleton";
+export * from "./input-otp";
export * from "./pagination";
export * from "./popover";
export * from "./resizable";
export * from "./separator";
+export * from "./scroll-area";
+export * from "./sheet";
export * from "./select";
+export * from "./sidebar";
export * from "./switch";
export * from "./tabs";
export * from "./textarea";
diff --git a/packages/ui/components/input-otp.tsx b/packages/ui/components/input-otp.tsx
new file mode 100644
index 0000000..c96c4e2
--- /dev/null
+++ b/packages/ui/components/input-otp.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import * as React from "react";
+import { DashIcon } from "@radix-ui/react-icons";
+import { OTPInput, OTPInputContext } from "input-otp";
+
+import { cn } from "@makify/ui/lib/utils";
+
+const InputOTP = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, containerClassName, ...props }, ref) => (
+
+));
+InputOTP.displayName = "InputOTP";
+
+const InputOTPGroup = React.forwardRef<
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div">
+>(({ className, ...props }, ref) => (
+
+));
+InputOTPGroup.displayName = "InputOTPGroup";
+
+const InputOTPSlot = React.forwardRef<
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
+>(({ index, className, ...props }, ref) => {
+ const inputOTPContext = React.useContext(OTPInputContext);
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] || {};
+
+ return (
+
+ {char}
+ {hasFakeCaret && (
+
+ )}
+
+ );
+});
+InputOTPSlot.displayName = "InputOTPSlot";
+
+const InputOTPSeparator = React.forwardRef<
+ React.ElementRef<"div">,
+ React.ComponentPropsWithoutRef<"div">
+>(({ ...props }, ref) => (
+
+
+
+));
+InputOTPSeparator.displayName = "InputOTPSeparator";
+
+export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
diff --git a/packages/ui/components/input.tsx b/packages/ui/components/input.tsx
index f3dca0e..21b9db7 100644
--- a/packages/ui/components/input.tsx
+++ b/packages/ui/components/input.tsx
@@ -11,9 +11,9 @@ const Input = React.forwardRef(
,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/packages/ui/components/sheet.tsx b/packages/ui/components/sheet.tsx
new file mode 100644
index 0000000..1521bfa
--- /dev/null
+++ b/packages/ui/components/sheet.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import * as React from "react";
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { Cross2Icon } from "@radix-ui/react-icons";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@makify/ui/lib/utils";
+
+const Sheet = SheetPrimitive.Root;
+
+const SheetTrigger = SheetPrimitive.Trigger;
+
+const SheetClose = SheetPrimitive.Close;
+
+const SheetPortal = SheetPrimitive.Portal;
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ },
+);
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {
+ hideCloseIcon?: boolean;
+}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(
+ (
+ { side = "right", className, hideCloseIcon = false, children, ...props },
+ ref,
+ ) => (
+
+
+
+ {!hideCloseIcon && (
+
+
+ Close
+
+ )}
+ {children}
+
+
+ ),
+);
+SheetContent.displayName = SheetPrimitive.Content.displayName;
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetHeader.displayName = "SheetHeader";
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+SheetFooter.displayName = "SheetFooter";
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetTitle.displayName = SheetPrimitive.Title.displayName;
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+};
diff --git a/packages/ui/components/sidebar.tsx b/packages/ui/components/sidebar.tsx
new file mode 100644
index 0000000..90064c3
--- /dev/null
+++ b/packages/ui/components/sidebar.tsx
@@ -0,0 +1,777 @@
+"use client";
+
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { VariantProps, cva } from "class-variance-authority";
+import { PanelLeft } from "lucide-react";
+
+import { useIsMobile } from "../hooks/use-mobile";
+import { cn } from "@makify/ui/lib/utils";
+import { Button } from "@makify/ui/components/button";
+import { Input } from "@makify/ui/components/input";
+import { Separator } from "@makify/ui/components/separator";
+import { Sheet, SheetContent } from "@makify/ui/components/sheet";
+import { Skeleton } from "@makify/ui/components/skeleton";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@makify/ui/components/tooltip";
+
+const SIDEBAR_COOKIE_NAME = "sidebar:state";
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH_MOBILE = "18rem";
+const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_KEYBOARD_SHORTCUT = "b";
+
+type SidebarContext = {
+ state: "expanded" | "collapsed";
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
+
+const SidebarContext = React.createContext(null);
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext);
+ if (!context) {
+ throw new Error("useSidebar must be used within a Sidebar.");
+ }
+
+ return context;
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ if (setOpenProp) {
+ return setOpenProp?.(
+ typeof value === "function" ? value(open) : value,
+ );
+ }
+
+ _setOpen(value);
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+ },
+ [setOpenProp, open],
+ );
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((open) => !open)
+ : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault();
+ toggleSidebar();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed";
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ ],
+ );
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ },
+);
+SidebarProvider.displayName = "SidebarProvider";
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
+ containerClassName?: string;
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ containerClassName,
+ children,
+ ...props
+ },
+ ref,
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ );
+ }
+
+ if (isMobile) {
+ return (
+
+ button]:hidden",
+ containerClassName,
+ )}
+ style={
+ {
+ "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
+ } as React.CSSProperties
+ }
+ side={side}
+ >
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ );
+ },
+);
+Sidebar.displayName = "Sidebar";
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+});
+SidebarTrigger.displayName = "SidebarTrigger";
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar();
+
+ return (
+
+ );
+});
+SidebarRail.displayName = "SidebarRail";
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInset.displayName = "SidebarInset";
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarInput.displayName = "SidebarInput";
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarHeader.displayName = "SidebarHeader";
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarFooter.displayName = "SidebarFooter";
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarSeparator.displayName = "SidebarSeparator";
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarContent.displayName = "SidebarContent";
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+SidebarGroup.displayName = "SidebarGroup";
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupLabel.displayName = "SidebarGroupLabel";
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarGroupAction.displayName = "SidebarGroupAction";
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarGroupContent.displayName = "SidebarGroupContent";
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenu.displayName = "SidebarMenu";
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuItem.displayName = "SidebarMenuItem";
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ const Comp = asChild ? Slot : "button";
+ const { isMobile, state } = useSidebar();
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ };
+ }
+
+ return (
+
+ {button}
+
+
+ );
+ },
+);
+SidebarMenuButton.displayName = "SidebarMenuButton";
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean;
+ showOnHover?: boolean;
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuAction.displayName = "SidebarMenuAction";
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuBadge.displayName = "SidebarMenuBadge";
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean;
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ );
+});
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+));
+SidebarMenuSub.displayName = "SidebarMenuSub";
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => );
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className,
+ )}
+ {...props}
+ />
+ );
+});
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+};
diff --git a/packages/ui/globals.css b/packages/ui/globals.css
index 155b00c..912e19b 100644
--- a/packages/ui/globals.css
+++ b/packages/ui/globals.css
@@ -29,6 +29,14 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
+ --sidebar-background: 0 0% 100%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 222 88% 58%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -56,6 +64,14 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
+ --sidebar-background: 222 50% 10%;
+ --sidebar-foreground: 210 40% 98%;
+ --sidebar-primary: 222 88% 58%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 217.2 32.6% 17.5%;
+ --sidebar-accent-foreground: 210 40% 98%;
+ --sidebar-border: 217.2 32.6% 17.5%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
}
diff --git a/packages/ui/hooks/use-mobile.tsx b/packages/ui/hooks/use-mobile.tsx
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/packages/ui/hooks/use-mobile.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 1da8975..8cc8e0c 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -24,33 +24,39 @@
"@types/react-dom": "18.2.19",
"eslint": "8.57.0",
"react": "18.3.1",
+ "tailwindcss": "3.4.14",
"typescript": "5.3.3"
},
"dependencies": {
"@hookform/resolvers": "3.9.0",
"@radix-ui/react-avatar": "1.0.4",
- "@radix-ui/react-dialog": "1.1.1",
+ "@radix-ui/react-collapsible": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "2.1.1",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-label": "2.1.0",
"@radix-ui/react-popover": "1.1.1",
+ "@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "2.1.1",
- "@radix-ui/react-separator": "1.1.0",
+ "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tabs": "1.1.0",
"@radix-ui/react-toast": "1.2.1",
"@radix-ui/react-toggle": "1.1.0",
"@radix-ui/react-toggle-group": "1.1.0",
- "@radix-ui/react-tooltip": "1.1.2",
- "class-variance-authority": "0.7.0",
+ "@radix-ui/react-tooltip": "^1.1.3",
+ "class-variance-authority": "^0.7.0",
"clsx": "2.1.0",
"cmdk": "1.0.0",
"embla-carousel-react": "^8.2.0",
+ "input-otp": "1.2.5",
+ "lucide-react": "^0.453.0",
"react-hook-form": "7.52.1",
"react-resizable-panels": "2.0.22",
"tailwind-merge": "2.2.2",
"tailwindcss-animate": "1.0.7",
+ "vaul": "1.1.1",
"zod": "3.23.8"
}
}
\ No newline at end of file
diff --git a/packages/ui/tailwind.config.ts b/packages/ui/tailwind.config.ts
index 41bdcac..c45f3f4 100644
--- a/packages/ui/tailwind.config.ts
+++ b/packages/ui/tailwind.config.ts
@@ -8,7 +8,6 @@ const config = {
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
- "../../packages/ui/src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
@@ -54,6 +53,16 @@ const config = {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
+ sidebar: {
+ DEFAULT: "hsl(var(--sidebar-background))",
+ foreground: "hsl(var(--sidebar-foreground))",
+ primary: "hsl(var(--sidebar-primary))",
+ "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
+ accent: "hsl(var(--sidebar-accent))",
+ "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
+ border: "hsl(var(--sidebar-border))",
+ ring: "hsl(var(--sidebar-ring))",
+ },
},
borderRadius: {
lg: "var(--radius)",
@@ -62,12 +71,20 @@ const config = {
},
keyframes: {
"accordion-down": {
- from: { height: "0" },
- to: { height: "var(--radix-accordion-content-height)" },
+ from: {
+ height: "0",
+ },
+ to: {
+ height: "var(--radix-accordion-content-height)",
+ },
},
"accordion-up": {
- from: { height: "var(--radix-accordion-content-height)" },
- to: { height: "0" },
+ from: {
+ height: "var(--radix-accordion-content-height)",
+ },
+ to: {
+ height: "0",
+ },
},
},
animation: {
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
index 6047079..54162e8 100644
--- a/packages/ui/tsconfig.json
+++ b/packages/ui/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "@makify/typescript-config/react-library.json",
"compilerOptions": {
+ "jsx": "react-jsx",
"outDir": "dist",
"baseUrl": ".",
"paths": {
diff --git a/turbo.json b/turbo.json
index 759c2b4..bca99fe 100644
--- a/turbo.json
+++ b/turbo.json
@@ -28,6 +28,5 @@
"cache": false,
"persistent": true
}
- },
- "ui": "tui"
+ }
}
\ No newline at end of file