philschmid commited on
Commit
3bb377e
·
unverified ·
1 Parent(s): 5dce6e4
Dockerfile ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.4
2
+
3
+ # Adapted from https://github.com/vercel/next.js/blob/e60a1e747c3f521fc24dfd9ee2989e13afeb0a9b/examples/with-docker/Dockerfile
4
+ # For more information, see https://nextjs.org/docs/pages/building-your-application/deploying#docker-image
5
+
6
+ FROM node:18 AS base
7
+
8
+ # Install dependencies only when needed
9
+ FROM base AS deps
10
+ WORKDIR /app
11
+
12
+ # Install dependencies based on the preferred package manager
13
+ COPY --link package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
14
+ RUN \
15
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
16
+ elif [ -f package-lock.json ]; then npm ci; \
17
+ elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
18
+ else echo "Lockfile not found." && exit 1; \
19
+ fi
20
+
21
+
22
+ # Rebuild the source code only when needed
23
+ FROM base AS builder
24
+ WORKDIR /app
25
+ COPY --from=deps --link /app/node_modules ./node_modules
26
+ COPY --link . .
27
+
28
+ ENV NEXT_TELEMETRY_DISABLED 1
29
+
30
+ RUN npm run build
31
+
32
+ # If using yarn comment out above and use below instead
33
+ # RUN yarn build
34
+
35
+ # Production image, copy all the files and run next
36
+ FROM base AS runner
37
+ WORKDIR /app
38
+
39
+ ENV NODE_ENV production
40
+ ENV NEXT_TELEMETRY_DISABLED 1
41
+
42
+ RUN \
43
+ addgroup --system --gid 1001 nodejs; \
44
+ adduser --system --uid 1001 nextjs
45
+
46
+ COPY --from=builder --link /app/public ./public
47
+
48
+ # Automatically leverage output traces to reduce image size
49
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
50
+ COPY --from=builder --link --chown=1001:1001 /app/.next/standalone ./
51
+ COPY --from=builder --link --chown=1001:1001 /app/.next/static ./.next/static
52
+
53
+ USER nextjs
54
+
55
+ EXPOSE 3000
56
+
57
+ ENV PORT 3000
58
+ ENV HOSTNAME 0.0.0.0
59
+
60
+
61
+ CMD ["node", "server.js"]
README copy.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
app/api/extract/route.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { GoogleGenerativeAI } from "@google/generative-ai";
3
+
4
+ const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
5
+ const MODEL_ID = "gemini-2.0-flash";
6
+
7
+ export async function POST(request: Request) {
8
+ try {
9
+ const formData = await request.formData();
10
+ const file = formData.get("file") as File;
11
+ const schema = JSON.parse(formData.get("schema") as string);
12
+
13
+ // Convert PDF to base64
14
+ const buffer = await file.arrayBuffer();
15
+ const base64 = Buffer.from(buffer).toString("base64");
16
+
17
+ const model = genAI.getGenerativeModel({
18
+ model: MODEL_ID,
19
+ generationConfig: {
20
+ responseMimeType: "application/json",
21
+ responseSchema: schema,
22
+ },
23
+ });
24
+
25
+ const prompt = "Extract the structured data from the following PDF file";
26
+
27
+ const result = await model.generateContent([
28
+ prompt,
29
+ {
30
+ inlineData: {
31
+ mimeType: "application/pdf",
32
+ data: base64,
33
+ },
34
+ },
35
+ ]);
36
+
37
+ const response = await result.response;
38
+ const extractedData = JSON.parse(response.text());
39
+
40
+ return NextResponse.json(extractedData);
41
+ } catch (error) {
42
+ console.error("Error extracting data:", error);
43
+ return NextResponse.json(
44
+ { error: "Failed to extract data" },
45
+ { status: 500 }
46
+ );
47
+ }
48
+ }
app/api/schema/route.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { GoogleGenerativeAI } from "@google/generative-ai";
3
+
4
+ const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
5
+ const MODEL_ID = "gemini-2.0-flash";
6
+
7
+ const META_PROMPT = `
8
+ You are a JSON Schema expert. Your task is to create JSON schema baed on the user input. The schema will be used for extra data.
9
+
10
+ You must also make sure:
11
+ - All fields in an object are set as required
12
+ - All objects must have properties defined
13
+ - Order matters! If the values are dependent or would require additional information, make sure to include the additional information in the description. Same counts for "reasoning" or "thinking" should come before the conclusion.
14
+ - $defs must be defined under the schema param
15
+ - Return only the schema JSON not more, use \`\`\`json to start and \`\`\` to end the JSON schema
16
+
17
+ Restrictions:
18
+ - You cannot use examples, if you think examples are helpful include them in the description.
19
+ - You cannot use default values, If you think default are helpful include them in the description.
20
+ - Top level cannot have a "title" property only "description"
21
+ - You cannot use $defs, directly in the schema, don't use any $defs and $ref in the schema. Directly define the schema in the properties.
22
+ - Never include a $schema
23
+ - The "type" needs to be a single value, no arrays
24
+
25
+ Guidelines:
26
+ - If the user prompt is short define a single object schema and fields based on your knowledge.
27
+ - If the user prompt is in detail about the data only use the data in the schema. Don't add more fields than the user asked for.
28
+
29
+ Examples:
30
+
31
+ Input: Cookie Recipes
32
+ Output: \`\`\`json
33
+ {
34
+ "description": "Schema for a cookie recipe, including ingredients and quantities. The 'ingredients' array lists each ingredient along with its corresponding quantity and unit of measurement. The 'instructions' array provides a step-by-step guide to preparing the cookies. The order of instructions is important.",
35
+ "type": "object",
36
+ "properties": {
37
+ "name": {
38
+ "type": "string",
39
+ "description": "The name of the cookie recipe."
40
+ },
41
+ "description": {
42
+ "type": "string",
43
+ "description": "A short description of the cookie, including taste and textures."
44
+ },
45
+ "ingredients": {
46
+ "type": "array",
47
+ "description": "A list of ingredients required for the recipe.",
48
+ "items": {
49
+ "type": "object",
50
+ "description": "An ingredient with its quantity and unit.",
51
+ "properties": {
52
+ "name": {
53
+ "type": "string",
54
+ "description": "The name of the ingredient (e.g., flour, sugar, butter)."
55
+ },
56
+ "quantity": {
57
+ "type": "number",
58
+ "description": "The amount of the ingredient needed."
59
+ },
60
+ "unit": {
61
+ "type": "string",
62
+ "description": "The unit of measurement for the ingredient (e.g., cups, grams, teaspoons). Use abbreviations like 'tsp' for teaspoon and 'tbsp' for tablespoon."
63
+ }
64
+ },
65
+ "required": [
66
+ "name",
67
+ "quantity",
68
+ "unit"
69
+ ]
70
+ }
71
+ },
72
+ "instructions": {
73
+ "type": "array",
74
+ "description": "A sequence of steps to prepare the cookie recipe. The order of instructions matters.",
75
+ "items": {
76
+ "type": "string",
77
+ "description": "A single instruction step."
78
+ }
79
+ }
80
+ },
81
+ "required": [
82
+ "name",
83
+ "description",
84
+ "ingredients",
85
+ "instructions"
86
+ ]
87
+ }
88
+ \`\`\`
89
+
90
+ Input: Book with title, author, and publication year.
91
+ Output: \`\`\`json
92
+ {
93
+ "type": "object",
94
+ "properties": {
95
+ "title": {
96
+ "type": "string",
97
+ "description": "The title of the book."
98
+ },
99
+ "author": {
100
+ "type": "string",
101
+ "description": "The author of the book."
102
+ },
103
+ "publicationYear": {
104
+ "type": "integer",
105
+ "description": "The year the book was published."
106
+ }
107
+ },
108
+ "required": [
109
+ "title",
110
+ "author",
111
+ "publicationYear"
112
+ ],
113
+ }
114
+ \`\`\`
115
+
116
+ Input: {USER_PROMPT}`.trim();
117
+
118
+ export async function POST(request: Request) {
119
+ try {
120
+ // Get the prompt from the request body
121
+ const { prompt } = await request.json();
122
+ // Get the model
123
+ const model = genAI.getGenerativeModel({ model: MODEL_ID });
124
+ // Generate the content
125
+ const result = await model.generateContent(
126
+ META_PROMPT.replace("{USER_PROMPT}", prompt)
127
+ );
128
+ // Get the response
129
+ const response = await result.response;
130
+ // Remove markdown code block markers if present
131
+ const jsonString = response
132
+ .text()
133
+ .replace(/^```json\n?/, "")
134
+ .replace(/\n?```$/, "");
135
+ // Return the schema
136
+ return NextResponse.json({ schema: JSON.parse(jsonString) });
137
+ } catch (error) {
138
+ console.error("Error generating schema:", error);
139
+ return NextResponse.json(
140
+ { error: "Failed to generate schema" },
141
+ { status: 500 }
142
+ );
143
+ }
144
+ }
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ body {
6
+ font-family: Arial, Helvetica, sans-serif;
7
+ }
8
+
9
+ @layer utilities {
10
+ .text-balance {
11
+ text-wrap: balance;
12
+ }
13
+ }
14
+
15
+ @layer base {
16
+ :root {
17
+ --background: 0 0% 100%;
18
+ --foreground: 240, 3%, 12%;
19
+ --card: 0 0% 100%;
20
+ --card-foreground: 240, 3%, 12%;
21
+ --popover: 0 0% 100%;
22
+ --popover-foreground: 240, 3%, 12%;
23
+ --primary: 214, 82%, 51%;
24
+ --primary-foreground: 0 0% 98%;
25
+ --secondary: 240 4.8% 95.9%;
26
+ --secondary-foreground: 214, 82%, 51%;
27
+ --muted: 240 4.8% 95.9%;
28
+ --muted-foreground: 240 3.8% 46.1%;
29
+ --accent: 240 4.8% 95.9%;
30
+ --accent-foreground: 214, 82%, 51%;
31
+ --destructive: 0 84.2% 60.2%;
32
+ --destructive-foreground: 0 0% 98%;
33
+ --border: 240 5.9% 90%;
34
+ --input: 240 5.9% 90%;
35
+ --ring: 214, 82%, 51%;
36
+ --radius: 0.5rem;
37
+ --chart-1: 12 76% 61%;
38
+ --chart-2: 173 58% 39%;
39
+ --chart-3: 197 37% 24%;
40
+ --chart-4: 43 74% 66%;
41
+ --chart-5: 27 87% 67%;
42
+ }
43
+
44
+ .dark {
45
+ --background: 240 3% 12%;
46
+ --foreground: 0 0% 98%;
47
+ --card: 220 7% 8%;
48
+ --card-foreground: 0 0% 98%;
49
+ --popover: 220 7% 8%;
50
+ --popover-foreground: 0 0% 98%;
51
+ --primary: 217 91% 60%;
52
+ --primary-foreground: 0 0% 98%;
53
+ --secondary: 240 3% 12%;
54
+ --secondary-foreground: 0 0% 98%;
55
+ --muted: 240 3% 12%;
56
+ --muted-foreground: 215 20.2% 65.1%;
57
+ --accent: 240 3% 12%;
58
+ --accent-foreground: 0 0% 98%;
59
+ --destructive: 0 62.8% 30.6%;
60
+ --destructive-foreground: 0 0% 98%;
61
+ --border: 225 9% 15%;
62
+ --input: 225 9% 15%;
63
+ --ring: 224 76.3% 48%;
64
+ --chart-1: 220 70% 50%;
65
+ --chart-2: 160 60% 45%;
66
+ --chart-3: 30 80% 55%;
67
+ --chart-4: 280 65% 60%;
68
+ --chart-5: 340 75% 55%;
69
+ }
70
+ }
71
+
72
+ @layer base {
73
+ * {
74
+ @apply border-border;
75
+ }
76
+ body {
77
+ @apply bg-background text-foreground;
78
+ }
79
+ }
80
+
81
+ h1,h2,h3,h4,h5,h6 {
82
+ @apply text-foreground dark:text-foreground;
83
+ }
84
+
85
+ .react-pdf__Page__canvas { display: inline-block; width: auto !important; height: 100% !important; }
86
+
app/layout.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata, Viewport } from "next";
2
+ import { Open_Sans } from "next/font/google";
3
+ import "./globals.css";
4
+ import { ThemeProviders } from "@/components/providers";
5
+
6
+ const openSans = Open_Sans({
7
+ weight: ["400", "500", "700"],
8
+ style: ["normal", "italic"],
9
+ subsets: ["latin"],
10
+ variable: "--font-open-sans",
11
+ });
12
+
13
+ export const metadata: Metadata = {
14
+ title: "PDF Extractor",
15
+ description: "Extract data from PDFs using Google DeepMind Gemini 2.0",
16
+ };
17
+
18
+ export const viewport: Viewport = {
19
+ themeColor: [
20
+ { media: "(prefers-color-scheme: light)", color: "white" },
21
+ { media: "(prefers-color-scheme: dark)", color: "black" },
22
+ ],
23
+ };
24
+
25
+ export default function RootLayout({
26
+ children,
27
+ }: Readonly<{
28
+ children: React.ReactNode;
29
+ }>) {
30
+ return (
31
+ <html lang="en" suppressHydrationWarning>
32
+ <body
33
+ className={`${openSans.className} antialiased bg-white dark:bg-slate-950`}
34
+ >
35
+ <ThemeProviders>{children}</ThemeProviders>
36
+ </body>
37
+ </html>
38
+ );
39
+ }
app/page.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { FileUpload } from "@/components/FileUpload";
4
+ import { PromptInput } from "@/components/PromptInput";
5
+ import { ResultDisplay } from "@/components/ResultDisplay";
6
+ import { FileIcon, FileText } from "lucide-react";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
+
9
+ export default function Home() {
10
+ const [prompt, setPrompt] = useState("");
11
+ const [schema, setSchema] = useState<string | null>(null);
12
+ const [file, setFile] = useState<File | null>(null);
13
+ const [result, setResult] = useState<string | null>(null);
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ const handleFileSelect = (selectedFile: File) => {
17
+ setFile(selectedFile);
18
+ };
19
+
20
+ const handlePromptSubmit = async (prompt: string) => {
21
+ try {
22
+ setLoading(true);
23
+ // First, get the JSON schema
24
+ setPrompt(prompt);
25
+ const schemaResponse = await fetch("/api/schema", {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ },
30
+ body: JSON.stringify({ prompt }),
31
+ });
32
+
33
+ const { schema } = await schemaResponse.json();
34
+
35
+ setSchema(schema);
36
+ setPrompt(prompt);
37
+ // Then, process the PDF with the schema
38
+ const formData = new FormData();
39
+ formData.append("file", file!);
40
+ formData.append("schema", JSON.stringify(schema));
41
+
42
+ const extractResponse = await fetch("/api/extract", {
43
+ method: "POST",
44
+ body: formData,
45
+ });
46
+
47
+ const data = await extractResponse.json();
48
+ setResult(data);
49
+ } catch (error) {
50
+ console.error("Error processing request:", error);
51
+ } finally {
52
+ setLoading(false);
53
+ }
54
+ };
55
+
56
+ const handleReset = () => {
57
+ setFile(null);
58
+ setResult(null);
59
+ setPrompt("");
60
+ setSchema(null);
61
+ setLoading(false);
62
+ };
63
+
64
+ return (
65
+ <main className="min-h-screen flex items-center justify-center bg-background p-8">
66
+ <Card className="w-full max-w-2xl border-0 bg-card shadow-none">
67
+ <CardHeader className="flex flex-col items-center justify-center space-y-2">
68
+ <CardTitle className="flex items-center gap-2 text-foreground">
69
+ <FileText className="w-8 h-8 text-primary" />
70
+ PDF to Structured Data
71
+ </CardTitle>
72
+ <span className="text-sm font-mono text-muted-foreground">
73
+ powered by Google DeepMind Gemini 2.0 Flash
74
+ </span>
75
+ </CardHeader>
76
+ <CardContent className="space-y-6 pt-6 w-full">
77
+ {!result && !loading ? (
78
+ <>
79
+ <FileUpload onFileSelect={handleFileSelect} />
80
+ <PromptInput onSubmit={handlePromptSubmit} file={file} />
81
+ </>
82
+ ) : loading ? (
83
+ <div
84
+ role="status"
85
+ className="flex items-center mx-auto justify-center h-56 max-w-sm bg-gray-300 rounded-lg animate-pulse dark:bg-secondary"
86
+ >
87
+ <FileIcon className="w-10 h-10 text-gray-200 dark:text-muted-foreground" />
88
+ <span className="pl-4 font-mono font-xs text-muted-foreground">
89
+ Processing...
90
+ </span>
91
+ </div>
92
+ ) : (
93
+ <ResultDisplay
94
+ result={result || ""}
95
+ schema={schema || ""}
96
+ onReset={handleReset}
97
+ />
98
+ )}
99
+ </CardContent>
100
+ </Card>
101
+ </main>
102
+ );
103
+ }
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "slate",
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
+ }
components/FileUpload.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { useDropzone } from "react-dropzone";
5
+ import { Button } from "./ui/button";
6
+ import { Upload as UploadIcon, File as FileIcon, X } from "lucide-react";
7
+ import PdfViewer from "./PdfViewer";
8
+
9
+ interface FileUploadProps {
10
+ onFileSelect: (file: File) => void;
11
+ }
12
+
13
+ export function formatFileSize(bytes: number): string {
14
+ if (bytes === 0) return "0 Bytes";
15
+ const k = 1024;
16
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
17
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
18
+ return (
19
+ Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
20
+ );
21
+ }
22
+
23
+ export function FileUpload({ onFileSelect }: FileUploadProps) {
24
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
25
+ const [file, setFile] = useState<File | null>(null);
26
+
27
+ const onDrop = useCallback(
28
+ (acceptedFiles: File[]) => {
29
+ const file = acceptedFiles[0];
30
+ setSelectedFile(file);
31
+ onFileSelect(file);
32
+ setFile(file);
33
+ },
34
+ [onFileSelect]
35
+ );
36
+
37
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
38
+ onDrop,
39
+ accept: {
40
+ "application/pdf": [".pdf"],
41
+ },
42
+ maxSize: 100 * 1024 * 1024, // 100MB
43
+ multiple: false,
44
+ });
45
+
46
+ return (
47
+ <div className={`"w-full min-h-[150px] `}>
48
+ {!selectedFile ? (
49
+ <div
50
+ {...getRootProps()}
51
+ className={`min-h-[150px] p-4 rounded-lg
52
+ ${isDragActive ? "bg-secondary/50" : "bg-secondary"}
53
+ transition-colors duration-200 ease-in-out hover:bg-secondary/50
54
+ border-2 border-dashed border-secondary
55
+ cursor-pointer flex items-center justify-center gap-4
56
+ `}
57
+ >
58
+ <input {...getInputProps()} />
59
+ <div className="flex flex-row items-center">
60
+ <UploadIcon className="w-8 h-8 text-primary mr-3 flex-shrink-0" />
61
+ <div className="">
62
+ <p className="text-sm font-medium text-foreground">
63
+ Drop your PDF here or click to browse
64
+ </p>
65
+ <p className="text-xs text-muted-foreground">
66
+ Maximum file size: 100MB
67
+ </p>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ ) : (
72
+ <div className="flex my-auto flex-row items-center p-4 rounded-lg bg-secondary">
73
+ <FileIcon className="w-8 h-8 text-primary mr-3 flex-shrink-0" />
74
+ <div className="flex-grow min-w-0">
75
+ <p className="text-sm font-medium truncate text-foreground">
76
+ {selectedFile?.name}
77
+ </p>
78
+ <p className="text-xs text-muted-foreground">
79
+ {formatFileSize(selectedFile?.size ?? 0)}
80
+ </p>
81
+ </div>
82
+ {file && <PdfViewer file={file} />}
83
+
84
+ <Button
85
+ variant="ghost"
86
+ size="icon"
87
+ onClick={() => setSelectedFile(null)}
88
+ className="flex-shrink-0 ml-2"
89
+ >
90
+ <X className="w-4 h-4" />
91
+ <span className="sr-only">Remove file</span>
92
+ </Button>
93
+ </div>
94
+ )}
95
+ </div>
96
+ );
97
+ }
components/PdfViewer.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { pdfjs, Document, Page } from "react-pdf";
5
+ import "react-pdf/dist/esm/Page/AnnotationLayer.css";
6
+ import "react-pdf/dist/esm/Page/TextLayer.css";
7
+ import { useResizeObserver } from "@wojtekmaj/react-hooks";
8
+
9
+ import type { PDFDocumentProxy, PDFPageProxy } from "pdfjs-dist";
10
+ import {
11
+ Sheet,
12
+ SheetContent,
13
+ SheetHeader,
14
+ SheetTitle,
15
+ SheetTrigger,
16
+ } from "./ui/sheet";
17
+ import { Button } from "./ui/button";
18
+
19
+ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
20
+ "pdfjs-dist/build/pdf.worker.min.mjs",
21
+ import.meta.url
22
+ ).toString();
23
+
24
+ const options = {
25
+ cMapUrl: "/cmaps/",
26
+ standardFontDataUrl: "/standard_fonts/",
27
+ };
28
+
29
+ export default function PdfViewer({ file }: { file: File }) {
30
+ const [numPages, setNumPages] = useState<number>();
31
+ const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
32
+ const [containerWidth, setContainerWidth] = useState<number>();
33
+
34
+ // Add resize observer
35
+ const onResize = useCallback<ResizeObserverCallback>((entries) => {
36
+ const [entry] = entries;
37
+ if (entry) {
38
+ setContainerWidth(entry.contentRect.width);
39
+ }
40
+ }, []);
41
+
42
+ useResizeObserver(containerRef, {}, onResize);
43
+
44
+ async function onDocumentLoadSuccess(page: PDFDocumentProxy): Promise<void> {
45
+ setNumPages(page._pdfInfo.numPages);
46
+ }
47
+
48
+ return (
49
+ <Sheet>
50
+ <SheetTrigger className="h-10 rounded-lg px-4 py-2 border-input bg-background border-2 hover:bg-accent hover:text-accent-foreground">
51
+ Preview
52
+ </SheetTrigger>
53
+ <SheetContent side="bottom">
54
+ <SheetHeader>
55
+ <SheetTitle>{file.name}</SheetTitle>
56
+ </SheetHeader>
57
+ <div ref={setContainerRef} className="max-w-2xl mx-auto mt-2">
58
+ <Document
59
+ file={file}
60
+ onLoadSuccess={onDocumentLoadSuccess}
61
+ options={options}
62
+ >
63
+ {Array.from(new Array(numPages), (_el, index) => (
64
+ <Page
65
+ key={`page_${index + 1}`}
66
+ pageNumber={index + 1}
67
+ width={containerWidth}
68
+ />
69
+ ))}
70
+ </Document>
71
+ </div>
72
+ </SheetContent>
73
+ </Sheet>
74
+ );
75
+ }
components/PromptInput.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Wand2 } from "lucide-react";
6
+ import { Textarea } from "@/components/ui/textarea";
7
+ interface PromptInputProps {
8
+ onSubmit: (prompt: string) => void;
9
+ file: File | null;
10
+ }
11
+
12
+ export function PromptInput({ onSubmit, file }: PromptInputProps) {
13
+ const [prompt, setPrompt] = useState("");
14
+
15
+ const handleSubmit = (e: React.FormEvent) => {
16
+ e.preventDefault();
17
+ if (prompt.trim()) {
18
+ onSubmit(prompt.trim());
19
+ }
20
+ };
21
+
22
+ return (
23
+ <form onSubmit={handleSubmit} className="space-y-4 rounded-lg">
24
+ <div className="space-y-2">
25
+ <p className="text-sm font-medium text-foreground">
26
+ Describe the structure and type of data you want to extract from the
27
+ PDF.
28
+ </p>
29
+ </div>
30
+
31
+ <Textarea
32
+ id="prompt"
33
+ className="min-h-[100px] border-secondary resize-none "
34
+ placeholder="Example: Extract all invoice details including invoice number, date, items, prices, and total amount..."
35
+ value={prompt}
36
+ onChange={(e) => setPrompt(e.target.value)}
37
+ />
38
+
39
+ <Button
40
+ type="submit"
41
+ disabled={!prompt.trim() || file === null}
42
+ className="w-full bg-primary hover:bg-primary/90"
43
+ >
44
+ <Wand2 className="w-4 h-4 mr-2" />
45
+ Extract Data
46
+ </Button>
47
+ </form>
48
+ );
49
+ }
components/ResultDisplay.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Braces, Copy, RotateCcw } from "lucide-react";
5
+ import { useState } from "react";
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverTrigger,
10
+ } from "@/components/ui/popover";
11
+ interface ResultDisplayProps {
12
+ result: string;
13
+ schema: string;
14
+ onReset: () => void;
15
+ }
16
+
17
+ export function ResultDisplay({ result, schema, onReset }: ResultDisplayProps) {
18
+ const [copied, setCopied] = useState(false);
19
+ const [schemaCopied, setSchemaCopied] = useState(false);
20
+
21
+ const handleCopy = () => {
22
+ navigator.clipboard.writeText(JSON.stringify(result, null, 2));
23
+ setCopied(true);
24
+ setTimeout(() => setCopied(false), 2000);
25
+ };
26
+ const handleSchemaCopy = () => {
27
+ navigator.clipboard.writeText(JSON.stringify(schema, null, 2));
28
+ setSchemaCopied(true);
29
+ setTimeout(() => setSchemaCopied(false), 2000);
30
+ };
31
+
32
+ return (
33
+ <div className="space-y-4">
34
+ <div className="flex items-center justify-between">
35
+ <h2 className="text-xl font-semibold">Extracted Data</h2>
36
+ <div className="space-x-2">
37
+ <Popover>
38
+ <PopoverTrigger>
39
+ <Button variant="outline" size="sm">
40
+ <Braces className="w-4 h-4 mr-2" />
41
+ Schema
42
+ </Button>
43
+ </PopoverTrigger>
44
+ <PopoverContent className="max-h-[500px] max-w-[700px] w-full overflow-y-auto">
45
+ <div className="relative p-4 rounded-lg bg-muted">
46
+ <Button
47
+ variant="secondary"
48
+ size="sm"
49
+ onClick={handleSchemaCopy}
50
+ className="absolute top-2 right-2"
51
+ >
52
+ <Copy className="w-4 h-4 mr-2" />
53
+ {schemaCopied ? "Copied!" : "Copy"}
54
+ </Button>
55
+ <pre className="overflow-auto">
56
+ <code className="text-xs">
57
+ {JSON.stringify(schema, null, 2)}
58
+ </code>
59
+ </pre>
60
+ </div>
61
+ </PopoverContent>
62
+ </Popover>
63
+ <Button variant="outline" size="sm" onClick={handleCopy}>
64
+ <Copy className="w-4 h-4 mr-2" />
65
+ {copied ? "Copied!" : "Copy"}
66
+ </Button>
67
+ <Button variant="outline" size="sm" onClick={onReset}>
68
+ <RotateCcw className="w-4 h-4 mr-2" />
69
+ Process Another PDF
70
+ </Button>
71
+ </div>
72
+ </div>
73
+ <pre className="p-4 rounded-lg bg-muted overflow-auto">
74
+ <code className="text-sm">{JSON.stringify(result, null, 2)}</code>
75
+ </pre>
76
+ </div>
77
+ );
78
+ }
components/providers.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { ThemeProvider } from "next-themes";
4
+ import { ReactNode } from "react";
5
+
6
+ export function ThemeProviders({ children }: { children: ReactNode }) {
7
+ return (
8
+ <ThemeProvider
9
+ attribute="class"
10
+ defaultTheme="dark"
11
+ enableSystem
12
+ disableTransitionOnChange
13
+ >
14
+ {children}
15
+ </ThemeProvider>
16
+ );
17
+ }
components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
components/ui/card.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = "CardTitle"
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLDivElement,
49
+ React.HTMLAttributes<HTMLDivElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = "CardDescription"
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ))
65
+ CardContent.displayName = "CardContent"
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = "CardFooter"
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
components/ui/input.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ className
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+ )
20
+ Input.displayName = "Input"
21
+
22
+ export { Input }
components/ui/popover.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Popover = PopoverPrimitive.Root
9
+
10
+ const PopoverTrigger = PopoverPrimitive.Trigger
11
+
12
+ const PopoverContent = React.forwardRef<
13
+ React.ElementRef<typeof PopoverPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16
+ <PopoverPrimitive.Portal>
17
+ <PopoverPrimitive.Content
18
+ ref={ref}
19
+ align={align}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ </PopoverPrimitive.Portal>
28
+ ))
29
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName
30
+
31
+ export { Popover, PopoverTrigger, PopoverContent }
components/ui/sheet.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SheetPrimitive from "@radix-ui/react-dialog"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { X } from "lucide-react"
7
+
8
+ import { cn } from "@/lib/utils"
9
+
10
+ const Sheet = SheetPrimitive.Root
11
+
12
+ const SheetTrigger = SheetPrimitive.Trigger
13
+
14
+ const SheetClose = SheetPrimitive.Close
15
+
16
+ const SheetPortal = SheetPrimitive.Portal
17
+
18
+ const SheetOverlay = React.forwardRef<
19
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <SheetPrimitive.Overlay
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ ))
31
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32
+
33
+ const sheetVariants = cva(
34
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35
+ {
36
+ variants: {
37
+ side: {
38
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39
+ bottom:
40
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42
+ right:
43
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ side: "right",
48
+ },
49
+ }
50
+ )
51
+
52
+ interface SheetContentProps
53
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54
+ VariantProps<typeof sheetVariants> {}
55
+
56
+ const SheetContent = React.forwardRef<
57
+ React.ElementRef<typeof SheetPrimitive.Content>,
58
+ SheetContentProps
59
+ >(({ side = "right", className, children, ...props }, ref) => (
60
+ <SheetPortal>
61
+ <SheetOverlay />
62
+ <SheetPrimitive.Content
63
+ ref={ref}
64
+ className={cn(sheetVariants({ side }), className)}
65
+ {...props}
66
+ >
67
+ {children}
68
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
69
+ <X className="h-4 w-4" />
70
+ <span className="sr-only">Close</span>
71
+ </SheetPrimitive.Close>
72
+ </SheetPrimitive.Content>
73
+ </SheetPortal>
74
+ ))
75
+ SheetContent.displayName = SheetPrimitive.Content.displayName
76
+
77
+ const SheetHeader = ({
78
+ className,
79
+ ...props
80
+ }: React.HTMLAttributes<HTMLDivElement>) => (
81
+ <div
82
+ className={cn(
83
+ "flex flex-col space-y-2 text-center sm:text-left",
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ )
89
+ SheetHeader.displayName = "SheetHeader"
90
+
91
+ const SheetFooter = ({
92
+ className,
93
+ ...props
94
+ }: React.HTMLAttributes<HTMLDivElement>) => (
95
+ <div
96
+ className={cn(
97
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ )
103
+ SheetFooter.displayName = "SheetFooter"
104
+
105
+ const SheetTitle = React.forwardRef<
106
+ React.ElementRef<typeof SheetPrimitive.Title>,
107
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
108
+ >(({ className, ...props }, ref) => (
109
+ <SheetPrimitive.Title
110
+ ref={ref}
111
+ className={cn("text-lg font-semibold text-foreground", className)}
112
+ {...props}
113
+ />
114
+ ))
115
+ SheetTitle.displayName = SheetPrimitive.Title.displayName
116
+
117
+ const SheetDescription = React.forwardRef<
118
+ React.ElementRef<typeof SheetPrimitive.Description>,
119
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
120
+ >(({ className, ...props }, ref) => (
121
+ <SheetPrimitive.Description
122
+ ref={ref}
123
+ className={cn("text-sm text-muted-foreground", className)}
124
+ {...props}
125
+ />
126
+ ))
127
+ SheetDescription.displayName = SheetPrimitive.Description.displayName
128
+
129
+ export {
130
+ Sheet,
131
+ SheetPortal,
132
+ SheetOverlay,
133
+ SheetTrigger,
134
+ SheetClose,
135
+ SheetContent,
136
+ SheetHeader,
137
+ SheetFooter,
138
+ SheetTitle,
139
+ SheetDescription,
140
+ }
components/ui/textarea.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Textarea = React.forwardRef<
6
+ HTMLTextAreaElement,
7
+ React.ComponentProps<"textarea">
8
+ >(({ className, ...props }, ref) => {
9
+ return (
10
+ <textarea
11
+ className={cn(
12
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
13
+ className
14
+ )}
15
+ ref={ref}
16
+ {...props}
17
+ />
18
+ )
19
+ })
20
+ Textarea.displayName = "Textarea"
21
+
22
+ export { Textarea }
empty-module.ts ADDED
File without changes
eslint.config.mjs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { FlatCompat } from "@eslint/eslintrc";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const compat = new FlatCompat({
9
+ baseDirectory: __dirname,
10
+ });
11
+
12
+ const eslintConfig = [
13
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
14
+ ];
15
+
16
+ export default eslintConfig;
lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
next.config.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ experimental: {
5
+ turbo: {
6
+ resolveAlias: {
7
+ canvas: "./empty-module.ts",
8
+ },
9
+ },
10
+ },
11
+ };
12
+
13
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nextjs-gemini-2-0-pdf-structured-data",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --turbopack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@google/generative-ai": "^0.21.0",
13
+ "@radix-ui/react-dialog": "^1.1.6",
14
+ "@radix-ui/react-popover": "^1.1.6",
15
+ "@radix-ui/react-slot": "^1.1.2",
16
+ "@wojtekmaj/react-hooks": "^1.22.0",
17
+ "class-variance-authority": "^0.7.1",
18
+ "clsx": "^2.1.1",
19
+ "lucide-react": "^0.475.0",
20
+ "next": "15.1.0",
21
+ "next-themes": "^0.4.4",
22
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0",
24
+ "react-dropzone": "^14.3.5",
25
+ "react-pdf": "^9.2.1",
26
+ "tailwind-merge": "^3.0.1",
27
+ "tailwindcss-animate": "^1.0.7"
28
+ },
29
+ "devDependencies": {
30
+ "@eslint/eslintrc": "^3",
31
+ "@types/node": "^20",
32
+ "@types/react": "^19",
33
+ "@types/react-dom": "^19",
34
+ "eslint": "^9",
35
+ "eslint-config-next": "15.1.0",
36
+ "postcss": "^8",
37
+ "prettier-eslint": "^16.3.0",
38
+ "tailwindcss": "^3.4.1",
39
+ "typescript": "^5"
40
+ }
41
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
tailwind.config.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+
3
+ export default {
4
+ darkMode: ["class"],
5
+ content: [
6
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
8
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
9
+ ],
10
+ theme: {
11
+ extend: {
12
+ colors: {
13
+ background: "hsl(var(--background))",
14
+ foreground: "hsl(var(--foreground))",
15
+ card: {
16
+ DEFAULT: "hsl(var(--card))",
17
+ foreground: "hsl(var(--card-foreground))",
18
+ },
19
+ popover: {
20
+ DEFAULT: "hsl(var(--popover))",
21
+ foreground: "hsl(var(--popover-foreground))",
22
+ },
23
+ primary: {
24
+ DEFAULT: "hsl(var(--primary))",
25
+ foreground: "hsl(var(--primary-foreground))",
26
+ },
27
+ secondary: {
28
+ DEFAULT: "hsl(var(--secondary))",
29
+ foreground: "hsl(var(--secondary-foreground))",
30
+ },
31
+ muted: {
32
+ DEFAULT: "hsl(var(--muted))",
33
+ foreground: "hsl(var(--muted-foreground))",
34
+ },
35
+ accent: {
36
+ DEFAULT: "hsl(var(--accent))",
37
+ foreground: "hsl(var(--accent-foreground))",
38
+ },
39
+ destructive: {
40
+ DEFAULT: "hsl(var(--destructive))",
41
+ foreground: "hsl(var(--destructive-foreground))",
42
+ },
43
+ border: "hsl(var(--border))",
44
+ input: "hsl(var(--input))",
45
+ ring: "hsl(var(--ring))",
46
+ chart: {
47
+ "1": "hsl(var(--chart-1))",
48
+ "2": "hsl(var(--chart-2))",
49
+ "3": "hsl(var(--chart-3))",
50
+ "4": "hsl(var(--chart-4))",
51
+ "5": "hsl(var(--chart-5))",
52
+ },
53
+ },
54
+ borderRadius: {
55
+ lg: "var(--radius)",
56
+ md: "calc(var(--radius) - 2px)",
57
+ sm: "calc(var(--radius) - 4px)",
58
+ },
59
+ },
60
+ },
61
+ plugins: [require("tailwindcss-animate")],
62
+ } satisfies Config;
tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }