From 37c1cc975d7806fd6c32662a216515d2a0e3959d Mon Sep 17 00:00:00 2001 From: Eric Ciarla Date: Fri, 13 Dec 2024 16:57:33 -0500 Subject: [PATCH 1/2] Firecrawl Integration --- app/api/generate-quiz/route.ts | 36 ++++- components/MindMap.tsx | 9 +- components/PDFExtractor.tsx | 250 +++++++++++++++++++++------------ package-lock.json | 156 ++++++++++++++++++++ package.json | 1 + 5 files changed, 349 insertions(+), 103 deletions(-) diff --git a/app/api/generate-quiz/route.ts b/app/api/generate-quiz/route.ts index 8093bf4..487a27c 100644 --- a/app/api/generate-quiz/route.ts +++ b/app/api/generate-quiz/route.ts @@ -1,12 +1,33 @@ import { pdfExtractSchema } from "@/lib/schemas"; import { google } from "@ai-sdk/google"; import { streamObject } from "ai"; +import FirecrawlApp from '@mendable/firecrawl-js'; export const maxDuration = 60; export async function POST(req: Request) { - const { files } = await req.json(); - const firstFile = files[0].data; + const { files, url } = await req.json(); + + let content = ''; + + if (url) { + const app = new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY! }); + + const scrapeResponse = await app.scrapeUrl(url, { + formats: ['markdown'], + }); + + if (!scrapeResponse.success) { + throw new Error(`Failed to scrape: ${scrapeResponse.error}`); + } + + content = scrapeResponse.markdown ?? ''; + console.log(content); + } else if (files && files.length > 0) { + content = files[0]?.data ?? ''; + } else { + throw new Error('No content provided'); + } const result = await streamObject({ model: google("gemini-1.5-pro-latest"), @@ -14,19 +35,20 @@ export async function POST(req: Request) { { role: "system", content: - "You are a document analyzer. Extract the most important points from the provided PDF document. Focus on key information, main ideas, and significant details.", + "You are a document analyzer. Extract the most important points from the provided document. Focus on key information, main ideas, and significant details.", }, { role: "user", content: [ { type: "text", - text: "Please read this PDF and extract the key points. Include relevant context where helpful.", + text: url + ? "Please analyze this webpage content and extract the key points. Include relevant context where helpful." + : "Please read this PDF and extract the key points. Include relevant context where helpful.", }, { - type: "file", - data: firstFile, - mimeType: "application/pdf", + type: "text", + text: content, }, ], }, diff --git a/components/MindMap.tsx b/components/MindMap.tsx index 6f79851..ae671d9 100644 --- a/components/MindMap.tsx +++ b/components/MindMap.tsx @@ -115,14 +115,9 @@ export default function MindMap({ data, onNodeClick }: MindMapProps) {

No Mind Map Yet

- Upload a PDF to generate an interactive mind map of its key concepts. + Upload a PDF or Enter a URL to generate an interactive mind map of its key concepts.

- + ) diff --git a/components/PDFExtractor.tsx b/components/PDFExtractor.tsx index 504562b..e85c22d 100644 --- a/components/PDFExtractor.tsx +++ b/components/PDFExtractor.tsx @@ -1,83 +1,121 @@ -'use client' +"use client"; -import { useState } from "react" -import { experimental_useObject } from "ai/react" -import { toast } from "sonner" -import { FileUp, Loader2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" -import { Link } from "@/components/ui/link" -import { pdfExtractSchema, type PDFExtract } from "@/lib/schemas" +import { useState } from "react"; +import { experimental_useObject } from "ai/react"; +import { toast } from "sonner"; +import { FileUp, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Link } from "@/components/ui/link"; +import { pdfExtractSchema, type PDFExtract } from "@/lib/schemas"; interface PDFExtractorProps { onExtractComplete?: (content: PDFExtract) => void; onPartialContent?: (content: Partial) => void; } -export default function PDFExtractor({ onExtractComplete, onPartialContent }: PDFExtractorProps) { - const [files, setFiles] = useState([]) - const [isDragging, setIsDragging] = useState(false) +export default function PDFExtractor({ + onExtractComplete, + onPartialContent, +}: PDFExtractorProps) { + const [files, setFiles] = useState([]); + const [url, setUrl] = useState(""); + const [isDragging, setIsDragging] = useState(false); const { submit, object: extractedContent, - isLoading + isLoading, } = experimental_useObject({ api: "/api/generate-quiz", schema: pdfExtractSchema, initialValue: undefined, onError: (error) => { - toast.error("Failed to analyze PDF. Please try again.") - setFiles([]) + toast.error("Failed to analyze PDF. Please try again."); + setFiles([]); + setUrl(""); }, onFinish: ({ object }) => { if (object) { - onExtractComplete?.(object) - onPartialContent?.(object) + onExtractComplete?.(object); + onPartialContent?.(object); } - } - }) + }, + }); const handleFileChange = (e: React.ChangeEvent) => { - const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); if (isSafari && isDragging) { - toast.error("Safari does not support drag & drop. Please use the file picker.") - return + toast.error( + "Safari does not support drag & drop. Please use the file picker." + ); + return; } - const selectedFiles = Array.from(e.target.files || []) + const selectedFiles = Array.from(e.target.files || []); const validFiles = selectedFiles.filter( - (file) => file.type === "application/pdf" && file.size <= 5 * 1024 * 1024, - ) + (file) => file.type === "application/pdf" && file.size <= 5 * 1024 * 1024 + ); if (validFiles.length !== selectedFiles.length) { - toast.error("Only PDF files under 5MB are allowed.") + toast.error("Only PDF files under 5MB are allowed."); } - setFiles(validFiles) - } + setFiles(validFiles); + setUrl(""); + }; const encodeFileAsBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.readAsDataURL(file) - reader.onload = () => resolve(reader.result as string) - reader.onerror = (error) => reject(error) - }) - } - - const handleSubmitWithFiles = async (e: React.FormEvent) => { - e.preventDefault() - const encodedFiles = await Promise.all( - files.map(async (file) => ({ - name: file.name, - type: file.type, - data: await encodeFileAsBase64(file), - })), - ) - submit({ files: encodedFiles }) - } + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = (error) => reject(error); + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (files.length > 0) { + const encodedFiles = await Promise.all( + files.map(async (file) => ({ + name: file.name, + type: file.type, + data: await encodeFileAsBase64(file), + })) + ); + submit({ files: encodedFiles }); + } else if (url) { + try { + new URL(url); + submit({ url }); + } catch { + toast.error("Please enter a valid URL"); + return; + } + } else { + toast.error("Please upload a PDF or enter a URL."); + } + } catch (error) { + toast.error("An error occurred. Please try again."); + } + }; + + const handleUrlChange = (e: React.ChangeEvent) => { + setUrl(e.target.value); + if (e.target.value) { + setFiles([]); + } + }; return ( @@ -87,7 +125,7 @@ export default function PDFExtractor({ onExtractComplete, onPartialContent }: PD Mind Map Maker - Upload a PDF to generate a mind map using{" "} + Upload a PDF or enter a URL to generate a mind map using{" "} Google's Gemini Pro @@ -97,11 +135,14 @@ export default function PDFExtractor({ onExtractComplete, onPartialContent }: PD -
-
+
+ rounded-lg px-8 py-6 transition-colors + ${url ? 'opacity-50 pointer-events-none' : ''}`} + >

{files.length > 0 ? ( - - {files[0].name} - + {files[0].name} ) : ( Drop your PDF here (max 5 MB) or click to browse. )}

-
+
- + {extractedContent && extractedContent.keyPoints && ( -
-

KEY POINTS

-
+

+ KEY POINTS +

+
-
-

{extractedContent.title}

-
    - {extractedContent.keyPoints.reduce((acc: JSX.Element[], item, index) => { - if (item && extractedContent.keyPoints) { - if (index === 0 || item.context !== extractedContent.keyPoints[index - 1]?.context) { - acc.push( -

    - {item.context || 'General'} -

    - ) - } - - acc.push( -
  • -

    {item.point}

    -
  • - ) - } - return acc - }, [])} -
-
-
+ hover:scrollbar-thumb-gray-400" + > +
+

+ {extractedContent.title} +

+
    + {extractedContent.keyPoints.reduce( + (acc: JSX.Element[], item, index) => { + if (item && extractedContent.keyPoints) { + if ( + index === 0 || + item.context !== + extractedContent.keyPoints[index - 1]?.context + ) { + acc.push( +

    + {item.context || "General"} +

    + ); + } + + acc.push( +
  • +

    + {item.point} +

    +
  • + ); + } + return acc; + }, + [] + )} +
+
+
)}
@@ -183,5 +255,5 @@ export default function PDFExtractor({ onExtractComplete, onPartialContent }: PD )}
- ) -} \ No newline at end of file + ); +} diff --git a/package-lock.json b/package-lock.json index c773bab..184c2d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@ai-sdk/anthropic": "^0.0.56", "@ai-sdk/google": "^0.0.55", "@ai-sdk/openai": "^0.0.72", + "@mendable/firecrawl-js": "^1.9.3", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-radio-group": "^1.2.1", @@ -952,6 +953,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mendable/firecrawl-js": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@mendable/firecrawl-js/-/firecrawl-js-1.9.3.tgz", + "integrity": "sha512-xzkO11mMsvtoTKPgk3+OWeRvwSywxGR7sMkjAkC87s2I6BwDKMSTy7sEvOZEtQ2mUk96ei3TQ06pGezHVpaGcg==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.8", + "isows": "^1.0.4", + "typescript-event-target": "^1.1.1", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.0" + } + }, "node_modules/@next/env": { "version": "15.0.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.3.tgz", @@ -2810,6 +2824,12 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -2873,6 +2893,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3204,6 +3235,18 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -3496,6 +3539,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4413,6 +4465,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4437,6 +4509,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -5319,6 +5405,21 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isows": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", + "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -6467,6 +6568,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7113,6 +7235,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8278,6 +8406,12 @@ "node": ">=14.17" } }, + "node_modules/typescript-event-target": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/typescript-event-target/-/typescript-event-target-1.1.1.tgz", + "integrity": "sha512-dFSOFBKV6uwaloBCCUhxlD3Pr/P1a/tJdcmPrTXCHlEFD3faj0mztjcGn6VBAhQ0/Bdy8K3VWrrqwbt/ffsYsg==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -8681,6 +8815,28 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", diff --git a/package.json b/package.json index 244c641..3ba6bc5 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@ai-sdk/anthropic": "^0.0.56", "@ai-sdk/google": "^0.0.55", "@ai-sdk/openai": "^0.0.72", + "@mendable/firecrawl-js": "^1.9.3", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-radio-group": "^1.2.1", From 5cf871c85685bf4c5786e072725bfcba88ec7328 Mon Sep 17 00:00:00 2001 From: Eric Ciarla Date: Fri, 13 Dec 2024 16:58:33 -0500 Subject: [PATCH 2/2] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c791742..b01adbe 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ yarn install ```env GOOGLE_GENERATIVE_AI_API_KEY=your_api_key_here +FIRECRAWL_API_KEY= ``` 4. Run the development server: