initial commit
This commit is contained in:
83
src/components/assistant/flowEngine.js
Normal file
83
src/components/assistant/flowEngine.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// ==============================|| 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) });
|
||||
};
|
||||
Reference in New Issue
Block a user