ai-native-form
Version:
AI-powered React form assistant with GPT-4, speech input, and schema auto-generation.
151 lines (150 loc) • 5.87 kB
JavaScript
import { useFormContext, useController, useWatch, } from "react-hook-form";
import { useRef, useState } from "react";
import { useAIFormContext } from "../context/AIFormProvider";
export function useAIForm(form, apiKeyOverride) {
const { apiKey: keyFromContext, defaultPrompt } = useAIFormContext();
const apiKey = apiKeyOverride ?? keyFromContext;
let resolvedForm;
try {
resolvedForm = form ?? useFormContext();
}
catch {
throw new Error("❌ useAIForm: No form context or object provided.");
}
const { register: baseRegister, setValue, handleSubmit, control, ...rest } = resolvedForm;
const formValues = useWatch({ control });
const schemaRef = useRef({ type: "object", properties: {}, required: [] });
const getSchema = () => schemaRef.current;
const [isLoading, setIsLoading] = useState(false);
const hasSchema = Object.keys(schemaRef.current.properties).length > 0;
function register(name, options = {}) {
const { fillRequired = false } = options;
const fieldType = options.type ?? "string";
const schemaType = fieldType === "radio" || fieldType === "select" ? "string" : fieldType;
const prop = { type: schemaType };
if (options.pattern)
prop.pattern = options.pattern.source;
if (options.enum)
prop.enum = options.enum;
if (options.min !== undefined)
prop.minimum = options.min;
if (options.max !== undefined)
prop.maximum = options.max;
if (options.format)
prop.format = options.format;
if (fieldType === "array") {
prop.items = { type: options.itemType ?? "string", enum: options.enum };
}
schemaRef.current.properties[name] = prop;
if (fillRequired && !schemaRef.current.required.includes(name)) {
schemaRef.current.required.push(name);
}
if (fieldType === "array") {
const baseProps = baseRegister(name, options);
return {
...baseProps,
checked: Array.isArray(formValues[name])
? formValues[name].includes(baseProps.value)
: false,
onChange: (e) => {
const val = e.target.value;
const cur = formValues[name] || [];
const next = e.target.checked
? [...cur, val]
: cur.filter((v) => v !== val);
setValue(name, next, { shouldValidate: true });
},
};
}
if (fieldType === "radio" || fieldType === "select") {
const { field } = useController({
name,
control,
defaultValue: options.enum && options.enum.length
? options.enum[0]
: "",
});
return field;
}
if (fieldType === "number") {
const opts = {
required: options.required,
valueAsNumber: true,
min: options.min,
max: options.max,
};
return baseRegister(name, opts);
}
const opts = {
required: options.required,
pattern: options.pattern,
};
return baseRegister(name, opts);
}
const fillFromPrompt = async (promptArg, schema) => {
const prompt = promptArg ?? defaultPrompt;
if (!prompt?.trim()) {
throw new Error("❌ useAIForm: No prompt provided.");
}
// Validate API key for Latin-1 characters
if (!apiKey || /[^\x00-\xFF]/.test(apiKey)) {
throw new Error("❌ useAIForm: Invalid or missing API key.");
}
const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.set("Authorization", `Bearer ${apiKey}`);
setIsLoading(true);
try {
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers,
body: JSON.stringify({
model: "gpt-4-1106-preview",
messages: [
{ role: "system", content: "You fill form fields from natural language prompts." },
{ role: "user", content: prompt },
],
tools: [
{
type: "function",
function: {
name: "fillForm",
description: "Fills out a form based on user input.",
parameters: schema ?? getSchema(),
},
},
],
tool_choice: { type: "function", function: { name: "fillForm" } },
}),
});
const result = await res.json();
const argsRaw = result.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments;
if (!argsRaw) {
alert("⚠️ GPT did not return valid form data.");
console.warn("Unexpected GPT response:", result);
return;
}
const args = JSON.parse(argsRaw);
Object.entries(args).forEach(([key, value]) => {
setValue(key, value);
});
}
catch (err) {
console.error("OpenAI Error:", err);
throw err;
}
finally {
setIsLoading(false);
}
};
return {
...rest,
...resolvedForm,
register,
handleSubmit,
getSchema,
fillFromPrompt,
isLoading,
hasSchema,
};
}