// ==============================|| Doormile AI — conversational flow engine ||============================== // // // One step-walker, shared by every conversational create (orderFlow.js, // bulkFlow.js). It was written inside orderFlow and extracted when the bulk // create became a conversation too — a second copy would have been a third // definition of the same branching rules to keep in sync. // // A step is a plain object: // // id required. Also the draft key the answer lands on. // type 'select' → the panel renders a dropdown (AIFlowStep) // 'rows' → the panel renders the file/paste input // 'text' → answered through the composer // ask the question // when (draft) => boolean. Omitted means always asked. THIS is branching. // options async (draft) => [{ value, label, record? }] — for 'select' // validate (raw, option) => error | null. Re-asks; stores nothing. // resolve async (raw) => { value } | { error }. May fail and re-ask — // geocoding. A value the rest of the flow depends on is never // stored half-resolved. // auto async (draft) => { value?, ask?, patch? }. The step answers itself // from real data and is only ASKED when that fails, with the reason. // apply (draft, value, option) => draft // // A step is skipped when `when` is false OR when `draft[step.id]` is already // set — which is what lets a caller seed the draft (a client login's tenant) or // one step fill several fields (picking an existing customer). const applicable = (step, draft) => (typeof step.when === 'function' ? step.when(draft) : true); // Finds the next step that applies and hasn't been answered. Async because a // step may answer itself from the network before we know whether to ask it. export const advanceFlow = async (steps, flow) => { let { step, draft } = flow; while (step < steps.length) { const s = steps[step]; if (!applicable(s, draft) || draft[s.id] !== undefined) { step += 1; // eslint-disable-next-line no-continue continue; } if (s.auto) { // eslint-disable-next-line no-await-in-loop const auto = await s.auto(draft); if (auto?.patch) draft = { ...draft, ...auto.patch }; if (auto?.value !== undefined) { draft = s.apply(draft, auto.value); step += 1; // eslint-disable-next-line no-continue continue; } return { flow: { ...flow, step, draft }, step: s, ask: auto?.ask || s.ask, done: false }; } return { flow: { ...flow, step, draft }, step: s, done: false }; } return { flow: { ...flow, step, draft, complete: true }, done: true, draft }; }; export const startFlow = (steps, kind, draft = {}) => advanceFlow(steps, { kind, step: 0, draft }); // Applies one answer — typed text, a chosen dropdown option, or a parsed file. export const answerFlowStep = async (steps, flow, raw, option) => { const s = steps[flow.step]; if (!s) return advanceFlow(steps, flow); if (s.validate) { const error = s.validate(raw, option); if (error) return { flow, step: s, ask: error, done: false, retry: true }; } let value = raw; if (s.resolve) { const resolved = await s.resolve(raw); if (resolved.error) return { flow, step: s, ask: resolved.error, done: false, retry: true }; value = resolved.value; } return advanceFlow(steps, { ...flow, step: flow.step + 1, draft: s.apply(flow.draft, value, option) }); };