Pro Tips

Building a file form filler in React

Oct 14, 2025

Building a smart form filler just got simpler. With the Fency SDK, React Hook Form, and Uppy, you can create an intelligent form experience that automatically suggests values for each field based on the content of an uploaded file. Instead of wiring up complex input logic and file parsing manually, this setup uses Fency.ai to analyze the file and generate real-time suggestions that seamlessly integrate into your existing form workflow.

This guide walks you through setting up a React + TypeScript project where Uppy handles file uploads, React Hook Form manages validation and input state, and the Fency SDK powers context-aware field suggestions — all styled with TailwindCSS for a modern, responsive UI.

Here’s an example of what your smart form suggestions will look like:

1. Create a new Vite project

Start with a fresh React + TypeScript project:

npm create vite@latest file-form-filler -- --template react-ts
cd file-form-filler
npm install
npm

You should now see the default Vite app running at http://localhost:5173.

2. Add TailwindCSS

Install Tailwind and its Vite plugin:

npm

Import tailwind at the very top of your index.css:

@import "tailwindcss";

3. Configure Vite for Tailwind

Install @types/node to enable path and __dirname.

npm install -D @types/node

Update vite.config.ts so Tailwind and path aliases work:

import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

4. Install Zod and the and Hookform resolvers for schema validation

npm

5. Install React Hook Form for form handling

npm

6. Install Uppy for managing file uploads

npm

7. Install Mantine as a component library

npm

Install PostCSS plugins and postcss-preset-mantine:

npm install --save-dev postcss postcss-preset-mantine postcss-simple-vars

Create a postcss.config.cjs file at the root of your application with the following content:

