Prompt Anything
A modular and customizable framework to build prompts of any kind (such as ones within the console)! Originally inspired by the need to create console-like prompts in other applications such as chatting with bots.
npm install prompt-anything
Table of Contents
Implementation
- The following interfaces should be implemented:
- The
Prompt
class must be extended to implement the abstract methods:createCollector
- Returns an event emitter that should also emitmessage
whenever your collector gets a messageonReject
- HandlesRejection
errors (see the rejecting input section)
- Your collector should stop when the emitter emits
stop
. - You may optionally emit the
exit
event for the user to prematurely exit
Usage
See the examples/console.ts
for a functioning implementation that accepts input from the console.
Creating a Prompt
A prompt is composed of two parts:
VisualInterface|VisualGenerator
- A object or function that determines how the prompt looks like to the userPromptFunction
- An (ideally pure) function that runs on every input from your collector
// Data type that is passed to each prompt // askNameFn is run on every message collected during this prompt. This should be a pure function. (see below for details)// Third argument is the optional PromptCondition
The PromptFunction
should be pure function to
- Minimize side effects that can affect every other function that depends on the data.
- Simplify unit-testing
As a result, the function should always be referencing the original data variable passed from the previous prompt, regardless of how many times the function is run.
Conditional Visuals
If you want a prompt's visual to be dependent on the given data, you can pass a function as the argument of a Prompt
instead of an object.
Rejecting Input
To reject input, you can check the the content of the message in PromptFunction
, and throw a Errors.Rejection
. Upon throwing it:
- The rejection's message will be sent via your channel implementation's
send
method - The prompt will again wait for input
- Run the prompt function again
Skipping Message Collection
To skip message collecting and only send a prompt's visual (usually done at the end of prompts), simply leave the second argument of Prompt
as undefined
.
Time Limits/Timeouts
To automatically end message collection after a set duration, pass your duration in milliseconds as the 3rd argument to Prompt
.
This causes a Errors.UserInactivityError
to be thrown when the timeout is reached. The default value is 90000.
Connecting Prompts
To connect prompts, you must put them into nodes and connect nodes together by setting their children. This allows prompts to be reused by attaching children to nodes instead of prompts.
askNameNode.addChildaskAgeNodeaskAgeNode.addChildaskLocationNode
Conditional Nodes
If you only want a node to run if it matches a condition (given the data from the previous prompt node), you can specify a condition function PromptNodeCondition
as the second argument of a PromptNode
.
// After we ask for the location, we'd like to send a prompt in a different language based on their input askNameNode.addChildaskAgeNodeaskAgeNode.addChildaskLocationNode// addChild can be daisy-chainedaskLocationNode .addChildenglishAskNode .addChildspanishAskNode// setChildren also worksaskLocationNode.setChildren
The order of the children matters. The first child that matches its condition based on the given data will run. In this example, if englishAskPrompt
's condition function returns true
, then spanishAskNode
will never run.
Running Prompts
After your prompt nodes are created, create a PromptRunner
that is initialized with the data you'll be passing to the first prompt, then call its run method with the first prompt node.
// The initial data that is given to the first prompt is passed to the PromptRunner's constructor // run resolves with the data returned from the last prompt// askName -> askAge -> askLocation -> (englishAsk OR spanishAsk)// lastPromptData is the data returned from either englishAsk or spanishAsk
You can also run an array of prompt nodes. The first node that either has no condition, or has a matching condition will be passd to the run
method.
// runArray resolves with the data returned from the last prompt// (askSurname OR askName) -> askAge -> askLocation -> (englishAsk OR spanishAsk)
Error Handling
Any error that throws within prompts will cause the PromptRunner
's run
to reject. In addition to regular errors, it may throw
Errors.UserVoluntaryExitError
if you emitexit
increateCollector
Errors.UserInactivityError
if timeout occurs (90000 ms by default)
Both are instances of Errors.UserError
.
try catch err
Testing
Unit testing is straightforward since the tree of responses is built up from individual prompts that can be exported for testing. The prompts can be further decomposed into their visual, functional and conditional parts for even more granular tests.
Integration testing can be asserted on the execution order of the phases. Unfortunately, a "flush promises" method must be used since we cannot normally await
the promises while we are waiting for messages from EventEmitter
, otherwise the promise would never resolve until the series of prompts has ended.
it'runs correctly for age <= 20',