From e84e5cc4779d675383a894d7533f2729c34fc4bf Mon Sep 17 00:00:00 2001 From: Jack Merrill Date: Tue, 15 Apr 2025 17:12:18 -0400 Subject: [PATCH] Add VSCode settings, update dependencies, and enhance document processing with image handling --- .vscode/extensions.json | 3 + .vscode/settings.json | 22 ++ app/dashboard/documents/[id]/page.tsx | 10 +- app/dashboard/upload/process/route.ts | 127 +++++++++- app/layout.tsx | 2 + bun.lockb | Bin 195241 -> 196712 bytes components/UploadZone.tsx | 119 +++++++--- components/nav-actions.tsx | 9 + components/ui/sonner.tsx | 25 ++ package.json | 5 +- supabase/.gitignore | 8 + supabase/config.toml | 308 ++++++++++++++++++++++++ supabase/functions/process-document.ts | 285 ++++++++++++++++++++++ utils/supabase/types.ts | 317 +++++++++++++------------ 14 files changed, 1043 insertions(+), 197 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 components/ui/sonner.tsx create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml create mode 100644 supabase/functions/process-document.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..74baffc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["denoland.vscode-deno"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0dccffe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "deno.enablePaths": ["supabase/functions"], + "deno.lint": true, + "deno.unstable": [ + "bare-node-builtins", + "byonm", + "sloppy-imports", + "unsafe-proto", + "webgpu", + "broadcast-channel", + "worker-options", + "cron", + "kv", + "ffi", + "fs", + "http", + "net" + ], + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/app/dashboard/documents/[id]/page.tsx b/app/dashboard/documents/[id]/page.tsx index 1884fd7..9e5b507 100644 --- a/app/dashboard/documents/[id]/page.tsx +++ b/app/dashboard/documents/[id]/page.tsx @@ -25,11 +25,7 @@ import { redirect } from "next/navigation"; import { remark } from "remark"; import remarkHtml from "remark-html"; -export default async function DocumentPage({ - params, -}: { - params: { id: string }; -}) { +export default async function DocumentPage(props: { params: { id: string } }) { const supabase = await createClient(); const { @@ -40,11 +36,13 @@ export default async function DocumentPage({ return redirect("/login"); } + const { id } = await props.params; + // Fetch the document details based on the ID from params const { data: document, error } = await supabase .from("documents") .select("*") - .eq("id", params.id) + .eq("id", id) .single(); if (error || !document) { diff --git a/app/dashboard/upload/process/route.ts b/app/dashboard/upload/process/route.ts index 76e1dd7..634cbfe 100644 --- a/app/dashboard/upload/process/route.ts +++ b/app/dashboard/upload/process/route.ts @@ -2,10 +2,22 @@ import { createClient } from "@/utils/supabase/server"; import { NextResponse } from "next/server"; import { Mistral } from "@mistralai/mistralai"; import { redirect } from "next/navigation"; +import { ChatCompletionChoice } from "@mistralai/mistralai/models/components"; +import pLimit from "p-limit"; const apiKey = process.env.MISTRAL_API_KEY; const client = new Mistral({ apiKey: apiKey }); +const PROCESSING_PROMPT = ` +You are a document processing AI. Your task is to process the Markdown text scanned from a document page and return it in a clean and structured format. + +The textual page data should only be returned in valid Markdown format. Use proper headings and subheadings to structure the content. +Any images should be included. +Do not return the Markdown as a code block, only as a raw string, without any new lines. + +The Markdown should be human-readable and well-formatted. +`; + export async function POST(request: Request) { const supabase = await createClient(); const formData = await request.formData(); @@ -33,15 +45,126 @@ export async function POST(request: Request) { }, }); + const limit = pLimit(1); // Limit to 1 concurrent request (adjust as needed) + + const promises: Promise[] = []; + + for (const page of ocrResponse.pages) { + const pagePromise = limit(async () => { + const response = await client.chat.complete({ + model: "mistral-small-latest", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: PROCESSING_PROMPT, + }, + ], + }, + ], + }); + + if (!response.choices) { + console.error("No choices in response"); + return; + } + + const imageData: { [key: string]: string } = {}; + + if (page.images.length > 0) { + for (const img of page.images) { + imageData[img.id] = img.imageBase64!; + } + } + + if (response.choices[0].message.content) { + const markdown = replaceImagesInMarkdown( + response.choices[0].message.content.toString(), + imageData + ); + + return { + ...page, + markdown, + }; + } else { + console.error("Message content is undefined"); + } + }); + + promises.push(pagePromise); + } + + const results = await Promise.all(promises); + const sortedResults = results.sort((a, b) => a.index - b.index); + const { data, error } = await supabase .from("documents") .update({ - ocr_data: ocrResponse, + ocr_data: sortedResults, }) .eq("id", id); if (error) { console.error(error); return NextResponse.json({ error: error.message }, { status: 500 }); } - return redirect(`/dashboard/documents/${id}`); // Redirect to the document page after processing + return NextResponse.json({ + id, + }); +} + +interface OCRResponse { + pages: { + markdown: string; + images: { id: string; image_base64: string }[]; + }[]; +} + +function replaceImagesInMarkdown( + markdownStr: string, + imagesDict: { [key: string]: string } +): string { + /** + * Replace image placeholders in markdown with base64-encoded images. + * + * Args: + * markdownStr: Markdown text containing image placeholders + * imagesDict: Dictionary mapping image IDs to base64 strings + * + * Returns: + * Markdown text with images replaced by base64 data + */ + for (const [imgName, base64Str] of Object.entries(imagesDict)) { + markdownStr = markdownStr.replace( + new RegExp(`!\\[${imgName}\\]\\(${imgName}\\)`, "g"), + `![${imgName}](${base64Str})` + ); + } + return markdownStr; +} + +function getCombinedMarkdown(ocrResponse: OCRResponse): string { + /** + * Combine OCR text and images into a single markdown document. + * + * Args: + * ocrResponse: Response from OCR processing containing text and images + * + * Returns: + * Combined markdown string with embedded images + */ + const markdowns: string[] = []; + // Extract images from page + for (const page of ocrResponse.pages) { + const imageData: { [key: string]: string } = {}; + for (const img of page.images) { + imageData[img.id] = img.image_base64; + } + // Replace image placeholders with actual images + markdowns.push(replaceImagesInMarkdown(page.markdown, imageData)); + } + + return markdowns.join("\n\n"); } diff --git a/app/layout.tsx b/app/layout.tsx index c805869..430e63b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,6 +7,7 @@ import { Geist } from "next/font/google"; import { ThemeProvider } from "next-themes"; import Link from "next/link"; import "./globals.css"; +import { Toaster } from "@/components/ui/sonner"; const defaultUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` @@ -38,6 +39,7 @@ export default function RootLayout({ disableTransitionOnChange > {children} + diff --git a/bun.lockb b/bun.lockb index c9a33ab49e7ddb64ad295c2cd822fd12fe50cbea..da4b3e2e8364d6e82331ab971a745616f2aa3836 100755 GIT binary patch delta 36741 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�K$m76n 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 diff --git a/components/UploadZone.tsx b/components/UploadZone.tsx index 7897eef..2e3c8a2 100644 --- a/components/UploadZone.tsx +++ b/components/UploadZone.tsx @@ -1,49 +1,83 @@ "use client"; import { createClient } from "@/utils/supabase/client"; -import { CloudUpload } from "lucide-react"; +import { CloudUpload, LoaderCircle } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { SSE } from "sse.js"; export default function UploadZone({ user }: { user?: { id: string } }) { const supabase = createClient(); + const [uploading, setUploading] = useState(false); + const [status, setStatus] = useState(""); const onUpload = async (file: File) => { - const uuid = crypto.randomUUID(); + setUploading(true); + setStatus("Uploading..."); - const { data: fileData, error: fileError } = await supabase.storage - .from("documents") - .upload(`${user!.id}/${uuid}.pdf`, file); - - if (fileError) { - console.error(fileError); - return; - } - - console.log("File uploaded successfully:", fileData); - - const { data, error } = await supabase.from("documents").insert({ - id: uuid, - file_name: file.name, - owner: user!.id, - raw_file: fileData.id, - }); + const { data, error } = await supabase.auth.getSession(); if (error) { - console.error(error); + toast.error("Failed to get user session."); + setUploading(false); return; } - console.log("Document inserted successfully:", data); + const body = new FormData(); + body.append("file", file); + body.append("jwt", data.session?.access_token || ""); - // process file at /dashboard/upload/process - const formData = new FormData(); - formData.append("file", file); - formData.append("fileName", file.name); - formData.append("id", uuid); - const response = await fetch("/dashboard/upload/process", { - method: "POST", - body: formData, + const edgeFunctionUrl = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/process-document`; + + // Start listening to the SSE stream + const eventSource = new SSE(edgeFunctionUrl, { + payload: body, + headers: { + apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`, + "Content-Type": "application/json", + }, }); - const result = await response.json(); - console.log("File processed successfully:", result); + + eventSource.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log("SSE Message:", data); + + if (data.message) { + setStatus(data.message); + } + }; + + eventSource.addEventListener("status", (event) => { + const data = JSON.parse(event.data); + console.log("Status Event:", data); + + setStatus(data.message); + }); + + eventSource.addEventListener("error", (event) => { + console.error("SSE Error:", event); + toast.error("An error occurred while processing the document."); + setUploading(false); + eventSource.close(); + }); + + eventSource.addEventListener("complete", (event) => { + const data = JSON.parse(event.data); + console.log("Processing Complete:", data); + toast.success("Document processing complete!"); + setUploading(false); + eventSource.close(); + }); + + // Invoke the serverless function + supabase.functions.invoke("process-document", { + body, + method: "POST", + }); + + toast.info( + "Document is being processed in the background. You will be notified when it's ready." + ); }; return ( @@ -53,13 +87,22 @@ export default function UploadZone({ user }: { user?: { id: string } }) { htmlFor="dropzone-file" className="flex flex-col items-center justify-center w-full h-64 border-2 border-muted border-dashed rounded-lg cursor-pointer bg-muted/50" > -
- -

- Click to upload or drag and - drop -

-
+ {uploading ? ( +
+ +

{status}

+
+ ) : ( + <> +
+ +

+ Click to upload or drag + and drop +

+
+ + )} Edit Oct 08 */} + + + + + +