├── .eslintrc.json ├── .example.env ├── .gitignore ├── .node-version ├── .prettierrc ├── LICENSE ├── README.md ├── components.json ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prisma └── schema.prisma ├── public ├── file.svg ├── globe.svg ├── next.svg ├── og.jpg ├── vercel.svg └── window.svg ├── src ├── app │ ├── actions.ts │ ├── api │ │ ├── image │ │ │ └── route.ts │ │ ├── s3-upload │ │ │ └── route.ts │ │ └── summarize │ │ │ └── route.ts │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── icon.png │ ├── layout.tsx │ ├── page.tsx │ └── pdf │ │ └── [id] │ │ ├── page.tsx │ │ └── smart-pdf-viewer.tsx ├── components │ ├── HomeLandingDrop.tsx │ ├── icons │ │ ├── github.tsx │ │ ├── sparkles.tsx │ │ └── x.tsx │ ├── images │ │ ├── homepage-image-1.tsx │ │ └── homepage-image-2.tsx │ └── ui │ │ ├── action-button.tsx │ │ ├── button.tsx │ │ ├── logo.tsx │ │ ├── select.tsx │ │ ├── spinner.tsx │ │ ├── summary-content.tsx │ │ ├── table-of-contents.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx ├── hooks │ └── use-toast.ts └── lib │ ├── ai.ts │ ├── prisma.ts │ ├── s3client.ts │ ├── summarize.ts │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | TOGETHER_API_KEY= 2 | DATABASE_URL= 3 | 4 | S3_UPLOAD_KEY= 5 | S3_UPLOAD_SECRET= 6 | S3_UPLOAD_BUCKET= 7 | S3_UPLOAD_REGION= 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.12.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hassan El Mghari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | SmartPDF 3 |

SmartPDF

4 |
5 | 6 |

7 | Instantly summarize and section your PDFs with AI. Powered by Llama 3.3 on Together AI. 8 |

