Creating an intelligent web-powered form assistant is easier than ever. With the Fency SDK, you can extract structured insights from any given website and use them to suggest accurate values for your form fields automatically. Instead of manually copying and pasting information, Fency.ai analyzes the site’s content in real time and provides smart, context-aware recommendations that fit naturally into your form workflow.
This guide walks you through building a React + TypeScript project where the Fency SDK fetches and interprets web data, then integrates it with React Hook Form for dynamic suggestions — all beautifully styled using TailwindCSS.
Here’s an example of what your AI-powered form suggestions will look like:

1. Create a new Vite project
Start with a fresh React + TypeScript project:
npm create vite@latest website-data-extractor -- --template react-ts
cd website-data-extractor
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:
Import tailwind at the very top of your index.css
:
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
5. Install React Hook Form for form handling
6. Install Mantine as a component library
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>
)
7. Add Fency.ai
We’ll use Fency to utilize LLMs in React. If you haven’t already:
Sign up at app.fency.ai/signup
Create a new publishable key in the dashboard
Install the npm packages:
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>
)
8. Build the App
Update your App.tsx
to include a simple app that extracts the content of a website and shows suggestions for the form fields.
import { useStructuredChatCompletions, useWebsites } from '@fencyai/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { Alert, Button, Loader, TextInput } from '@mantine/core'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
const linkSchema = z.object({
link: z.string().min(1).url(),
})
const formSchema = z.object({
companyName: z.string(),
companyOrganizationNumber: z.string(),
companyFullAddress: z.string(),
})
type ExampleState =
| 'waiting_for_url'
| 'getting_website_content'
| 'getting_suggestions'
| 'suggestions_received'
| 'filling_form_error'
export default function App() {
const [suggestions, setSuggestions] = useState<z.infer<
typeof formSchema
> | null>(null)
const [formsState, setFormsState] =
useState<ExampleState>('waiting_for_url')
const { createWebsite } = useWebsites({
async onTextContentReady(event) {
setFormsState('getting_suggestions')
console.log(event.textContent)
const companyFormResponse =
await chatCompletions.createStructuredChatCompletion({
responseFormat: formSchema,
gemini: {
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: 'gemini-2.5-flash-lite-preview-06-17',
},
})
if (companyFormResponse.type === 'success') {
setSuggestions(companyFormResponse.data.structuredResponse)
setFormsState('suggestions_received')
} else {
setFormsState('filling_form_error')
}
},
})
const chatCompletions = useStructuredChatCompletions()
const [formSubmitted, setFormSubmitted] = useState(false)
const companyFormData = useForm({
resolver: zodResolver(formSchema),
})
const linkForm = useForm({
resolver: zodResolver(linkSchema),
defaultValues: {
link: 'https://www.allabolag.se/foretag/spotify-ab/stockholm/datacenters/5567037485',
},
})
const submitForm = async (values: z.infer<typeof linkSchema>) => {
setFormsState('getting_website_content')
await createWebsite({
url: values.link,
})
}
const isLoading =
formsState !== 'waiting_for_url' &&
formsState !== 'suggestions_received'
return (
<div className="w-screen h-screen bg-gray-100 pt-10">
<div className="flex flex-col gap-2 mb-2 max-w-2xl mx-auto bg-white p-4 rounded-lg">
<form onSubmit={linkForm.handleSubmit(submitForm)}>
<TextInput
label="Link"
{...linkForm.register('link')}
error={linkForm.formState.errors.link?.message}
/>
<div className="flex justify-end pt-2">
<Button
type="submit"
leftSection={
isLoading ? <Loader size="xs" /> : undefined
}
disabled={isLoading}
>
{getStateMeta(formsState).title}
</Button>
</div>
</form>
<form
onSubmit={companyFormData.handleSubmit(() => {
setFormSubmitted(true)
})}
>
<TextInput
label="Company Name"
{...companyFormData.register('companyName')}
error={
companyFormData.formState.errors.companyName
?.message
}
/>
<Suggestions
suggestions={
suggestions?.companyName
? [suggestions.companyName]
: []
}
onClick={(companyName) =>
companyFormData.setValue('companyName', companyName)
}
/>
<TextInput
label="Company Organization Number"
{...companyFormData.register(
'companyOrganizationNumber'
)}
error={
companyFormData.formState.errors
.companyOrganizationNumber?.message
}
/>
<Suggestions
suggestions={
suggestions?.companyOrganizationNumber
? [suggestions.companyOrganizationNumber]
: []
}
onClick={(companyOrganizationNumber) =>
companyFormData.setValue(
'companyOrganizationNumber',
companyOrganizationNumber
)
}
/>
<TextInput
label="Company Full Address"
{...companyFormData.register('companyFullAddress')}
error={
companyFormData.formState.errors.companyFullAddress
?.message
}
/>
<Suggestions
suggestions={
suggestions?.companyFullAddress
? [suggestions.companyFullAddress]
: []
}
onClick={(companyFullAddress) =>
companyFormData.setValue(
'companyFullAddress',
companyFullAddress
)
}
/>
</form>
{formSubmitted && (
<Alert
variant="light"
color="teal"
title="Form submitted successfully"
>
Form submitted successfully.
</Alert>
)}
</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 getStateMeta = (
state: ExampleState
): {
title: string
} => {
switch (state) {
case 'waiting_for_url':
return {
title: 'Get suggestions',
}
case 'getting_website_content':
return {
title: 'Getting website content...',
}
case 'getting_suggestions':
return {
title: 'Getting suggestions...',
}
case 'suggestions_received':
return {
title: 'Suggestions received!',
}
case 'filling_form_error':
return {
title: 'Error filling form!',
}
}
}
9. Try It Out 🚀
Run the app with npm run dev
Click “Create Chat Completion”.
Watch the LLM response stream in, beautifully rendered with markdown, code blocks, and formatting thanks to <Response>
.
The complete codebase is available at https://github.com/fencyai/website-data-extractor-example