init
Browse files- Dockerfile +61 -0
- README copy.md +36 -0
- app/api/extract/route.ts +48 -0
- app/api/schema/route.ts +144 -0
- app/favicon.ico +0 -0
- app/globals.css +86 -0
- app/layout.tsx +39 -0
- app/page.tsx +103 -0
- components.json +21 -0
- components/FileUpload.tsx +97 -0
- components/PdfViewer.tsx +75 -0
- components/PromptInput.tsx +49 -0
- components/ResultDisplay.tsx +78 -0
- components/providers.tsx +17 -0
- components/ui/button.tsx +56 -0
- components/ui/card.tsx +79 -0
- components/ui/input.tsx +22 -0
- components/ui/popover.tsx +31 -0
- components/ui/sheet.tsx +140 -0
- components/ui/textarea.tsx +22 -0
- empty-module.ts +0 -0
- eslint.config.mjs +16 -0
- lib/utils.ts +6 -0
- next.config.ts +13 -0
- package-lock.json +0 -0
- package.json +41 -0
- postcss.config.mjs +8 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- public/window.svg +1 -0
- tailwind.config.ts +62 -0
- tsconfig.json +27 -0
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 |
+
}
|