diff --git a/.github/workflows/supabase-tests-pull-request.yml b/.github/workflows/supabase-tests-pull-request.yml new file mode 100644 index 0000000..ce1f3a0 --- /dev/null +++ b/.github/workflows/supabase-tests-pull-request.yml @@ -0,0 +1,55 @@ +name: Supabase checks + +on: + push: + paths: + # Only run the workflow when changes are made to the migrations directory + - "apps/chat-with-pdf/supabase/migrations/**" + branches: + - "feature/**" + workflow_dispatch: + +jobs: + test: + name: Supabase tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: supabase/setup-cli@v1 + with: + version: latest + + - name: Init Supabase local development setup + run: supabase init + + - name: Start Supabase local development setup + run: supabase start + + - name: Run Supabase lint locally + run: supabase db lint + + - name: Run Supabase tests locally + run: supabase test db + push: + name: Push to Supabase + needs: test + runs-on: ubuntu-latest + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_PROJECT_ID: ${{ github.base_ref == 'main' && secrets.SUPABASE_PROD_PROJECT_ID || secrets.SUPABASE_STAGING_PROJECT_ID }} + SUPABASE_DB_PASSWORD: ${{ github.base_ref == 'main' && secrets.SUPABASE_PROD_DB_PASSWORD || secrets.SUPABASE_STAGING_DB_PASSWORD }} + SUPABASE_AUTH_GITHUB_CLIENT_ID: ${{ secrets.SUPABASE_AUTH_GITHUB_CLIENT_ID }} + SUPABASE_AUTH_GITHUB_SECRET: ${{ secrets.SUPABASE_AUTH_GITHUB_SECRET }} + + steps: + - uses: actions/checkout@v4 + - uses: supabase/setup-cli@v1 + with: + version: latest + + - run: supabase link --project-ref $SUPABASE_PROJECT_ID --debug + working-directory: apps/chat-with-pdf + - run: supabase db push + working-directory: apps/chat-with-pdf diff --git a/.github/workflows/supabase-update-db.yml b/.github/workflows/supabase-update-db.yml new file mode 100644 index 0000000..64c0562 --- /dev/null +++ b/.github/workflows/supabase-update-db.yml @@ -0,0 +1,32 @@ +name: Supabase update + +on: + push: + paths: + - "apps/chat-with-pdf/**" + branches: + - "develop" + - "main" + workflow_dispatch: + +jobs: + update: + name: Update Supabase db + runs-on: ubuntu-latest + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + SUPABASE_PROJECT_ID: ${{ github.base_ref == 'main' && secrets.SUPABASE_PROD_PROJECT_ID || secrets.SUPABASE_STAGING_PROJECT_ID }} + SUPABASE_DB_PASSWORD: ${{ github.base_ref == 'main' && secrets.SUPABASE_PROD_DB_PASSWORD || secrets.SUPABASE_STAGING_DB_PASSWORD }} + SUPABASE_AUTH_GITHUB_CLIENT_ID: ${{ secrets.SUPABASE_AUTH_GITHUB_CLIENT_ID }} + SUPABASE_AUTH_GITHUB_SECRET: ${{ secrets.SUPABASE_AUTH_GITHUB_SECRET }} + + steps: + - uses: actions/checkout@v4 + - uses: supabase/setup-cli@v1 + with: + version: latest + + - run: supabase link --project-ref $SUPABASE_PROJECT_ID --debug + working-directory: apps/chat-with-pdf + - run: supabase db push + working-directory: apps/chat-with-pdf diff --git a/.gitignore b/.gitignore index 96fab4f..9f746aa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,7 @@ node_modules # Local env files .env -.env.local -.env.development.local -.env.test.local -.env.production.local +.env** # Testing coverage diff --git a/apps/chat-with-pdf/app/(authentication)/auth-failed/page.tsx b/apps/chat-with-pdf/app/(authentication)/auth-failed/page.tsx new file mode 100644 index 0000000..259c32f --- /dev/null +++ b/apps/chat-with-pdf/app/(authentication)/auth-failed/page.tsx @@ -0,0 +1,40 @@ +import { ArrowRightIcon } from "lucide-react"; +import Link from "next/link"; + +export default function AuthFailed() { + return ( +
+
+
+ + + +
+

+ Authentication Failed +

+

+ There was a problem authenticating your account. Please try signing in + again. +

