---
title: "How to build an AI legal assistant in an hour with Nutrient Document Authoring"
canonical_url: "https://www.nutrient.io/blog/ai-legal-assistant-document-authoring/"
md_url: "https://www.nutrient.io/blog/ai-legal-assistant-document-authoring.md"
last_updated: "2026-07-10T06:08:04.770Z"
description: "Build a minimal AI legal assistant that reviews a real NDA in the browser. Every AI edit lands as a tracked change with a comment explaining it, so counsel reviews suggestions instead of trusting a black box."
---

**TL;DR**

In about an hour, you can build an AI assistant that edits a real contract with Nutrient Document Authoring. What makes it usable for legal work is how those edits are made:

- **Targeted edits.** The model works through document tools that change specific text, so it edits the clause you asked about and leaves the rest of the contract alone, instead of regenerating the whole document from a prompt and hoping the wording and formatting survive.

- **Tracked changes you can review anywhere.** Every AI edit is a real tracked change. Counsel accepts or rejects each one, and since it’s a standard DOCX revision, the same review works after you export to Word.

- **Each edit comes with a reason.** The assistant attaches a comment to every change saying what it did and why. The comment is saved in the document itself, so the reasoning is still there when someone reopens the file next week.

**Call to Action**

Explore Nutrient Document Authoring