module.exports = {
  plugins: {
    'postcss-preset-mantine': {},
    'postcss-simple-vars': {
      variables: {
        'mantine-breakpoint-xs': '36em',
        'mantine-breakpoint-sm': '48em',
        'mantine-breakpoint-md': '62em',
        'mantine-breakpoint-lg': '75em',
        'mantine-breakpoint-xl': '88em'

Add styles imports and MantineProvider your application root component (usually main.tsx):

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import '@mantine/core/styles.css'
import { MantineProvider } from '@mantine/core'

createRoot(document.getElementById('root')!).render(
    <StrictMode>
        <MantineProvider>
            <App />
        </MantineProvider>
    </StrictMode>
)

8. Add Fency.ai

We’ll use Fency to utilize LLMs in React. If you haven’t already:

  1. Sign up at app.fency.ai/signup

  2. Create a new publishable key in the dashboard

Install the npm packages:

npm

Update main.tsx to include the FencyProvider and your newly created publishable key.

import { loadFency } from '@fencyai/js'
import { FencyProvider } from '@fencyai/react'
import { MantineProvider } from '@mantine/core'
import '@mantine/core/styles.css'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

const fency = loadFency({ publishableKey: 'fency_pk_replace_with_your_own' })

createRoot(document.getElementById('root')!).render(
    <StrictMode>
        <MantineProvider>
            <FencyProvider fency={fency}>
                <App />
            </FencyProvider>
        </MantineProvider>
    </StrictMode>
)

9. Build the App

Update your App.tsx to include a simple app that uploads a file and suggests values for the fields in your form:

import { useFiles, useStructuredChatCompletions } from '@fencyai/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { Alert, Button, TextInput } from '@mantine/core'
import AwsS3 from '@uppy/aws-s3'
import Uppy from '@uppy/core'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
import { Dashboard } from '@uppy/react'
import { useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

const formSchema = z.object({
    companyName: z.string(),
    email: z.string(),
    address: z.string(),
})

const suggestionsSchema = z.object({
    companyNames: z.array(z.string()),
    emails: z.array(z.string()),
    fullAddresses: z.array(z.string()),
})

type AppState =
    | 'waiting_for_file'
    | 'getting_file_text_content'
    | 'getting_suggestions'
    | 'suggestions_received'

export default function App() {
    const chatCompletions = useStructuredChatCompletions()
    const [formSubmitted, setFormSubmitted] = useState(false)
    const [state, setState] = useState<AppState>('waiting_for_file')
    const [suggestions, setSuggestions] = useState<z.infer<
        typeof suggestionsSchema
    > | null>(null)
    const form = useForm({
        resolver: zodResolver(formSchema),
    })
    const { createFile } = useFiles({
        async onUploadCompleted() {
            setState('getting_file_text_content')
        },
        async onTextContentReady(event) {
            setState('getting_suggestions')
            const response =
                await chatCompletions.createStructuredChatCompletion({
                    responseFormat: suggestionsSchema,
                    openai: {
                        messages: [
                            {
                                role: 'user',
                                content:
                                    'Find suggestions for the following form based on this content. Make sure to include all the relevant datapoints you can find ' +
                                    event.textContent,
                            },
                        ],
                        model: 'gpt-4.1-nano',
                    },
                })

            if (response.type === 'success') {
                setSuggestions(response.data.structuredResponse)
                setState('suggestions_received')
            }
        },
    })

    const uppy = useMemo(() => {
        const u = new Uppy({
            restrictions: {
                maxNumberOfFiles: 1,
                allowedFileTypes: ['application/pdf'],
            },
            autoProceed: false,
        })

        u.use(AwsS3, {
            getUploadParameters: async (file) => {
                if (file.size && file.name) {
                    const response = await createFile({
                        fileName: file.name,
                        fileSize: file.size,
                        fileType: file.type,
                    })

                    if (response.type !== 'success') {
                        throw Error('Could not create upload')
                    }

                    const p = response.file
                    const fields: Record<string, string> = {
                        key: p.s3PostRequest.key,
                        policy: p.s3PostRequest.policy,
                        'x-amz-algorithm': p.s3PostRequest.xAmzAlgorithm,
                        'x-amz-credential': p.s3PostRequest.xAmzCredential,
                        'x-amz-date': p.s3PostRequest.xAmzDate,
                        'x-amz-signature': p.s3PostRequest.xAmzSignature,
                        'x-amz-security-token': p.s3PostRequest.sessionToken,
                    }

                    return {
                        url: p.s3PostRequest.uploadUrl,
                        method: 'POST',
                        fields,
                        headers: {},
                    }
                } else {
                    throw Error('Filename or size is null')
                }
            },
            shouldUseMultipart: false,
        })

        u.on('error', (error, file) => {
            console.log(
                `Error occured, ${error.name} ${error.message}, ${error.details}, ${file?.error}`
            )
        })

        return u
    }, [])

    const statusMeta = getStatusMeta(state)

    return (
        <div className="w-screen h-screen">
            <div className="flex flex-col gap-2 mb-2 max-w-2xl mx-auto mt-10">
                <span className="text-gray-500 text-sm">
                    You can use this example file if you want to try it out:
                    <br />
                    <Button
                        component={'a'}
                        mt="xs"
                        size="xs"
                        radius="xl"
                        target="_blank"
                        href="https://fency-public-content.s3.eu-west-1.amazonaws.com/CloudSentinel_Company_Description.pdf"
                        download={true}
                    >
                        Example file
                    </Button>
                </span>
                <form
                    onSubmit={form.handleSubmit(() => {
                        setFormSubmitted(true)
                    })}
                >
                    <TextInput
                        label="Company Name"
                        {...form.register('companyName')}
                        error={form.formState.errors.companyName?.message}
                    />
                    <Suggestions
                        suggestions={suggestions?.companyNames || []}
                        onClick={(companyName) =>
                            form.setValue('companyName', companyName)
                        }
                    />
                    <TextInput
                        label="Email"
                        {...form.register('email')}
                        error={form.formState.errors.email?.message}
                    />
                    <Suggestions
                        suggestions={suggestions?.emails || []}
                        onClick={(email) => form.setValue('email', email)}
                    />
                    <TextInput
                        label="Address"
                        {...form.register('address')}
                        error={form.formState.errors.address?.message}
                    />

                    <Suggestions
                        suggestions={suggestions?.fullAddresses || []}
                        onClick={(address) => form.setValue('address', address)}
                    />
                    <div className="min-h-36 overflow-y-auto bg-gray-100 p-4 rounded-md mb-2 flex flex-col justify-center items-center mt-2">
                        <div className="flex flex-col justify-center items-center w-full h-full">
                            <span className="text-gray-500">
                                {statusMeta.text}
                            </span>
                        </div>
                    </div>
                </form>
                {formSubmitted && (
                    <Alert
                        variant="light"
                        color="teal"
                        title="Form submitted successfully"
                    >
                        Form submitted successfully.
                    </Alert>
                )}
                <Dashboard uppy={uppy} width={'100%'} />
            </div>
        </div>
    )
}

function Suggestions({
    suggestions,
    onClick,
}: {
    suggestions: string[]
    onClick: (suggestion: string) => void
}) {
    return (
        <div className="flex gap-1 mt-2">
            {suggestions.map((suggestion) => (
                <Suggestion
                    key={suggestion}
                    value={suggestion}
                    onClick={() => onClick(suggestion)}
                />
            ))}
        </div>
    )
}

function Suggestion({
    value,
    onClick,
}: {
    value: string
    onClick: () => void
}) {
    return (
        <Button
            color="grape"
            size="xs"
            radius={'lg'}
            className="h-2"
            onClick={onClick}
        >
            {value}
        </Button>
    )
}

const getStatusMeta = (
    state: AppState
): {
    text: string
} => {
    switch (state) {
        case 'waiting_for_file':
            return {
                text: 'Waiting for your file!',
            }
        case 'getting_suggestions':
            return {
                text: 'Getting suggestions...',
            }
        case 'suggestions_received':
            return {
                text: 'Suggestions received!',
            }
        case 'getting_file_text_content':
            return {
                text: 'Getting file text content...',
            }
    }
}

10. Try It Out 🚀

  1. Run the app with npm run dev

  2. Try out your very own file form filler!

The complete codebase is available at https://github.com/fencyai/file-form-filler-example