UNPKG

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
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, }; }