9 | 10 | ## Tech stack 11 | 12 | - [Together AI](https://togetherai.link) for inference 13 | - [Llama 3.3](https://togetherai.link/llama-3.3) for the LLM 14 | - Next.js with Tailwind & TypeScript 15 | - Prisma ORM with Neon (Postgres) 16 | - Helicone for observability 17 | - Plausible for analytics 18 | - S3 for PDF storage 19 | 20 | ## Cloning & running 21 | 22 | 1. Clone the repo: `git clone https://github.com/Nutlope/smartpdfs` 23 | 2. Create a `.env` file and add your environment variables (see `.example.env`): 24 | - `TOGETHER_API_KEY=` 25 | - `DATABASE_URL=` 26 | - `S3_UPLOAD_KEY=` 27 | - `S3_UPLOAD_SECRET=` 28 | - `S3_UPLOAD_BUCKET=` 29 | - `S3_UPLOAD_REGION=us-east-1` 30 | - `HELICONE_API_KEY=` (optional, for observability) 31 | 3. Run `pnpm install` to install dependencies 32 | 4. Run `pnpm prisma generate` to generate the Prisma client 33 | 5. Run `pnpm dev` to start the development server 34 | 35 | ## Roadmap 36 | 37 | - [ ] Add some rate limiting by IP address 38 | - [ ] Integrate OCR for image parsing in PDFs 39 | - [ ] Add a bit more polish (make the link icon nicer) & add a "powered by Together" sign 40 | - [ ] Implement additional revision steps for improved summaries 41 | - [ ] Add a demo PDF for new users to be able to see it in action 42 | - [ ] Add feedback system with thumbs up/down feature 43 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "napkinsdev.s3.us-east-1.amazonaws.com", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "api.together.ai", 13 | }, 14 | ], 15 | }, 16 | }; 17 | 18 | export default nextConfig; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdf-summary", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "postinstall": "prisma generate", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "update:all": "pnpm update --interactive --latest" 12 | }, 13 | "dependencies": { 14 | "@ai-sdk/togetherai": "^0.2.13", 15 | "@aws-sdk/client-s3": "^3.797.0", 16 | "@neondatabase/serverless": "^1.0.0", 17 | "@prisma/adapter-neon": "^6.6.0", 18 | "@prisma/client": "^6.6.0", 19 | "@radix-ui/react-select": "^2.1.2", 20 | "@radix-ui/react-slot": "^1.1.0", 21 | "@radix-ui/react-toast": "^1.2.2", 22 | "@tailwindcss/typography": "^0.5.16", 23 | "ai": "^4.3.10", 24 | "class-variance-authority": "^0.7.0", 25 | "clsx": "^2.1.1", 26 | "dedent": "^1.5.3", 27 | "lucide-react": "^0.503.0", 28 | "nanoid": "^5.1.5", 29 | "next": "15.3.1", 30 | "next-plausible": "^3.12.4", 31 | "next-s3-upload": "^0.3.4", 32 | "openai": "^4.73.1", 33 | "pdfjs-dist": "^4.8.69", 34 | "react": "19.1.0", 35 | "react-dom": "19.1.0", 36 | "react-dropzone": "^14.3.5", 37 | "tailwind-merge": "^2.5.4", 38 | "tailwindcss-animate": "^1.0.7", 39 | "together-ai": "^0.15.2", 40 | "ws": "^8.18.0", 41 | "zod": "^3.24.3", 42 | "zod-to-json-schema": "^3.23.5" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^22.15.3", 46 | "@types/react": "^19.1.2", 47 | "@types/react-dom": "^19.1.2", 48 | "@types/ws": "^8.5.13", 49 | "eslint": "9.25.1", 50 | "eslint-config-next": "15.3.1", 51 | "postcss": "^8", 52 | "prettier": "^3.3.3", 53 | "prettier-plugin-tailwindcss": "^0.6.8", 54 | "prisma": "^6.6.0", 55 | "tailwindcss": "^3.4.1", 56 | "typescript": "^5" 57 | }, 58 | "engines": { 59 | "node": "22.x" 60 | }, 61 | "pnpm": { 62 | "onlyBuiltDependencies": [ 63 | "@prisma/client" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model SmartPDF { 17 | id String @id @default(nanoid(5)) 18 | createdAt DateTime @default(now()) 19 | imageUrl String 20 | pdfUrl String 21 | pdfName String 22 | 23 | sections Section[] 24 | } 25 | 26 | model Section { 27 | id String @id @default(nanoid(5)) 28 | type String 29 | title String 30 | summary String 31 | position Int 32 | 33 | SmartPDF SmartPDF @relation(fields: [smartPDFId], references: [id]) 34 | smartPDFId String 35 | } 36 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/smartpdfs/76205d1a70bd5c488bcf15780abf951c4a9e084a/public/og.jpg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { nanoid } from "nanoid"; 4 | import client from "@/lib/prisma"; 5 | import { redirect } from "next/navigation"; 6 | 7 | const slugify = (text: string) => { 8 | return text 9 | .toLowerCase() 10 | .replace(/ /g, "-") 11 | .replace(/[^\w-]+/g, "") 12 | .replace(/--+/g, "-") 13 | .replace(/^-+/, "") 14 | .replace(/-+$/, "") 15 | .slice(0, 20); 16 | }; 17 | 18 | export async function sharePdf({ 19 | pdfName, 20 | pdfUrl, 21 | imageUrl, 22 | sections, 23 | }: { 24 | pdfName: string; 25 | pdfUrl: string; 26 | imageUrl: string; 27 | sections: { 28 | type: string; 29 | title: string; 30 | summary: string; 31 | position: number; 32 | }[]; 33 | }) { 34 | const smartPdf = await client.smartPDF.create({ 35 | data: { 36 | id: `${slugify(sections[0].title)}-${nanoid(4)}`, 37 | pdfName, 38 | pdfUrl, 39 | imageUrl, 40 | sections: { 41 | createMany: { 42 | data: sections, 43 | }, 44 | }, 45 | }, 46 | }); 47 | 48 | redirect(`/pdf/${smartPdf.id}`); 49 | } 50 | -------------------------------------------------------------------------------- /src/app/api/image/route.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { togetheraiBaseClient } from "@/lib/ai"; 3 | import { ImageGenerationResponse } from "@/lib/summarize"; 4 | import { awsS3Client } from "@/lib/s3client"; 5 | import { PutObjectCommand } from "@aws-sdk/client-s3"; 6 | 7 | export async function POST(req: Request) { 8 | const json = await req.json(); 9 | const text = "text" in json ? json.text : ""; 10 | 11 | const start = new Date(); 12 | 13 | const prompt = dedent` 14 | I'm going to give you a short summary of what is in a PDF. I need you to create an image that captures the essence of the content. 15 | 16 | The image should be one that looks good as a hero image on a blog post or website. It should not include any text. 17 | 18 | Here is the summary: 19 | 20 | ${text} 21 | `; 22 | 23 | const generatedImage = await togetheraiBaseClient.images.create({ 24 | model: "black-forest-labs/FLUX.1-dev", 25 | width: 1280, 26 | height: 720, 27 | steps: 24, 28 | prompt: prompt, 29 | }); 30 | 31 | const end = new Date(); 32 | console.log( 33 | `Flux took ${end.getTime() - start.getTime()}ms to generate an image`, 34 | ); 35 | 36 | const fluxImageUrl = generatedImage.data[0].url; 37 | 38 | if (!fluxImageUrl) throw new Error("No image URL from Flux"); 39 | 40 | const fluxFetch = await fetch(fluxImageUrl); 41 | const fluxImage = await fluxFetch.blob(); 42 | const imageBuffer = Buffer.from(await fluxImage.arrayBuffer()); 43 | 44 | const coverImageKey = `pdf-cover-${generatedImage.id}.jpg`; 45 | 46 | const uploadedFile = await awsS3Client.send( 47 | new PutObjectCommand({ 48 | Bucket: process.env.S3_UPLOAD_BUCKET || "", 49 | Key: coverImageKey, 50 | Body: imageBuffer, 51 | ContentType: "image/jpeg", 52 | }), 53 | ); 54 | 55 | if (!uploadedFile) { 56 | throw new Error("Failed to upload enhanced image to S3"); 57 | } 58 | 59 | return Response.json({ 60 | url: `https://${process.env.S3_UPLOAD_BUCKET}.s3.${ 61 | process.env.S3_UPLOAD_REGION || "us-east-1" 62 | }.amazonaws.com/${coverImageKey}`, 63 | } as ImageGenerationResponse); 64 | } 65 | 66 | export const runtime = "edge"; 67 | -------------------------------------------------------------------------------- /src/app/api/s3-upload/route.ts: -------------------------------------------------------------------------------- 1 | export { POST } from "next-s3-upload/route"; 2 | -------------------------------------------------------------------------------- /src/app/api/summarize/route.ts: -------------------------------------------------------------------------------- 1 | import { togetheraiClient } from "@/lib/ai"; 2 | import assert from "assert"; 3 | import dedent from "dedent"; 4 | import { z } from "zod"; 5 | import { generateObject } from "ai"; 6 | 7 | export async function POST(req: Request) { 8 | const { text, language } = await req.json(); 9 | 10 | assert.ok(typeof text === "string"); 11 | assert.ok(typeof language === "string"); 12 | 13 | const systemPrompt = dedent` 14 | You are an expert at summarizing text. 15 | 16 | Your task: 17 | 1. Read the document excerpt I will provide 18 | 2. Create a concise summary in ${language} 19 | 3. Generate a short, descriptive title in ${language} 20 | 21 | Guidelines for the summary: 22 | - Format the summary in HTML 23 | - Use

tags for paragraphs (2-3 sentences each) 24 | - Use