!7 concept Cmd { S patterns; // for mmo_parsePattern S exampleInputs; // line-separated S cmd; // a star pattern recognized by the backend S questionsForArguments; // line-separated int index; transient MMOPattern parsedPattern; void change { parsedPattern = null; super.change(); } MMOPattern parsedPattern() { if (parsedPattern == null) parsedPattern = mmo_parsePattern(patterns); ret parsedPattern; } sS _fieldOrder = "exampleInputs patterns cmd index"; } cmodule ChatBotFrontend > DynCRUD { switchable S backendModuleLibID = "#1027443/Scenarios"; O currentAttractor; class HandleArgument implements IF1 { S cmd; LS argQuestions; new LS arguments; *() {} *(S *cmd, LS *argQuestions) {} // process an argument public S get(S arg) { arguments.add(arg); if (l(arguments) >= countAsteriskTokens(cmd)) ret sendToBackend(format_quoteAll(cmd, asObjectArray(arguments))); else ret handleQuestionWithArguments(this); } S currentQuestion() { ret or2(_get(argQuestions, l(arguments)), "Need argument"); } } runnable class Cancel { setField(currentAttractor := null); } class UndoHandler implements IF1 { public S get(Bool yes) { if (!yes) ret "OK"; // user said no ret (S) sendToBackend("undo"); } } start { dm_watchFieldAndNow backendModuleLibID(r { dm_setModuleName("Frontend for " + dm_moduleName(backend())) }); crud.multiLineFields = litset("exampleInputs", "questionsForArguments"); crud.sorter = func(Cl l) -> L { sortByField index(l) }; crud.formFixer = 48; } S handleCommand(Cmd cmd) { if (cmd == null) null; print("handleCommand " + cmd.cmd); if (match("undo after confirm", cmd.cmd)) { O undo = dm_call(backend(), 'lastUndo); if (undo == null) ret "Nothing to undo"; new WaitForAnswer_YesNo attractor; attractor.question = "Undo " + undo + "?"; setField(currentAttractor := attractor); print("Set attractor to: " + attractor); attractor.processAnswer = new UndoHandler; attractor.cancelSilently = new Cancel; ret attractor.question; } if (hasAsteriskTokens(cmd.cmd)) ret handleQuestionWithArguments( new HandleArgument(cmd.cmd, tlft(cmd.questionsForArguments))); else ret sendToBackend(cmd.cmd); } S handleQuestionWithArguments(HandleArgument handler) { new WaitForName attractor; attractor.question = handler.currentQuestion(); setField(currentAttractor := attractor); print("Set attractor to: " + attractor); attractor.processName = handler; attractor.cancelSilently = new Cancel; ret attractor.question; } S sendToBackend(S cmd) { ret (S) dm_call(backend(), 'answer, cmd); } S backend() { ret dm_require(backendModuleLibID); } afterVisualize { addComponents(crud.buttons, jbutton("Talk to me", rThread talkToMe), jPopDownButton_noText("Consistency check", rThread consistencyCheck)); } void talkToMe enter { dm_showConversationPopupForModule(); } void consistencyCheck enter { reindex(); L cmds = list(Cmd); L errors = mmo_consistencyCheck( map(cmds, cmd -> pair(cmd.exampleInputs, cmd.patterns))); if (empty(errors)) infoBox("No consistency problems"); else dm_showText(n2(errors, "consistency problem"), lines(errors)); } void reindex { int index = 1; for (Cmd cmd : conceptsSortedByField Cmd('index)) cset(cmd, index := index++); } // API void copyCommandsFromBackend() { for (S cmd : allIfQuotedPatterns(loadSnippet(beforeSlash(backendModuleLibID))) ) uniqConcept(+cmd); } S answer(S s) { if (currentAttractor != null) { print("Sending to attractor " + currentAttractor + ": " + s); S a = strOrNull(call(currentAttractor, 'answer, s)); if (a != null) { print("Attractor said: " + a); ret a; } } Cmd cmd = mmo_matchMultiWithTypos(1, filter(conceptsSortedByField Cmd('index), c -> nempty(c.patterns)), c -> c.parsedPattern(), s); if (cmd != null) ret handleCommand(cmd); ret sendToBackend(s); } }