[Learn More](https://www.nutrient.io/sdk/document-authoring/)

Most “AI for contracts” demos drop a chat box next to a PDF and call it done. That’s fine until a lawyer asks the obvious question: What exactly did it change, and can I see it before it’s final? This tutorial builds the version that answers those questions. The assistant reads a non-disclosure agreement (NDA), suggests edits as tracked changes, and writes a short comment explaining each edit.

We use [Nutrient Document Authoring](https://www.nutrient.io/sdk/document-authoring/), which gives you a DOCX-capable editor in the browser, plus [`@nutrient-sdk/document-authoring-ai`](https://www.npmjs.com/package/@nutrient-sdk/document-authoring-ai), which exposes the document to a model as a set of tools. The model loop runs through the [Vercel AI SDK](https://ai-sdk.dev/docs/introduction).

Think of this as an AI document assistant you own. It brings the in-document AI editing people expect from an Office suite, without the lock-in: It isn’t tied to Microsoft Office, and it isn’t tied to one AI vendor. Point it at any model the Vercel AI SDK supports, including a private or self-hosted one, so sensitive contract text can stay inside your own infrastructure. The files stay standard DOCX that open in Word.

## What you’ll build

The app has two panes: a chat panel on the left, the contract on the right. Ask a question, and the assistant reads the document to answer it. Ask for a change, and it proposes a tracked edit with a comment, like the Effective Date edit in the header image above.

## Before you start

You’ll need:

- A Next.js app (`npx create-next-app@latest`, App Router, TypeScript).

- An OpenAI API key in `.env.local` as `OPENAI_API_KEY`.

- A contract to work on. We use the [Common Paper Mutual NDA](https://commonpaper.com/standards/mutual-nda/) DOCX (released under CC BY 4.0). Download it, rename it to `sample.docx`, and drop it in `public/`. Any DOCX works.

## 1. Install

```sh

npm install @nutrient-sdk/document-authoring @nutrient-sdk/document-authoring-ai \
  ai@^6 @ai-sdk/openai@^3 @ai-sdk/react@^3

```

`@nutrient-sdk/document-authoring` is the editor itself. `@nutrient-sdk/document-authoring-ai` is the AI layer, and you use it in two places: on the server, to tell the model which document tools exist; and in the browser, to run the tool calls the model makes. It’s all one package. The browser code is imported from `@nutrient-sdk/document-authoring-ai/editor`.

The remaining packages are the [Vercel AI SDK](https://ai-sdk.dev/docs/introduction): `ai` plus `@ai-sdk/openai` for the model, and `@ai-sdk/react` for the chat hook. The major versions shown above keep the examples on the AI SDK 6 API. It runs the model loop and connects the model’s tool calls to your browser code.

## 2. Configure Next.js for the editor

Document Authoring serves its engine from Nutrient’s content delivery network (CDN) and loads it in the browser at runtime, so you point webpack at that CDN build instead of bundling it:

```ts

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  webpack(config, { isServer }) {
    if (!isServer) {
      config.externals.push({
        "@nutrient-sdk/document-authoring":
          "script DocAuth@https://document-authoring.cdn.nutrient.io/releases/document-authoring-1.17.0-umd.js",
      });
      config.output.environment = {...config.output.environment,
        asyncFunction: true,
      };
    }
    return config;
  },
};

export default nextConfig;

```

Run the `dev` and `build` scripts with the `--webpack` flag so this configuration applies:

```json

{
  "scripts": {
    "dev": "next dev --webpack",
    "build": "next build --webpack"
  }
}

```

## 3. The server route

The route tells the model which document tools it can call and streams its responses back. The tools don’t run on the server; the browser runs them in the next step. The `reviewComments: "create"` option is what turns on the explanatory comments: It requires the model to attach a short note to every edit it makes. In the next step, the browser turns each of those notes into a comment in the document.

```ts

// src/app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import {
  getAiPromptGuide,
  getAiToolDefinitions,
} from "@nutrient-sdk/document-authoring-ai";
import { toVercelAiTools } from "@nutrient-sdk/document-authoring-ai/vercel";

const tools = toVercelAiTools(getAiToolDefinitions({ reviewComments: "create" }));
const system = `${getAiPromptGuide()}

Include a short reviewComment on every write tool call that explains the change for the reviewer.`;

export async function POST(req: Request) {
  const body = await req.json();

  const result = streamText({
    model: openai("gpt-5.4-mini"),
    system,
    messages: await convertToModelMessages(body.messages),
    stopWhen: stepCountIs(20),
    tools,
  });

  return result.toUIMessageStreamResponse();
}

```

Any model the Vercel AI SDK supports works here. We use `gpt-5.4-mini`.

## 4. The assistant component

This is where the document lives and where tool calls execute. Two parts matter.

First, the editor lifecycle: Create the system, import the NDA, set the editor to Review mode so writes become tracked changes, set an author (it shows on the changes and comments), and bind the toolkit.

Second, `onToolCall`: Every tool the model calls arrives here. Read tools run as-is. Write tools run with `writeMode: "track_changes"` and `reviewComments: "create"`, which is what turns an edit into a tracked change plus a comment thread.

```tsx

// src/components/legal-assistant.tsx
"use client";

import { useChat } from "@ai-sdk/react";
import {
  DefaultChatTransport,
  lastAssistantMessageIsCompleteWithToolCalls,
} from "ai";
import { useEffect, useRef, useState } from "react";
import { isAiWriteToolName } from "@nutrient-sdk/document-authoring-ai";
import {
  getAiToolkit,
  type AiToolkit,
} from "@nutrient-sdk/document-authoring-ai/editor";
import {
  createDocAuthSystem,
  type DocAuthEditor,
  type DocAuthSystem,
} from "@nutrient-sdk/document-authoring";

export function LegalAssistant() {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const editorRef = useRef<DocAuthEditor | null>(null);
  const toolkitRef = useRef<AiToolkit | null>(null);
  const [ready, setReady] = useState(false);
  const [input, setInput] = useState("");

  // Create the editor once, load the NDA, and bind the AI toolkit to it.
  useEffect(() => {
    let system: DocAuthSystem | null = null;
    let editor: DocAuthEditor | null = null;
    let toolkit: AiToolkit | null = null;
    let disposed = false;

    const init = async () => {
      const host = hostRef.current;
      if (!host) return;

      system = await createDocAuthSystem();
      const document = await system.import(fetch("/sample.docx"), {
        fileName: "sample.docx",
      });
      editor = await system.createEditor(host, { document });
      editor.setEditorMode("review"); // edits arrive as tracked changes
      editor.setAuthor("AI Assistant"); // author shown on changes and comments
      toolkit = getAiToolkit(editor);

      if (disposed) {
        toolkit.dispose();
        editor.destroy();
        system.destroy();
        return;
      }
      editorRef.current = editor;
      toolkitRef.current = toolkit;
      setReady(true);
    };

    void init();

    return () => {
      disposed = true;
      toolkit?.dispose();
      editor?.destroy();
      system?.destroy();
      editorRef.current = null;
      toolkitRef.current = null;
    };
  }, []);

  const { messages, sendMessage, addToolOutput, status } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
    async onToolCall({ toolCall }) {
      const editor = editorRef.current;
      const toolkit = toolkitRef.current;
      if (!editor ||!toolkit) {
        addToolOutput({
          tool: toolCall.toolName,
          toolCallId: toolCall.toolCallId,
          state: "output-error",
          errorText: "The editor is still loading. Try again in a moment.",
        });
        return;
      }

      const isWriteTool = isAiWriteToolName(toolCall.toolName);
      try {
        const executed = await toolkit.executeTool(
          {
            id: toolCall.toolCallId,
            name: toolCall.toolName,
            args: toolCall.input,
          },
          isWriteTool? { writeMode: "track_changes", reviewComments: "create" }
            : undefined,
        );
        addToolOutput({
          tool: toolCall.toolName,
          toolCallId: toolCall.toolCallId,
          output: executed,
        });
      } catch (error) {
        addToolOutput({
          tool: toolCall.toolName,
          toolCallId: toolCall.toolCallId,
          state: "output-error",
          errorText: error instanceof Error? error.message : "Tool failed.",
        });
      }
    },
  });

  const busy = status === "submitted" || status === "streaming";

  const handleSubmit = (event: React.FormEvent) => {
    event.preventDefault();
    if (!input.trim() || busy ||!ready) return;
    sendMessage({ text: input });
    setInput("");
  };

  return (
    <main className="layout">
      <section className="chat">
        <div className="feed">
          {messages.map((message) =>
            message.parts.map((part, index) => {
              const key = `${message.id}-${index}`;
              if (part.type === "text" && part.text) {
                return (
                  <p key={key} className={`bubble ${message.role}`}>
                    <strong>
                      {message.role === "user"? "You" : "Assistant"}:
                    </strong>{" "}
                    {part.text}
                  </p>
                );
              }
              if (part.type.startsWith("tool-") && "state" in part) {
                const failed = part.state === "output-error";
                const done = part.state === "output-available";
                return (
                  <p key={key} className={`bubble tool ${failed? "failed" : ""}`}>
                    {failed? "⚠ " : done? "✓ " : "… "}
                    {part.type.slice("tool-".length)}
                    {failed && part.errorText? ` — ${part.errorText}` : ""}
                  </p>
                );
              }
              return null;
            }),
          )}
        </div>
        <form onSubmit={handleSubmit}>
          <textarea
            value={input}
            onChange={(event) => setInput(event.target.value)}
            placeholder="Ask about the NDA, or request an edit..."
            rows={3}
            disabled={!ready || busy}
          />
          <button type="submit" disabled={!ready || busy}>
            {busy? "Working..." : "Send"}
          </button>
        </form>
      </section>
      <div ref={hostRef} className="editor" />
    </main>
  );
}

```

`isAiWriteToolName` tells you whether a tool call mutates the document. That’s the one decision your app makes: Read tools always run; write tools run as tracked changes here because the editor is in Review mode.

## 5. Wire it together

Add a page to render the component, a root layout, and the styles. The editor fills its container, so the layout gives it a full-height column.

```tsx

// src/app/page.tsx
import { LegalAssistant } from "../components/legal-assistant";

export default function Page() {
  return <LegalAssistant />;
}

```

```tsx

// src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "AI Legal Assistant",
  description: "An AI assistant that reviews and edits an NDA in the browser.",
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

```css

/* src/app/globals.css */

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  font-family: system-ui, sans-serif;
}.layout {
  display: grid;
  grid-template-columns: 360px 1fr;
  grid-template-rows: minmax(0, 1fr);
  height: 100vh;
  min-height: 0;
  overflow: hidden;
}.chat {
  display: flex;
  flex-direction: column;
  border-right: 1px solid #e2e2e2;

  min-height: 0;
  overflow: hidden;
}.feed {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}.bubble {
  margin: 0 0 12px;
  padding: 8px 12px;
  border-radius: 8px;
  background: #f4f4f5;

  white-space: pre-wrap;
}.bubble.user {
  background: #e7f0ff;

}.bubble.tool {
  font-family: ui-monospace, monospace;
  font-size: 13px;
  color: #555;

}.bubble.tool.failed {
  color: #b42318;

  background: #fef3f2;

}

form {
  display: flex;
  gap: 8px;
  padding: 12px;
  border-top: 1px solid #e2e2e2;

}

textarea {
  flex: 1;
  resize: none;
  padding: 8px;
  border: 1px solid #ccc;

  border-radius: 6px;
  font: inherit;
}

button {
  padding: 8px 16px;
  border: 0;
  border-radius: 6px;
  background: #111;

  color: #fff;

  cursor: pointer;
}

button:disabled {
  opacity: 0.5;
  cursor: default;
}.editor {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 0;
  overflow: hidden;
  background: #eef2f7;

}

```

## 6. Run it

```sh

npm run dev

```

Open the app and wait for the NDA to load. Try:

> Set the Cover Page effective date to 25 June 2026.

The assistant reads the document to find the right field and then proposes the edit. In the editor, you’ll see the old placeholder struck through, the new date inserted, and a comment from AI Assistant explaining the change. Accept or reject it like any tracked change.

Here are a few more prompts worth trying:

- _List every bracketed blank that must be filled in before this NDA can be signed._

- _Change the Term of Confidentiality to three years from the date of last disclosure._

- _Who is allowed to receive Confidential Information under the Standard Terms?_

## Tracked changes and comments for a legal team

For a legal team, “the AI edited the contract” is a liability unless someone can see and approve each change before it’s final. That’s why the assistant runs in Review mode.

Every edit it makes shows up as a tracked change — the same as if a colleague had suggested it. A reviewer reads each one in context and accepts or rejects it, and nothing is final until a person says so. Each change also carries the assistant’s comment explaining what it did and why.

Together, the tracked changes and comments leave an [audit trail](https://todaysgeneralcounsel.com/contract-audit-trails-help-meet-compliance-and-legal-requirements/) of what the AI proposed and the reasoning behind it. That record stays in the file and opens in Microsoft Word, which is what a legal team reaches for during a compliance review or a dispute, instead of reconstructing the history from email and memory.

Your app, not the model, decides how edits land: applied directly, or held for review as tracked changes. The [review and approval](https://www.nutrient.io/guides/document-authoring/ai/review-and-approval.md) guide covers the Edit, Review, and View modes in full.

## FAQ

#### Do the tracked changes and comments work in Microsoft Word?

Yes. When you export the document to DOCX, the AI’s tracked changes and comments are written as standard Word revisions and comments. A reviewer can open the file in Word and accept, reject, or reply to them like any other markup.

#### Where do the AI’s edits go?

In Review mode, they become tracked changes a person accepts or rejects. In Edit mode, they apply directly. The editor mode is set by your code in `editor.setEditorMode`, not by the model.

#### What exactly is an AI comment?

It’s a normal document comment, the same kind a person would leave on a clause. It holds the model’s explanation for the edit, and because it lives in the document, the reasoning stays attached to the change.

#### Can I keep contract data off third-party model providers?

Yes. The assistant isn’t tied to OpenAI. The Vercel AI SDK works with any provider, including a private or self-hosted model, so sensitive contract text can stay inside your own infrastructure. Swap the provider in the server route; the rest of the code stays the same.

#### Do I need a license to try this?

No — you don’t need one just to try it. Without a key, Document Authoring runs in evaluation mode: You can import and export DOCX, tracked changes and comments included, but documents carry an “Evaluation Copy” watermark and evaluation is time-limited. DOCX support is a licensed capability, so for production, you add a license key to `createDocAuthSystem` to remove the watermark. [Contact Sales](https://www.nutrient.io/contact-sales/) for a key.

## Where to go next

Where you take it next depends on the job. To control what the model can change, limit which tools it can call so it only edits the clauses you allow. When the task is fixed rather than open-ended — like proofreading or translating a contract — a structured workflow is a better fit than the chat loop. And to ship this for real, point the editor at your own templates and wire it to your authentication. The guides below cover each scenario.

- [Quick start: Document Authoring AI](https://www.nutrient.io/guides/document-authoring/ai/quick-start.md) — The smallest agentic loop, start to finish.

- [Agentic tools](https://www.nutrient.io/guides/document-authoring/ai/agentic-tools.md) — The full tool reference and element ID rules.

- [Vercel AI SDK integration](https://www.nutrient.io/guides/document-authoring/ai/integrations/vercel-ai-sdk.md) — The chat loop and edge cases.

- [Document Authoring AI example on GitHub](https://github.com/PSPDFKit/nutrient-document-authoring-ai-example) — The full app this tutorial is based on, with proofreading, translation, and a template builder.

**Call to Action**

Get started with Nutrient Document Authoring

[Learn More](https://www.nutrient.io/sdk/document-authoring/getting-started.md)
---

## Related pages

- [Accessibility Untangled Why It Matters Guide](/blog/accessibility-untangled-why-it-matters-guide.md)
- [The business case for accessibility: Five ways it drives enterprise value](/blog/5-ways-accessibility-drives-enterprise-value.md)
- [Advanced Techniques For React Native Ui Components](/blog/advanced-techniques-for-react-native-ui-components.md)
- [Angular File Viewer Pdf Image Office Files](/blog/angular-file-viewer-pdf-image-office-files.md)
- [Ai Document Automation Extraction To Action](/blog/ai-document-automation-extraction-to-action.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [Convert One Drive Files To Pdf In Sharepoint](/blog/convert-one-drive-files-to-pdf-in-sharepoint.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [Construction Document Data Extraction](/blog/construction-document-data-extraction.md)
- [Create And Edit Pdfs In Flutter](/blog/create-and-edit-pdfs-in-flutter.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Creating A Document Scanner With Ocr In Python](/blog/creating-a-document-scanner-with-ocr-in-python.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Digital Workflow Automation](/blog/digital-workflow-automation.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [Document Viewer](/blog/document-viewer.md)
- [app.py](/blog/extract-text-from-pdf-using-python.md)
- [How To Build A Dotnet Maui Pdf Viewer](/blog/how-to-build-a-dotnet-maui-pdf-viewer.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [How To Build A Flutter Pdf Viewer](/blog/how-to-build-a-flutter-pdf-viewer.md)
- [How To Build A Javascript Pdf Viewer](/blog/how-to-build-a-javascript-pdf-viewer.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [How To Build A React Powerpoint Viewer](/blog/how-to-build-a-react-powerpoint-viewer.md)
- [or](/blog/how-to-build-a-nextjs-pdf-viewer.md)
- [How To Build A Powerpoint Viewer Using Javascript](/blog/how-to-build-a-powerpoint-viewer-using-javascript.md)
- [How To Build A React Native Pdf Viewer](/blog/how-to-build-a-react-native-pdf-viewer.md)
- [How To Build A Reactjs File Viewer](/blog/how-to-build-a-reactjs-file-viewer.md)
- [Using Yarn](/blog/how-to-build-a-react-excel-viewer.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [How To Build A Vuejs Pdf Viewer With Pdfjs](/blog/how-to-build-a-vuejs-pdf-viewer-with-pdfjs.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [How To Build A Vuejs Pdf Viewer](/blog/how-to-build-a-vuejs-pdf-viewer.md)
- [How To Build An Angular Pdf Viewer With Pdfjs](/blog/how-to-build-an-angular-pdf-viewer-with-pdfjs.md)
- [How To Build An Angular Pdf Viewer With Ng2 Pdf Viewer](/blog/how-to-build-an-angular-pdf-viewer-with-ng2-pdf-viewer.md)
- [How To Convert Html To Pdf Using Html2pdf](/blog/how-to-convert-html-to-pdf-using-html2pdf.md)
- [How To Build An Android Pdf Viewer](/blog/how-to-build-an-android-pdf-viewer.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [or](/blog/how-to-convert-html-to-pdf-using-react.md)
- [How To Convert Word To Pdf In Nodejs](/blog/how-to-convert-word-to-pdf-in-nodejs.md)
- [How To Create Pdfs With React To Pdf](/blog/how-to-create-pdfs-with-react-to-pdf.md)
- [How To Edit Pdfs Using Ios Pdf Library](/blog/how-to-edit-pdfs-using-ios-pdf-library.md)
- [How To Embed A Pdf Viewer In Your Website](/blog/how-to-embed-a-pdf-viewer-in-your-website.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [How To Generate Pdf From Html With Nodejs](/blog/how-to-generate-pdf-from-html-with-nodejs.md)
- [From an HTML string.](/blog/html-in-pdf-format.md)
- [Swift Package Manager](/blog/mobile-pdf-sdk.md)
- [Open an image.](/blog/how-to-use-tesseract-ocr-in-python.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [`elements` come from your document parser — each has a type and content.](/blog/multimodal-rag.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Pdfjs Annotation Editor Layer](/blog/pdfjs-annotation-editor-layer.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdfjs Area Annotations Canvas Capture](/blog/pdfjs-area-annotations-canvas-capture.md)
- [Pdfjs Coordinate Systems Pdf To Screen](/blog/pdfjs-coordinate-systems-pdf-to-screen.md)
- [Pdfjs Eventbus Guide](/blog/pdfjs-eventbus-guide.md)
- [Pdfjs Navigation Zoom Rotation](/blog/pdfjs-navigation-zoom-rotation.md)
- [Pdfjs Native Annotation Layer Forms](/blog/pdfjs-native-annotation-layer-forms.md)
- [Pdfjs Pdf Page Manipulation Pdf Lib](/blog/pdfjs-pdf-page-manipulation-pdf-lib.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs Sticky Note Annotations](/blog/pdfjs-sticky-note-annotations.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Pdfjs Server Side Text Extraction](/blog/pdfjs-server-side-text-extraction.md)
- [Pdfjs Rendering Overlays React Portals](/blog/pdfjs-rendering-overlays-react-portals.md)
- [Pdfjs Thumbnail Sidebar](/blog/pdfjs-thumbnail-sidebar.md)
- [Pdfjs Text Highlight Annotations](/blog/pdfjs-text-highlight-annotations.md)
- [Pdfjs Text Search Pdffindcontroller](/blog/pdfjs-text-search-pdffindcontroller.md)
- [Process Flows](/blog/process-flows.md)
- [or](/blog/sample-blog-updated.md)
- [React Native Pdf Annotation](/blog/react-native-pdf-annotation.md)
- [Open an image file.](/blog/tesseract-python-guide.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [Top 5 Javascript Pdf Viewers](/blog/top-5-javascript-pdf-viewers.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [Why Pdfium Is A Trusted Platform For Pdf Rendering](/blog/why-pdfium-is-a-trusted-platform-for-pdf-rendering.md)

