Build a PDF Summarizer in Next.js Using Gemini, PDF.js and Strapi
In this tutorial, we will create a PDF summarizer using Next.js, Pdf.js, Google Generative AI (Gemini), and Strapi. The application will allow users to upload a PDF file, extract its text content, generate a summary of the text using an AI model, and store the summary in a Strapi backend. We'll also create a page where you can view all your summarized PDFs without having to navigate to the Strapi backend. To start, we need to install the necessary dependencies: ```bash npm install next@latest react@latest react-dom@latest typescript @types/react @types/react-dom @mui/material @emotion/react @emotion/styled pdfjs axios googleapis ``` Next, we'll create a new Next.js project: ```bash npx create-next-app@latest --ts ``` Now let's start building our application. First, we need to set up the Pdf.js library for extracting text from PDF files. Create a file called `pdf-reader.tsx` in your components folder and add the following code: ```javascript import { useEffect, useState } from "react"; import * as pdfjsLib from "pdfjs-dist/build/pdf"; import "@pdfjs/dist/web/pdf_viewer.css"; interface PDFData { text: string; } const PDFReader = ({ file }: { file: File }) => { const [data, setData] = useState<PDFData>({ text: "" }); useEffect(() => { if (!file) return; const loadingTask = pdfjsLib.getDocument(URL.createObjectURL(file)); loadingTask.promise.then((pdf) => { let pageNumber = 1; const maxPages = pdf.numPages; const extractTextFromPage = async (page: any) => { return new Promise((resolve, reject) => { page.getTextContent().then((textContent) => { resolve(textContent.items.map((item: any) => item.str).join("")); }); }); }; const extractText = async () => { let text = ""; while (pageNumber <= maxPages) { const currentPage = await pdf.getPage(pageNumber); text += await extractTextFromPage(currentPage); pageNumber++; } setData({ text }); }; void extractText(); }); }, [file]); return <div>{data.text}</div>; }; export default PDFReader; ``` Now let's create the page where users can upload their PDF files and view the extracted text content. Create a file called `page.tsx` in your pages folder and add the following code: ```javascript import { useState } from "react"; import type { NextPage } from "next"; import Head from "next/head"; import Image from "next/image"; import styles from "../styles/Home.module.css"; import PDFReader from "../components/PDFReader"; const Home: NextPage = () => { const [file, setFile] = useState<File | null>(null); return ( <div className={styles.container}> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <h1 className={styles.title}>PDF Summarizer</h1> <div className={styles.grid}> <div className={styles.card}> <input type="file" onChange={(e) => setFile(e.target.files?.[0])} /> {file && ( <PDFReader file={file} /> )} </div> </div> </main> </div> ); }; export default Home; ``` Now let's create the API route for summarizing the extracted text content. Create a file called `api/summarize-pdf.ts` and add the following code: ```javascript import { NextResponse } from "next/server"; import { GoogleGenerativeAI } from "@google/generative-ai"; const genAI = new GoogleGenerativeAI(process.env.API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-pro" }); export async function POST(req) { try { const body = await req.json(); console.log("Received title:", body.title); console.log("Received text length:", body.text.length); if (!body.title) { throw new Error("No title provided"); } const prompt = "summarize the following extracted texts: " + body.text; const result = await model.generateContent(prompt); const summaryText = result.response.text(); console.log("Summary generated successfully"); const strapiRes = await fetch("http://localhost:1337/api/summarized-pdfs", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ data: { Title: body.title, Summary: summaryText, }, }), }); if (!strapiRes.ok) { const errorText = await strapiRes.text(); console.error("Strapi error response:", errorText); throw new Error( `Failed to store summary in Strapi: ${strapiRes.status} ${strapiRes.statusText}` ); } const strapiData = await strapiRes.json(); console.log("Successfully stored in Strapi:", strapiData); return NextResponse.json({ success: true, message: "Text summarized and stored successfully", Summary: summaryText, }); } catch (error) { console.error("Error in API route:", error); return NextResponse.json( { success: false, message: "Error processing request", error: error.message, }, { status: 500 } ); } } ``` Now let's test the app to see if it works: You can see it summarizes the PDF. It is also added to your Strapi backend: We’ve accomplished summarizing a PDF and storing the summarized content in Strapi! That’s huge! Now, you can choose to stop here or continue with me by creating a table where you can view all your summarized PDFs without having to navigate to the Strapi backend. Let’s try to add that in the next section. To do this, you’ll need to use the link component. Go back to your page.js and import it into the page: ```javascript import Link from "next/link"; ``` Below the div created for displaying the summarized PDF, add the following: ```javascript <div className="w-full max-w-md text-center"> <Link href="/summaries" className="bg-green-600 text-white px-6 py-2 rounded hover:bg-green-500 transition-colors inline-block"> View Summarized PDFs </Link> </div>; ``` Now, let’s create the endpoint. Inside the app folder, create a folder called summaries and inside the folder, you'll first create a file called page.js. Inside the file, add the following code: ```javascript import { useState, useEffect } from "react"; import Link from "next/link"; import ReactMarkdown from "react-markdown"; export default function Summaries() { const [summaries, setSummaries] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetchSummaries(); }, []); const fetchSummaries = async () => { try { const response = await fetch("http://localhost:1337/api/summarized-pdfs"); if (!response.ok) { throw new Error("Failed to fetch summaries"); } const data = await response.json(); console.log("Fetched data:", data); setSummaries(data.data || []); setIsLoading(false); } catch (error) { console.error("Fetch error:", error); setError(error.message); setIsLoading(false); } }; if (isLoading) return <div className="text-white">Loading...</div>; if (error) return <div className="text-white">Error: {error}</div>; return ( <div className="min-h-screen bg-[#32324d] py-8 text-white"> <div className="max-w-4xl mx-auto"> <h1 className="text-3xl font-bold mb-8 text-center">Summarized PDFs</h1> <Link href="/" className="bg-[#4945ff] text-white px-4 py-2 rounded mb-4 inline-block"> Back to Upload </Link> {summaries.length === 0 ? ( <p>No summaries available.</p> ) : ( <table className="min-w-full bg-gray-800 border-collapse"> <thead> <tr> <th className="border border-gray-600 px-4 py-2">ID</th> <th className="border border-gray-600 px-4 py-2">Title</th> <th className="border border-gray-600 px-4 py-2">Short Text</th> <th className="border border-gray-600 px-4 py-2">View</th> </tr> </thead> <tbody> {summaries.map((summary) => ( <tr key={summary.id} className="hover:bg-gray-700"> <td className="border border-gray-600 px-4 py-2"> {summary.id} </td> <td className="border border-gray-600 px-4 py-2"> {summary.Title} </td> <td className="border border-gray-600 px-4 py-2"> <ReactMarkdown className="prose prose-invert max-w-none"> {typeof summary.Summary === "string" ? summary.Summary.slice(0, 100) + "..." : "Summary not available"} </ReactMarkdown> </td> <td className="border border-gray-600 px-4 py-2"> <Link href={`/summaries/${summary.id}`} className="bg-[#4945ff] text-white px-4 py-2 rounded"> View </Link> </td> </tr> ))} </tbody> </table> )} </div> </div> ); } ``` In the code above, we created a React component that displays a list of summarized PDFs fetched from a Strapi backend. It renders a table with summary details, including an ID, title, and a shortened version of the summary content. The href={/summaries/${summary.id}} in the "View" button dynamically generates a URL based on the id of each summary. This allows you to click the "View" button and navigate to a page to view the specific summarized PDF for that id. If you click the view summarized button, it should redirect you to this page: To manage dynamic routing for each summary based on its id, you must create a folder named [id] and two files inside the app/summaries directory. The first to create is the page.js. After creating it, add the following code: ```javascript import { Suspense } from "react"; import Link from "next/link"; import SummaryContent from "./SummaryContent"; export default function SummaryPage({ params }) { return ( <div className="min-h-screen bg-[#32324d] py-8 text-white"> <div className="max-w-4xl mx-auto px-4"> <Link href="/summaries" className="bg-[#4945ff] text-white px-4 py-2 rounded mb-4 inline-block"> Back to Summaries </Link> <Suspense fallback={<div>Loading...</div>}> <SummaryContent id={params.id} /> </Suspense> </div> </div> ); } ``` In the code above, we created a component responsible for displaying the detailed view of a summarized PDF based on its id. The Suspense component displays a fallback loading message (<div>Loading...</div>) while SummaryContent is fetched. The SummaryContent component (which we'll create shortly) is passed the id from params.id, corresponding to the specific summary being viewed. Now let's create the second page. Still inside the [id] folder, create a file called SummaryContent.js and add the following code: ```javascript import { Suspense } from "react"; import { useState, useEffect } from "react"; import ReactMarkdown from "react-markdown"; export default function SummaryContent({ id }) { const [summary, setSummary] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchSummary = async () => { try { const response = await fetch( `http://localhost:1337/api/summarized-pdfs?filters[id][$eq]=${id}`, ); if (!response.ok) { throw new Error("Failed to fetch summary"); } const data = await response.json(); if (data.data && data.data.length > 0) { setSummary(data.data[0]); } else { throw new Error("Summary not found"); } setIsLoading(false); } catch (error) { console.error("Fetch error:", error); setError(error.message); setIsLoading(false); } }; fetchSummary(); }, [id]); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; if (!summary) return <div>Summary not found</div>; return ( <> <h1 className="text-3xl font-bold mb-4">{summary.Title}</h1> <div className="bg-gray-800 p-6 rounded-lg"> <ReactMarkdown className="prose prose-invert max-w-none"> {summary.Summary} </ReactMarkdown> </div> </> ); } ``` The SummaryContent component fetches and displays a specific summarized PDF based on the provided id. Now let's check the result in the browser to see if it works: It works! Our PDF summarizer is complete! Here's the link to the code on GitHub. That's How to Create a PDF Summarizer. In this tutorial, we learned how to build a PDF summarizer in Next.js using Pdf.js, Google Generative AI (Gemini), and Strapi. You can also choose to enhance yours by adding other features too. There are quite a lot of things you can build using AI tools and Strapi. Love to see what you can build. Please share if you found this tutorial helpful.
Company
Strapi
Date published
Sept. 24, 2024
Author(s)
Temitope Oyedele
Word count
3614
Language
English
Hacker News points
None found.