+ + Return to Login + +
+
+ ); +} diff --git a/apps/chat-with-pdf/app/(authentication)/layout.tsx b/apps/chat-with-pdf/app/(authentication)/layout.tsx new file mode 100644 index 0000000..9cbda6c --- /dev/null +++ b/apps/chat-with-pdf/app/(authentication)/layout.tsx @@ -0,0 +1,11 @@ +export default function AuthenticationLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
{children}
+
+ ); +} diff --git a/apps/chat-with-pdf/app/(authentication)/login/page.tsx b/apps/chat-with-pdf/app/(authentication)/login/page.tsx new file mode 100644 index 0000000..5d0cc62 --- /dev/null +++ b/apps/chat-with-pdf/app/(authentication)/login/page.tsx @@ -0,0 +1,10 @@ +import { LoginContainer } from "@/components/pages-containers/login/login-container"; +import { Suspense } from "react"; + +export default function LoginPage() { + return ( + + + + ); +} diff --git a/apps/chat-with-pdf/app/(authentication)/signup/page.tsx b/apps/chat-with-pdf/app/(authentication)/signup/page.tsx new file mode 100644 index 0000000..633593c --- /dev/null +++ b/apps/chat-with-pdf/app/(authentication)/signup/page.tsx @@ -0,0 +1,10 @@ +import { SignUpContainer } from "@/components/pages-containers/signup/sign-up-container"; +import { Suspense } from "react"; + +export default function LoginPage() { + return ( + + + + ); +} diff --git a/apps/chat-with-pdf/app/(default)/chat/[documentId]/layout.tsx b/apps/chat-with-pdf/app/(default)/chat/[documentId]/layout.tsx new file mode 100644 index 0000000..bf47a99 --- /dev/null +++ b/apps/chat-with-pdf/app/(default)/chat/[documentId]/layout.tsx @@ -0,0 +1,7 @@ +export default async function ChatLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/apps/chat-with-pdf/app/chat/[documentId]/loading.tsx b/apps/chat-with-pdf/app/(default)/chat/[documentId]/loading.tsx similarity index 100% rename from apps/chat-with-pdf/app/chat/[documentId]/loading.tsx rename to apps/chat-with-pdf/app/(default)/chat/[documentId]/loading.tsx diff --git a/apps/chat-with-pdf/app/chat/[documentId]/page.tsx b/apps/chat-with-pdf/app/(default)/chat/[documentId]/page.tsx similarity index 55% rename from apps/chat-with-pdf/app/chat/[documentId]/page.tsx rename to apps/chat-with-pdf/app/(default)/chat/[documentId]/page.tsx index 0d9c4b0..500b38e 100644 --- a/apps/chat-with-pdf/app/chat/[documentId]/page.tsx +++ b/apps/chat-with-pdf/app/(default)/chat/[documentId]/page.tsx @@ -1,24 +1,24 @@ -import { getCachedChatMessages } from "@/app/actions/get-chat-messages"; import { ChatIdContainer } from "@/components/pages-containers/chat-id-container"; -import { Metadata, ResolvingMetadata } from "next"; +import { Metadata } from "next"; import { redirect } from "next/navigation"; +import { getChat } from "supabase/queries/get-chat"; +import { getDocumentByChatId } from "@/app/actions/get-document-by-chat-id"; type Props = { params: { documentId: string; }; }; - export async function generateMetadata({ params }: Props): Promise { - const chatData = await getCachedChatMessages(params.documentId); + const document = await getDocumentByChatId(params.documentId); return { - title: `Chat - ${(chatData?.documentMetadata as Record)?.title}`, + title: `Chat - ${document?.name || "Untitled"}`, }; } export default async function Page({ params }: Props) { - const chatData = await getCachedChatMessages(params.documentId); + const chatData = await getChat(params.documentId); if (!chatData) redirect("/chat"); diff --git a/apps/chat-with-pdf/app/(default)/chat/layout.tsx b/apps/chat-with-pdf/app/(default)/chat/layout.tsx new file mode 100644 index 0000000..1e61671 --- /dev/null +++ b/apps/chat-with-pdf/app/(default)/chat/layout.tsx @@ -0,0 +1,13 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Chats", +}; + +export default async function ChatLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/apps/chat-with-pdf/app/chat/(default)/loading.tsx b/apps/chat-with-pdf/app/(default)/chat/loading.tsx similarity index 100% rename from apps/chat-with-pdf/app/chat/(default)/loading.tsx rename to apps/chat-with-pdf/app/(default)/chat/loading.tsx diff --git a/apps/chat-with-pdf/app/(default)/chat/page.tsx b/apps/chat-with-pdf/app/(default)/chat/page.tsx new file mode 100644 index 0000000..18b5047 --- /dev/null +++ b/apps/chat-with-pdf/app/(default)/chat/page.tsx @@ -0,0 +1,12 @@ +import { ChatsContainer } from "@/components/pages-containers/chats-container"; +import { getChats } from "supabase/queries/get-chats"; +import { getDocuments } from "supabase/queries/get-documents"; + +export const dynamic = "force-dynamic"; + +export default async function Page() { + const chats = await getChats(); + const documents = await getDocuments(); + + return ; +} diff --git a/apps/chat-with-pdf/app/(default)/layout.tsx b/apps/chat-with-pdf/app/(default)/layout.tsx new file mode 100644 index 0000000..880ade3 --- /dev/null +++ b/apps/chat-with-pdf/app/(default)/layout.tsx @@ -0,0 +1,42 @@ +import { Header } from "@/components/header/header"; +import { AppSidebar } from "@/components/sidebar/app-sidebar"; +import { createClient } from "@/lib/supabase/server"; +import { SidebarProvider } from "@makify/ui"; +import { cn } from "@makify/ui/lib/utils"; +import { User } from "@supabase/supabase-js"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Chats", +}; + +export default async function DefaultLayout({ + children, +}: { + children: React.ReactNode; +}) { + const supabase = createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + return ( + + +
+
+
{children}
+
+
+ ); +} diff --git a/apps/chat-with-pdf/app/actions/auth.ts b/apps/chat-with-pdf/app/actions/auth.ts new file mode 100644 index 0000000..a1db0b6 --- /dev/null +++ b/apps/chat-with-pdf/app/actions/auth.ts @@ -0,0 +1,18 @@ +"use server"; + +import { createClient } from "@/lib/supabase/server"; + +export const verifyOtp = async (data: { + email: string; + otp: string; + type: string; +}) => { + const supabase = createClient(); + + const res = await supabase.auth.verifyOtp({ + email: data.email, + token: data.otp, + type: "email", + }); + return JSON.stringify(res); +}; diff --git a/apps/chat-with-pdf/app/actions/create-new-chat.ts b/apps/chat-with-pdf/app/actions/create-new-chat.ts deleted file mode 100644 index 23aca4f..0000000 --- a/apps/chat-with-pdf/app/actions/create-new-chat.ts +++ /dev/null @@ -1,54 +0,0 @@ -"use server"; - -import { chunkedUpsert } from "@/lib/chunked-upsert"; -import { embedDocument, prepareDocument } from "@/lib/embed-document"; -import { prisma } from "@/lib/prisma"; -import { WebPDFLoader } from "@langchain/community/document_loaders/web/pdf"; -import { PineconeRecord } from "@pinecone-database/pinecone"; -import { Chat } from "@prisma/client"; -import { redirect } from "next/navigation"; - -export async function createNewChat(formData: FormData) { - let chat: Chat; - - try { - // Get the document url from the form data - const documentUrl = formData.get("document-url") as string; - - // Create a new chat in the database - console.log("Creating new chat in the database"); - chat = await prisma.chat.create({ - data: { - documentUrl: documentUrl, - }, - }); - - // Load the PDF - console.log("Loading PDF"); - const response = await fetch(documentUrl); - const blob = await response.blob(); - const loader = new WebPDFLoader(blob); - const pages = await loader.load(); - - // Split it into chunks - console.log("Splitting documents"); - const documents = await Promise.all( - pages.map((page) => prepareDocument(page, chat.id)), - ); - - // Vectorize the documents - console.log("Embedding documents"); - const vectors = (await Promise.all( - documents.flat().map(embedDocument), - )) as PineconeRecord[]; - - // Store the vectors in Pinecone - console.log("Storing vectors in Pinecone"); - await chunkedUpsert(vectors, chat.id); - } catch (error) { - console.log("Error creating new chat: ", error); - throw new Error(`Error creating new chat ${error}`); - } - - redirect(`/chat/${chat.id}`); -} diff --git a/apps/chat-with-pdf/app/actions/delete-chat.ts b/apps/chat-with-pdf/app/actions/delete-chat.ts index 04a9462..83ceded 100644 --- a/apps/chat-with-pdf/app/actions/delete-chat.ts +++ b/apps/chat-with-pdf/app/actions/delete-chat.ts @@ -1,49 +1,35 @@ "use server"; -import { getPineconeClient } from "@/lib/pinecone.client"; -import { prisma } from "@/lib/prisma"; -import { supabase } from "@/lib/supabase"; -import { Chat } from "@prisma/client"; -import { revalidatePath } from "next/cache"; +import { createClient } from "@/lib/supabase/server"; +import { Tables } from "database.types"; +import { revalidatePath, revalidateTag } from "next/cache"; import { redirect } from "next/navigation"; -async function deleteChat(chat: Chat) { - return prisma.chat.delete({ - where: { - id: chat.id, - }, - }); -} - -async function deleteDocumentFile(chat: Chat) { - if (chat.documentUrl?.includes(process.env.SUPABASE_URL as string)) { - return supabase.storage.from("documents").remove([`${chat.id}.pdf`]); - } - return null; -} - -async function deleteNamespace(chat: Chat) { - const pinecone = await getPineconeClient(chat.id); - - return pinecone.deleteAll(); -} - -export async function deleteChatAndDependencies( - chat: Chat, +export async function deleteChat( + chatId: Tables<"Chat">["id"], shouldRedirect = true, ) { - await Promise.allSettled([ - deleteChat(chat), - deleteDocumentFile(chat), - deleteNamespace(chat), - ]); + const supabase = createClient(); + + const { data, error } = await supabase + .from("Chat") + .delete() + .eq("id", chatId) + .select("id"); + if (error) return { error }; + + revalidateTag("documents"); revalidatePath("/chat"); - if (!shouldRedirect) return; + if (!shouldRedirect) return { error: null }; + + const { data: firstDocument } = await supabase + .from("Document") + .select("chatId") + .single(); - const firstChat = await prisma.chat.findFirst(); + if (firstDocument?.chatId) redirect(`/chat/${firstDocument.chatId}`); - if (firstChat?.id) return redirect(`/chat/${firstChat.id}`); redirect("/chat"); } diff --git a/apps/chat-with-pdf/app/actions/edit-chat.ts b/apps/chat-with-pdf/app/actions/edit-chat.ts index 7e450a2..2c67df3 100644 --- a/apps/chat-with-pdf/app/actions/edit-chat.ts +++ b/apps/chat-with-pdf/app/actions/edit-chat.ts @@ -1,24 +1,20 @@ "use server"; -import { prisma } from "@/lib/prisma"; -import { Chat, Prisma } from "@prisma/client"; -import { revalidatePath } from "next/cache"; +import { createClient } from "@/lib/supabase/server"; +import { Tables } from "database.types"; +import { revalidatePath, revalidateTag } from "next/cache"; -export async function editChat(chat: Chat, title: string) { - const newChat = await prisma.chat.update({ - where: { - id: chat.id, - }, - data: { - documentMetadata: { - ...(chat.documentMetadata as Prisma.JsonObject), - title, - }, - }, - }); +export async function editChat(document: Tables<"Document">, title: string) { + const supabase = createClient(); - revalidatePath(`/chat/${chat.id}`); - revalidatePath("/chat"); + const { error } = await supabase + .from("Document") + .update({ name: title }) + .eq("id", document.id); + + if (error) throw error; - return newChat; + revalidatePath(`/chat/${document.chatId}`); + revalidatePath("/chat"); + revalidateTag("documents"); } diff --git a/apps/chat-with-pdf/app/actions/generate-document-title.ts b/apps/chat-with-pdf/app/actions/generate-document-title.ts new file mode 100644 index 0000000..0269b2d --- /dev/null +++ b/apps/chat-with-pdf/app/actions/generate-document-title.ts @@ -0,0 +1,32 @@ +"use server"; + +import { getContext } from "@/lib/context"; +import { createClient } from "@/lib/supabase/server"; +import { google } from "@ai-sdk/google"; +import { generateObject } from "ai"; +import { revalidatePath, revalidateTag } from "next/cache"; +import { z } from "zod"; + +export async function generateDocumentTitle(documentId: string) { + const documentSummary = await getContext( + "Give me a summary of the document.", + documentId, + ); + + const { object } = await generateObject({ + model: google("gemini-1.5-flash-latest"), + schema: z.object({ + title: z.string(), + }), + prompt: `You are an AI assistant that generates a title for a given text in a max of 40 characters: ${documentSummary}`, + }); + + if (object.title) { + const supabase = createClient(); + const { data } = await supabase.auth.getSession(); + revalidatePath(`/chat/${documentId}`); + revalidateTag(documentId); + } + + return object; +} diff --git a/apps/chat-with-pdf/app/actions/generate-suggested-questions.ts b/apps/chat-with-pdf/app/actions/generate-suggested-questions.ts new file mode 100644 index 0000000..6e661d9 --- /dev/null +++ b/apps/chat-with-pdf/app/actions/generate-suggested-questions.ts @@ -0,0 +1,26 @@ +"use server"; + +import { getContext } from "@/lib/context"; +import { google } from "@ai-sdk/google"; +import { generateObject } from "ai"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +export async function generateSuggestedQuestions(documentId: string) { + const documentSummary = await getContext( + "Give me a summary of the document.", + documentId, + ); + + const { object } = await generateObject({ + model: google("gemini-1.5-flash-latest"), + schema: z.object({ + questions: z.array(z.string()).min(3).max(5), + }), + prompt: `You are an AI assistant that generates a list of 3-5 suggested questions based on the following document summary. The questions should be diverse and cover different aspects of the document: ${documentSummary}`, + }); + + revalidatePath(`/chat/${documentId}`, "page"); + + return object; +} diff --git a/apps/chat-with-pdf/app/actions/get-chat-messages.ts b/apps/chat-with-pdf/app/actions/get-chat-messages.ts deleted file mode 100644 index b78137e..0000000 --- a/apps/chat-with-pdf/app/actions/get-chat-messages.ts +++ /dev/null @@ -1,18 +0,0 @@ -"use server"; - -import { prisma } from "@/lib/prisma"; -import { cache } from "react"; - -export async function getCachedChatMessages(documentId: string) { - return cache(getChatMessages)(documentId); -} - -async function getChatMessages(documentId: string) { - const chatData = await prisma.chat.findUnique({ - where: { - id: documentId, - }, - }); - - return chatData; -} diff --git a/apps/chat-with-pdf/app/actions/get-document-by-chat-id.ts b/apps/chat-with-pdf/app/actions/get-document-by-chat-id.ts new file mode 100644 index 0000000..952f0aa --- /dev/null +++ b/apps/chat-with-pdf/app/actions/get-document-by-chat-id.ts @@ -0,0 +1,42 @@ +"use server"; + +import { createClient } from "@/lib/supabase/server"; +import { SupabaseClient } from "@supabase/supabase-js"; +import { unstable_cache } from "next/cache"; + +async function retrieveDocumentByChatId( + supabase: SupabaseClient, + chatId: string, +) { + const { data: document, error: errorOnFetchingDocument } = await supabase + .from("Document") + .select("*") + .eq("chatId", chatId) + .single(); + + if (errorOnFetchingDocument) { + throw errorOnFetchingDocument; + } + + return document; +} + +export async function getDocumentByChatId(chatId: string) { + const supabase = createClient(); + const { data, error: errorOnFetchingSession } = await supabase.auth.getUser(); + + if (errorOnFetchingSession) { + throw errorOnFetchingSession; + } + + const document = unstable_cache( + (supabase: SupabaseClient) => retrieveDocumentByChatId(supabase, chatId), + [data.user.id || "", chatId], + { + revalidate: 60 * 60, + tags: ["document", data?.user?.id || "", chatId], + }, + )(supabase); + + return document; +} diff --git a/apps/chat-with-pdf/app/actions/login.ts b/apps/chat-with-pdf/app/actions/login.ts new file mode 100644 index 0000000..78f060c --- /dev/null +++ b/apps/chat-with-pdf/app/actions/login.ts @@ -0,0 +1,23 @@ +"use server"; + +import { createClient } from "@/lib/supabase/server"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +type LoginProps = { + email: string; + password: string; +}; + +export async function login(loginData: LoginProps) { + const supabase = createClient(); + + const { error } = await supabase.auth.signInWithPassword(loginData); + + if (error) { + throw error; + } + + revalidatePath("/", "layout"); + redirect("/"); +} diff --git a/apps/chat-with-pdf/app/actions/sign-in-with-oauth.ts b/apps/chat-with-pdf/app/actions/sign-in-with-oauth.ts new file mode 100644 index 0000000..e47a17e --- /dev/null +++ b/apps/chat-with-pdf/app/actions/sign-in-with-oauth.ts @@ -0,0 +1,29 @@ +"use server"; + +import { getOAuthRedirectUrl } from "@/lib/oauth-redirect-url"; +import { createClient } from "@/lib/supabase/server"; +import { SignInWithOAuthCredentials } from "@supabase/supabase-js"; +import { ReadonlyURLSearchParams, redirect } from "next/navigation"; + +export async function signInWithOAuth( + provider: SignInWithOAuthCredentials["provider"], + searchParams: ReadonlyURLSearchParams, +) { + const supabase = createClient(); + const redirectTo = getOAuthRedirectUrl(searchParams); + + const { data, error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + redirectTo, + }, + }); + + if (error) { + throw error; + } + + if (data.url) { + redirect(data.url); + } +} diff --git a/apps/chat-with-pdf/app/actions/signup.ts b/apps/chat-with-pdf/app/actions/signup.ts new file mode 100644 index 0000000..aa6b6bf --- /dev/null +++ b/apps/chat-with-pdf/app/actions/signup.ts @@ -0,0 +1,32 @@ +"use server"; + +import { createClient } from "@/lib/supabase/server"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +type SignUpProps = { + email: string; + password: string; +}; + +export async function signup(signUpData: SignUpProps) { + const supabase = createClient(); + + const baseUrl = process.env.VERCEL_URL || "https://localhost:3000"; + + const emailRedirectTo = `${baseUrl}/api/auth/callback`; + + const { error } = await supabase.auth.signUp({ + ...signUpData, + options: { + emailRedirectTo, + }, + }); + + if (error) { + throw error; + } + + revalidatePath("/", "layout"); + redirect("/"); +} diff --git a/apps/chat-with-pdf/app/actions/update-chat-messages.ts b/apps/chat-with-pdf/app/actions/update-chat-messages.ts index 96a91db..da4f253 100644 --- a/apps/chat-with-pdf/app/actions/update-chat-messages.ts +++ b/apps/chat-with-pdf/app/actions/update-chat-messages.ts @@ -1,12 +1,13 @@ "use server"; -import { prisma } from "@/lib/prisma"; -import { Chat, Prisma } from "@prisma/client"; +import { createClient } from "@/lib/supabase/server"; +import { Tables } from "database.types"; +import { revalidatePath } from "next/cache"; type UpdateChatMessagesParams = { documentId: string; - messages?: Chat["messages"]; - documentMetadata?: Chat["documentMetadata"]; + messages?: Tables<"Chat">["messages"]; + documentMetadata?: Tables<"Chat">["documentMetadata"]; }; export async function updateChatMessages({ @@ -14,14 +15,15 @@ export async function updateChatMessages({ messages, documentMetadata, }: UpdateChatMessagesParams) { - const chat = await prisma.chat.update({ - where: { - id: documentId as string, - }, - data: { - messages: messages as unknown as Chat["messages"][], - documentMetadata: documentMetadata as Prisma.JsonObject, - }, - }); - return chat; + const supabase = createClient(); + + await supabase + .from("Chat") + .update({ + messages, + documentMetadata, + }) + .eq("id", documentId); + + revalidatePath(`/chat/${documentId}`, "page"); } diff --git a/apps/chat-with-pdf/app/api/auth/callback/route.ts b/apps/chat-with-pdf/app/api/auth/callback/route.ts new file mode 100644 index 0000000..d132e87 --- /dev/null +++ b/apps/chat-with-pdf/app/api/auth/callback/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +// The client you created from the Server-Side Auth instructions +import { createClient } from "@/lib/supabase/server"; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get("code"); + // if "next" is in param, use it as the redirect URL + const next = searchParams.get("next") ?? "/"; + + if (code) { + const supabase = createClient(); + const { error } = await supabase.auth.exchangeCodeForSession(code); + + if (!error) { + const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer + const isLocalEnv = process.env.NODE_ENV === "development"; + if (isLocalEnv) { + // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host + return NextResponse.redirect(`${origin}${next}`); + } else if (forwardedHost) { + return NextResponse.redirect(`https://${forwardedHost}${next}`); + } else { + return NextResponse.redirect(`${origin}${next}`); + } + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth/auth-code-error`); +} diff --git a/apps/chat-with-pdf/app/api/auth/confirm/route.ts b/apps/chat-with-pdf/app/api/auth/confirm/route.ts new file mode 100644 index 0000000..2d0d063 --- /dev/null +++ b/apps/chat-with-pdf/app/api/auth/confirm/route.ts @@ -0,0 +1,28 @@ +import { type EmailOtpType } from "@supabase/supabase-js"; +import { type NextRequest } from "next/server"; + +import { redirect } from "next/navigation"; +import { createClient } from "@/lib/supabase/server"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const token_hash = searchParams.get("token_hash"); + const type = searchParams.get("type") as EmailOtpType | null; + const next = searchParams.get("next") ?? "/"; + + if (token_hash && type) { + const supabase = createClient(); + + const { error } = await supabase.auth.verifyOtp({ + type, + token_hash, + }); + if (!error) { + // redirect user to specified redirect URL or root of app + redirect(next); + } + } + + // redirect the user to an error page with some instructions + redirect("/error"); +} diff --git a/apps/chat-with-pdf/app/api/auth/signup/route.tsx b/apps/chat-with-pdf/app/api/auth/signup/route.tsx new file mode 100644 index 0000000..2fe53b4 --- /dev/null +++ b/apps/chat-with-pdf/app/api/auth/signup/route.tsx @@ -0,0 +1,35 @@ +import { VerifyEmailTemplate } from "@/components/email-templates/verify-email-template"; +import { supabaseAdmin } from "lib/supabase/admin"; +import { Resend } from "resend"; + +const resend = new Resend(process.env.RESEND_API_KEY); + +export async function POST(request: Request) { + // rate limit + + const data = await request.json(); + const supabase = supabaseAdmin(); + + const res = await supabase.auth.admin.generateLink({ + type: "signup", + email: data.email, + password: data.password, + }); + + if (res.data.properties?.email_otp) { + // resend email + const resendRes = await resend.emails.send({ + from: `Makify `, + to: [data.email], + subject: "Verify Email", + react: ( + + ), + }); + return Response.json(resendRes); + } else { + return Response.json({ data: null, error: res.error }); + } +} diff --git a/apps/chat-with-pdf/app/api/chat/new-chat/route.ts b/apps/chat-with-pdf/app/api/chat/new-chat/route.ts index 8fabbbf..88e4277 100644 --- a/apps/chat-with-pdf/app/api/chat/new-chat/route.ts +++ b/apps/chat-with-pdf/app/api/chat/new-chat/route.ts @@ -1,15 +1,14 @@ -import { deleteChatAndDependencies } from "@/app/actions/delete-chat"; -import { INPUT_NAME } from "@/components/header/document-switcher/constants/input-names"; -import { chunkedUpsert } from "@/lib/chunked-upsert"; +import { deleteChat } from "@/app/actions/delete-chat"; +import { INPUT_NAME } from "@/components/header/document-title/constants/input-names"; import { embedDocument, prepareDocument } from "@/lib/embed-document"; import { getLoadingMessages } from "@/lib/get-loading-messages"; import { getPdfData } from "@/lib/get-pdf-metadata"; -import { prisma } from "@/lib/prisma"; import { rateLimitRequests } from "@/lib/rate-limit-requests"; -import { supabase } from "@/lib/supabase"; +import { createClient } from "@/lib/supabase/server"; import { WebPDFLoader } from "@langchain/community/document_loaders/web/pdf"; import { PineconeRecord } from "@pinecone-database/pinecone"; -import { Prisma } from "@prisma/client"; +import { Tables } from "database.types"; +import { revalidateTag } from "next/cache"; import { NextRequest, NextResponse } from "next/server"; export const revalidate = 0; @@ -27,13 +26,15 @@ export async function POST(request: NextRequest) { }); } + let stream; + try { const formData = await request.formData(); const documentUrl = formData.get(INPUT_NAME.LINK) as string; const documentFile = formData.get(INPUT_NAME.FILE) as File; - const stream = new ReadableStream({ + stream = new ReadableStream({ async start(controller) { /* for await (const loadingMessages of createNewChatMocked({ documentUrl, @@ -64,15 +65,17 @@ export async function POST(request: NextRequest) { controller.close(); }, }); - - return new NextResponse(stream, { - headers, - }); } catch (error: any) { return new Response(JSON.stringify({ error: error?.message }), { status: 500, }); } + + revalidateTag("chats"); + + return new NextResponse(stream, { + headers, + }); } async function* createNewChat({ @@ -82,6 +85,7 @@ async function* createNewChat({ documentUrl: string; documentFile?: File; }) { + const supabase = createClient(); // Fetching PDF data and creating a new chat in the database yield getLoadingMessages({ isViaLink: !!documentUrl, @@ -90,22 +94,43 @@ async function* createNewChat({ // TODO: How to remove this delay? // It doesn't work well without it, the data seems to arrive appended to the fronted await new Promise((resolve) => setTimeout(resolve, 1000)); - let chat: any; - let pdfData; - try { - pdfData = await getPdfData({ documentUrl, documentFile }); - chat = await prisma.chat.create({ - data: { - documentUrl: documentUrl, - documentMetadata: pdfData?.metadata, - }, + const pdfData = await getPdfData({ documentUrl, documentFile }); + // Insert the chat in the database + const { data: chat, error: chatError } = await supabase + .from("Chat") + .insert({ + documentUrl, + documentMetadata: pdfData?.metadata, + }) + .select("id") + .single(); + + if (chatError) { + console.error(chatError); + return getLoadingMessages({ + isViaLink: !!documentUrl, + chatId: null, + errorMessage: chatError?.message || chatError, }); - } catch (error: any) { - console.error(error); + } + + // Insert the document in the database + const { data: document, error: documentError } = await supabase + .from("Document") + .insert({ + url: documentUrl, + metadata: pdfData?.metadata, + chatId: chat?.id, + }) + .select("id") + .single(); + + if (documentError) { + console.error(documentError); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: null, - errorMessage: error?.message || error, + errorMessage: documentError?.message || documentError, }); } @@ -115,27 +140,28 @@ async function* createNewChat({ .upload(`${chat.id}.pdf`, documentFile!); if (error) { console.error(error); - await deleteChatAndDependencies(chat, false); + await deleteChat(chat.id, false); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: chat.id, errorMessage: error?.message, }); } - try { - documentUrl = getPdfUrlFromSupabaseStorage(data!); - await prisma.chat.update({ - where: { id: chat.id }, - data: { documentUrl }, - }); - } catch (error: any) { - console.error(error); - await deleteChatAndDependencies(chat, false); + documentUrl = getPdfUrlFromSupabaseStorage(data!); + const { error: chatUpdateError } = await supabase + .from("Chat") + .update({ + documentUrl, + }) + .eq("id", chat.id); + + if (chatUpdateError) { + await deleteChat(chat.id, false); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: chat.id, - errorMessage: error?.message || error, + errorMessage: chatUpdateError?.message || chatUpdateError, }); } } @@ -153,7 +179,7 @@ async function* createNewChat({ const loader = new WebPDFLoader(pdfData?.pdfBlob as Blob); pages = await loader.load(); if (pages.length > 5) { - await deleteChatAndDependencies(chat, false); + await deleteChat(chat.id, false); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: chat.id, @@ -164,7 +190,7 @@ async function* createNewChat({ } } catch (error: any) { console.error(error); - await deleteChatAndDependencies(chat, false); + await deleteChat(chat.id, false); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: chat.id, @@ -183,7 +209,7 @@ async function* createNewChat({ ); } catch (error: any) { console.error(error); - await deleteChatAndDependencies(chat, false); + await deleteChat(chat.id, false); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: chat.id, @@ -206,7 +232,7 @@ async function* createNewChat({ )) as PineconeRecord[]; } catch (error: any) { console.error(error); - await deleteChatAndDependencies(chat, false); + await deleteChat(chat.id, false); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: chat.id, @@ -222,15 +248,25 @@ async function* createNewChat({ // TODO: How to remove this delay? // It doesn't work well without it, the data seems to arrive appended to the fronted await new Promise((resolve) => setTimeout(resolve, 10)); - try { - await chunkedUpsert(vectors, chat.id); - } catch (error: any) { - console.error(error); - await deleteChatAndDependencies(chat, false); + + const vectorsToInsert = vectors.map((vector) => ({ + chatId: chat.id, + embedding: vector.values, + text: vector.metadata?.text ? vector.metadata.text : null, + textChunk: vector.metadata?.textChunk ? vector.metadata.textChunk : null, + pageNumber: vector.metadata?.pageNumber ? vector.metadata.pageNumber : null, + documentId: document.id, + })); + + const { error: documentSectionsError } = await supabase + .from("DocumentSections") + .insert(vectorsToInsert); + if (documentSectionsError) { + await deleteChat(chat.id, false); return getLoadingMessages({ isViaLink: !!documentUrl, chatId: chat.id, - errorMessage: error?.message || error, + errorMessage: documentSectionsError?.message || documentSectionsError, }); } @@ -247,85 +283,3 @@ async function* createNewChat({ function getPdfUrlFromSupabaseStorage({ fullPath }: { fullPath: string }) { return `${process.env.SUPABASE_URL}/storage/v1/object/public/${fullPath}`; } - -async function* createNewChatMocked({ - documentUrl, - documentFile, -}: { - documentUrl: string; - documentFile?: File; -}) { - // Fetching PDF data and creating a new chat in the database - yield getLoadingMessages({ - isViaLink: !!documentUrl, - chatId: "f7cd3ecc-3e94-43a4-a19d-cffbd35779a0", - }); - // TODO: How to remove this delay? - // It doesn't work well without it, the data seems to arrive appended to the fronted - try { - await new Promise((resolve, reject) => - setTimeout(() => reject("weird error oh my god"), 1000), - ); - } catch (error: any) { - console.log("We got an error:"); - return getLoadingMessages({ - isViaLink: !!documentUrl, - chatId: "f7cd3ecc-3e94-43a4-a19d-cffbd35779a0", - errorMessage: error?.message || error, - }); - } - /* const pdfData = await getPdfData({ link: documentUrl }); - const chat = await prisma.chat.create({ - data: { - documentUrl: documentUrl, - documentMetadata: pdfData?.metadata, - }, - }); */ - // Load the PDF - // TODO: How to remove this delay? - // It doesn't work well without it, the data seems to arrive appended to the fronted - await new Promise((resolve) => setTimeout(resolve, 1000)); - /* const loader = new WebPDFLoader(pdfData?.pdfBlob as Blob); - const pages = await loader.load(); */ - - // Split it into chunks - yield getLoadingMessages({ - isViaLink: !!documentUrl, - chatId: "f7cd3ecc-3e94-43a4-a19d-cffbd35779a0", - }); - // TODO: How to remove this delay? - // It doesn't work well without it, the data seems to arrive appended to the fronted - await new Promise((resolve) => setTimeout(resolve, 1000)); - /* const documents = await Promise.all( - pages.map((page) => prepareDocument(page, chat.id)), - ); */ - - // Vectorize the documents - yield getLoadingMessages({ - isViaLink: !!documentUrl, - chatId: "f7cd3ecc-3e94-43a4-a19d-cffbd35779a0", - }); - // TODO: How to remove this delay? - // It doesn't work well without it, the data seems to arrive appended to the fronted - await new Promise((resolve) => setTimeout(resolve, 1000)); - /* const vectors = await Promise.all(documents.flat().map(embedDocument)); */ - - // Store the vectors in Pinecone - yield getLoadingMessages({ - isViaLink: !!documentUrl, - chatId: "f7cd3ecc-3e94-43a4-a19d-cffbd35779a0", - }); - // TODO: How to remove this delay? - // It doesn't work well without it, the data seems to arrive appended to the fronted - await new Promise((resolve) => setTimeout(resolve, 2100)); - /* await chunkedUpsert(vectors, chat.id); */ - - // Set as completed the last message - yield getLoadingMessages({ - isViaLink: !!documentUrl, - chatId: "f7cd3ecc-3e94-43a4-a19d-cffbd35779a0", - }); - // TODO: How to remove this delay? - // It doesn't work well without it, the data seems to arrive appended to the fronted - /* await new Promise((resolve) => setTimeout(resolve, 0)); */ -} diff --git a/apps/chat-with-pdf/app/api/chat/route.ts b/apps/chat-with-pdf/app/api/chat/route.ts index 77082a1..86bc106 100644 --- a/apps/chat-with-pdf/app/api/chat/route.ts +++ b/apps/chat-with-pdf/app/api/chat/route.ts @@ -1,7 +1,7 @@ import { rateLimitRequests } from "@/lib/rate-limit-requests"; import { google } from "@ai-sdk/google"; -import { Chat } from "@prisma/client"; -import { CoreMessage, JSONValue, Message, StreamData, streamText } from "ai"; +import { Message, StreamData, convertToCoreMessages, streamText } from "ai"; +import { Tables } from "database.types"; import { getContext } from "utils/context"; export const revalidate = 0; @@ -10,7 +10,7 @@ export const maxDuration = 30; type RequestBody = { messages: Message[]; - documentId: Chat["id"]; + documentId: Tables<"Chat">["id"]; data: { [key: string]: any; }; @@ -33,6 +33,7 @@ export async function POST(req: Request) { data: messageData = {}, } = (await req.json()) as RequestBody; const lastMessage = messages.at(-1) as Message; + const userMessage = parsedUserMessage( lastMessage, (lastMessage.data as Record)?.quotedText as string, @@ -42,11 +43,7 @@ export async function POST(req: Request) { ? await getContext(userMessage, documentId) : { page1: "" }; - const userMessages = messages.filter((message) => message?.role === "user"); - - userMessages[userMessages.length - 1]!.content = userMessage; - console.log(documentContext); - const messagesToAI = [...userMessages]; + messages[messages.length - 1]!.content = userMessage; const systemInstructions = `AI assistant is a brand new, powerful, human-like artificial intelligence. The traits of AI include expert knowledge, helpfulness, cleverness, and articulateness. @@ -57,26 +54,35 @@ export async function POST(req: Request) { ${documentContext} END OF DOCUMENT BLOCK AI assistant will take into account any DOCUMENT BLOCK that is provided in a conversation. - The DOCUMENT BLOCK includes a START PAGE {page number} BLOCK, AI will use the {page number} in the response to inform the user where the information was found, the page number should have this format :page[{page number}]. - If the document does not provide the answer to question, will try to answer the question based on the document. + When referencing information from the document, AI will use the following format: + 1. Apply underline styling to the AI's interpretation or paraphrase of the information using HTML tags. + 2. Include two attributes in the tag: + - data-page: containing the page number where the information is found + - data-based-text: containing the exact text from the DOCUMENT BLOCK that the AI used as a basis for its response + 3. The content inside the tags can be the AI's own words, related to but not necessarily identical to the data-based-text. + 4. Immediately after the closing tag, add the page reference as a superscript with the format {page number}. + 5. If information spans multiple pages or comes from different pages, use separate underline tags and superscript references for each. + 6. Always try to wrap entire paragraphs, sentences or meaningful phrases within the tags. + + Example: "The document discusses language learning difficulty. It suggests that English is relatively simple for many learners to acquire3. In contrast, Mandarin Chinese is described as particularly challenging for English speakers, largely due to its tonal aspects and intricate writing system5." + + AI assistant must ensure that the data-based-text attribute contains the exact text extracted from the DOCUMENT BLOCK, while the content inside the tags can be the AI's interpretation or paraphrase of that information. + AI assistant can format the response using either Markdown or HTML, but should not mix the two formats within the same response. For example: + Correct (HTML): "

This is a paragraph.

  • List item
" + Correct (Markdown): "This is a paragraph.\n\n- List item" + Incorrect (Mixed): "

This is a **bold** paragraph.

" + If the document does not provide the answer to a question, AI will try to answer based on the document's context without inventing information. AI assistant will not invent anything that is not drawn directly from the document.`; const data = new StreamData(); data.append(messageData); const result = await streamText({ - model: google("models/gemini-1.5-pro-latest"), - messages: messagesToAI as CoreMessage[], + model: google("gemini-1.5-flash-latest"), + messages: convertToCoreMessages(messages), system: systemInstructions, maxTokens: 3000, - onFinish({ - text, - toolCalls, - toolResults, - usage, - finishReason, - rawResponse, - }) { + onFinish({ text, toolCalls, toolResults, usage, finishReason }) { console.log({ onFinish: { text, @@ -84,17 +90,19 @@ export async function POST(req: Request) { toolResults, usage, finishReason, - rawResponse, }, }); data.close(); }, + maxSteps: 5, }); - return result.toAIStreamResponse({ data: data, headers }); + return result.toDataStreamResponse({ data, headers }); } function parsedUserMessage(lastMessage: Message, quotedText: string) { + if (!lastMessage) return ""; + if (quotedText) { return `Given this text extracted from the document: "${quotedText}" diff --git a/apps/chat-with-pdf/app/auth/callback/route.ts b/apps/chat-with-pdf/app/auth/callback/route.ts new file mode 100644 index 0000000..20d3ae3 --- /dev/null +++ b/apps/chat-with-pdf/app/auth/callback/route.ts @@ -0,0 +1,39 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { type CookieOptions, createServerClient } from "@supabase/ssr"; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get("code"); + const next = searchParams.get("next") ?? "/"; + + console.log({ code, next }); + + if (code) { + const cookieStore = cookies(); + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + get(name: string) { + return cookieStore.get(name)?.value; + }, + set(name: string, value: string, options: CookieOptions) { + cookieStore.set({ name, value, ...options }); + }, + remove(name: string, options: CookieOptions) { + cookieStore.delete({ name, ...options }); + }, + }, + }, + ); + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (!error) { + return NextResponse.redirect(`${origin}${next}`); + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth-failed`); +} diff --git a/apps/chat-with-pdf/app/chat/(default)/layout.tsx b/apps/chat-with-pdf/app/chat/(default)/layout.tsx deleted file mode 100644 index ebcf44d..0000000 --- a/apps/chat-with-pdf/app/chat/(default)/layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Header } from "@/components/header/header"; -import { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Chats", -}; - -export default async function ChatLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( -
-
-
{children}
-
- ); -} diff --git a/apps/chat-with-pdf/app/chat/(default)/page.tsx b/apps/chat-with-pdf/app/chat/(default)/page.tsx deleted file mode 100644 index a5e6bf0..0000000 --- a/apps/chat-with-pdf/app/chat/(default)/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ChatsContainer } from "@/components/pages-containers/chats-container"; -import { prisma } from "@/lib/prisma"; -import { cache } from "react"; - -export const dynamic = "force-dynamic"; - -const getCachedChats = cache(getChats); - -async function getChats() { - const chats = await prisma.chat.findMany(); - return chats; -} - -export default async function Page() { - const chats = await getCachedChats(); - - return ; -} diff --git a/apps/chat-with-pdf/app/chat/[documentId]/layout.tsx b/apps/chat-with-pdf/app/chat/[documentId]/layout.tsx deleted file mode 100644 index 6da01f0..0000000 --- a/apps/chat-with-pdf/app/chat/[documentId]/layout.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Header } from "@/components/header/header"; -import { prisma } from "@/lib/prisma"; -import { cache } from "react"; - -const getCachedChats = cache(getChats); - -async function getChats() { - const chats = await prisma.chat.findMany(); - return chats; -} - -export default async function ChatLayout({ - children, -}: { - children: React.ReactNode; -}) { - const chats = await getCachedChats(); - return ( -
-
- {children} -
- ); -} diff --git a/apps/chat-with-pdf/app/components/chat/assistant-message.tsx b/apps/chat-with-pdf/app/components/chat/assistant-message.tsx index aa3f29a..5c42f75 100644 --- a/apps/chat-with-pdf/app/components/chat/assistant-message.tsx +++ b/apps/chat-with-pdf/app/components/chat/assistant-message.tsx @@ -6,10 +6,8 @@ import { } from "@makify/ui"; import { cn } from "@makify/ui/lib/utils"; import { useGlobalChat } from "hooks/use-global-chat"; -import { HTMLAttributes, ReactNode } from "react"; -import Markdown, { Components } from "react-markdown"; -import remarkDirective from "remark-directive"; -import remarkDirectiveRehype from "remark-directive-rehype"; +import Markdown, { ExtraProps } from "react-markdown"; +import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; import { MESSAGE_TYPE } from "./constants/message-type"; @@ -22,96 +20,115 @@ export function AssistantMessage({ }) { const { useChatReturn: { append }, + globalContext: { setDocumentState }, } = useGlobalChat(); function submitQuestion(question: string) { append({ role: "user", content: question }); } + function handlePageNumberChange(event: React.MouseEvent) { + // find a element with a data-page attribute value + const pageElement = + event.currentTarget.closest("[data-page]") || + event.currentTarget.querySelector("[data-page]"); + const pageNumber = parseInt(pageElement?.getAttribute("data-page") ?? "1"); + + if (pageNumber) setDocumentState({ currentPage: pageNumber }); + } + return (

{children}

, - a: ({ children, href }) => ( - - {children} - - ), - ul: ({ children }) => ( -
    - {children} -
- ), - ol: ({ children }) => ( -
    - {children} -
- ), - li: ({ children }) => ( -
  • - {type === MESSAGE_TYPE.SUGGESTION_MESSAGES ? ( - + ) : ( + children + )} +
  • + ), + sup: ({ children }: React.HTMLProps) => ( + + + + {children} - - ) : ( - children - )} - - ), - page: ({ children }: { children: ReactNode }) => ( - - - - - {children} - - - Based on the page {children} - - - ), - } as Partial & { - // change the type of the page component to type of HTMLAttributes - page: HTMLAttributes; - } - } + + + Based on the page {children} + + + ), + u: ({ children, ...props }: React.HTMLProps) => { + const { node, ...attributes } = props as { + node: ExtraProps["node"]; + [key: string]: any; + }; + return ( + + ); + }, + }} > {message}
    ); } - -function PageComponent({ children }: { children: string }) { - return
    {children}
    ; -} diff --git a/apps/chat-with-pdf/app/components/chat/chat-footer.tsx b/apps/chat-with-pdf/app/components/chat/chat-footer.tsx index 192abaa..88bc84e 100644 --- a/apps/chat-with-pdf/app/components/chat/chat-footer.tsx +++ b/apps/chat-with-pdf/app/components/chat/chat-footer.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Alert, AlertDescription, @@ -10,14 +12,18 @@ import { Message } from "ai"; import { AnimatePresence, motion } from "framer-motion"; import { useGlobalChat } from "hooks/use-global-chat"; import { SendIcon, XIcon } from "lucide-react"; -import { FormEvent, KeyboardEvent, useRef, useState } from "react"; +import { FormEvent, KeyboardEvent, useEffect, useRef, useState } from "react"; +import { SuggestedQuestions } from "./chat-footer/suggested-questions"; + +const AnimatedSuggestedQuestions = motion(SuggestedQuestions); export function ChatFooter() { const formRef = useRef(null); + const textareaRef = useRef(null); const [hasTextareaGrown, setHasTextareaGrown] = useState(false); const { - globalContext: { extraData, setExtraData }, + globalContext: { extraData, chatData, setExtraData }, useChatReturn: { input: inputValue, setInput, @@ -27,6 +33,8 @@ export function ChatFooter() { }, } = useGlobalChat(); + useEffect(autoFocusTextarea, [extraData?.quotedText]); + function extractTextareaLineHeight(textarea: HTMLTextAreaElement) { const computedStyle = window.getComputedStyle(textarea); const lineHeight = computedStyle.lineHeight; @@ -64,6 +72,12 @@ export function ChatFooter() { setInput(""); } + function autoFocusTextarea() { + if (textareaRef.current && Boolean(extraData?.quotedText)) { + textareaRef.current.focus(); + } + } + function handleOnSubmit( event: FormEvent | KeyboardEvent, ) { @@ -77,7 +91,12 @@ export function ChatFooter() { } return ( -
    +
    + + {chatData.suggestedQuestions && ( + + )} + {(extraData?.quotedText as string) && ( )} +