Files
doormilxpress_astryx/src/components/assistant/flowEngine.js
2026-08-26 15:01:15 +05:30

84 lines
3.4 KiB
JavaScript

// ==============================|| 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) });
};