import React, { createContext, useContext, useLayoutEffect, useState, } from "react"; /* * InputProvider * * Provides all asset URLs, s3 URLs, and taskObject input variables to the frontend * by querying for hidden elements in the DOM with attributes that contain * the input data. * * This provider works regardless of if we're in a HITL environment (ie an A2I or * GT job) or running the frontend locally for development by parsing local overrides * from the data-local attribute. */ type InputMap = { [key: string]: any; }; const InputContext = createContext({} as InputMap); const getAttrOrFail = (el: Element, attr: string): string => { const attrVal = el.getAttribute(attr); if (!attrVal) { throw new Error(`${attr} must be specified on ${el.outerHTML}`); } return attrVal; }; export const queryInputs = async (doc: HTMLDocument, isHitl: boolean) => { const inputs: InputMap = {}; Array.from(doc.querySelectorAll("input.asset")).forEach((el) => { const name = getAttrOrFail(el, "data-name"); const src = getAttrOrFail(el, "data-src"); inputs[name] = src; }); Array.from(doc.querySelectorAll("input.s3-file")).forEach((el) => { const name = getAttrOrFail(el, "data-name"); inputs[name] = isHitl ? getAttrOrFail(el, "data-src") : getAttrOrFail(el, "data-local"); }); for (const el of Array.from(doc.querySelectorAll("input.json-var"))) { const name = getAttrOrFail(el, "data-name"); if (isHitl) { const srcAttr = getAttrOrFail(el, "data-src"); try { const parsed = JSON.parse(srcAttr); inputs[name] = parsed; continue; } catch (e) { throw new Error(`${srcAttr} can't be parsed as JSON: ${e}`); } } const localAttr = getAttrOrFail(el, "data-local"); // LocalAttr is either valid literal JSON or a path to a file with valid JSON. try { const parsed = JSON.parse(localAttr); inputs[name] = parsed; continue; } catch (e) { // Do nothing, this field must be a file path. } // LocalAttr must be a URL to JSON since the previous try failed. const res = await fetch(localAttr); const parsedJson = await res.json(); inputs[name] = parsedJson; } return inputs; }; const InputProvider: React.FC = (props) => { const [inputs, setInputs] = useState({}); // Wait until the dom has fully loaded before trying to query inputs from the DOM. useLayoutEffect(() => { const runEffect = async () => { const doc = window.document; const isHitl = Boolean(process.env.REACT_APP_HITL); const queriedInputs = await queryInputs(doc, isHitl); setInputs(queriedInputs); }; runEffect(); }, []); return ( {props.children} ); }; export const useInput = (name: string) => { return useContext(InputContext)[name]; }; export default InputProvider;