Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add question screen docs #33

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion QuestionScreen.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
# QuestionScreen
# Introducing the screens concept

From now on, the screens concept will be around when talking about the interaction system we have designed using Botman. And, precisely because we are using Botman, a screen is an special type of question, extending the Botman question, designed to wrap a field question and control the interaction of the user with the question.

As we think of this screens concept, it comes to mind that it is not specific for the implementation of pagination, we already have implementend some interactive features that work as separate from the process of actually answering a question, and that is absolutely right. It just happens that this is when we actually need it, and it's a must have in order to keep consistency, simplicity and logic.

As a consequence of this implementation, some refactoring (not reinventing the wheel here, keep calm) is expected. Things like "Show more info", displaying errors and showing hints are going to be lifted from the conversation level to the screen level.
leninpaulino marked this conversation as resolved.
Show resolved Hide resolved

Here's how the different levels would look:

[https://whimsical.com/7VzPWjnvBo7q8XjJ2tgZ9L](https://whimsical.com/7VzPWjnvBo7q8XjJ2tgZ9L)

# Explaining the three levels of interaction

And here is how each level will work:

## **Survey Conversation level:**

Has the survey and fields data. Creates question objects and pass them to the screens. It communicates with screens only.

- It gets the text content from the screen and passes it to botman.
- When the user sends something, it takes the answer from botman and passes it to the screen.
- If the screen indicates that it should be sent back again to the user, the question screen is repeated with the content returned it's new content (maybe is the next/previous page, or hints, or errors, we'll never know at this level). This step will repeat until the question screen indicates otherwise, meaning that there will not be more interaction with the user for that question.
- After that, the flow will continue as it is already designed.

## **Question Screen level:**

Everything the user see, read, send or interact with have to go through this level. Things like getting the payload from the field question can be done by calling the methods on the the field question itself.

- It gets the field question label, instructions, options, hints, errors, etc from the field question and paginates accordingly by taking into consideration things like the max characters limit stablished and the amount of characters reserved to include navigation options.
- It intercepts all the answers passed from the survey conversation to verify if the user is trying to navigate or interact with the question before answering. If the input sent is not recognized as a known trigger and there is not any other condition to prevent it, the answer will be passed to the field question for evaluation.
- It will use the the field question to determine which options to include, for example: Skip question, Show more info, etc

## **Field Question Level:**

This is where translations, validations, and everything else happens. In other words, anything field-specific or related to the field data happens at this level.


# The QuestionScreen class

In order to separate concerns between asnwering or choosing a option and interacting and navigating through the content of a question, we created the Question Screen. Which basically is a Botman Question wrapping our FieldQuestions. This allow us to pass the question screens to the botman bot, as questions, and handle all user interactions for the question in context.


The question screen takes all the text pieces from the FieldQuestion and paginate them, adding navigation options also.

**Important**:
This does not apply for transitions between questions or steps.
87 changes: 87 additions & 0 deletions app/Messages/Outgoing/Screen/AbstractScreen.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ abstract class AbstractScreen extends Question
*/
protected $done = false;

/**
* Indicates if the screen should have the cancel option.
*
* @var bool
*/
protected $includeCancelOption;

/**
* Indicates if the screen was canceled by the user.
*
* @var bool
*/
protected $canceled = false;

public function __construct(bool $includeCancelOption = true)
Expand All @@ -31,15 +41,45 @@ public function __construct(bool $includeCancelOption = true)
parent::__construct($this->getText());
}

/**
* {@inheritdoc}
*
* Returns current page's text as the text Botman should show
* when asking the question
*/
public function getText(): string
{
return $this->currentPage->getText();
}

/**
* Generates the initial page for the screen.
* If the text content does not fit in a page, considering the limited
* amount of characters, the page will automatically create and save the
* reference to the next page, this happens recursively.
*
* @return Page
*/
abstract public function buildInitialPage(): Page;

/**
* Implement how the screen will handle an answer from Botman.
*
* It could pass the answer to another question or ignore it,
* it will depend on the actual implementation.
*
* @param Answer $answer
* @return void
*/
abstract public function setAnswer(Answer $answer): void;

/**
* Indicates if within a conversation context this screen is done,
* meaning that if it is, the conversation can move forward on the flow.
* If it is not, the screen is usually repeated.
*
* @return bool
*/
public function isDone(): bool
{
return $this->done;
Expand All @@ -50,17 +90,33 @@ public function wasCanceled(): bool
return $this->canceled;
}

/**
* Sets the screen as canceled and indicates to not reapat this screen again.
*
* @return void
*/
private function cancel()
{
$this->canceled = true;
$this->dontRepeatAgain();
}

/**
* Marks the screen as done.
*
* @return void
*/
public function dontRepeatAgain(): void
{
$this->done = true;
}

/**
* Returns the array of options that should be available on all the pages
* that may come out of this screen.
*
* @return array
*/
public function getDefaultScreenOptions(): array
{
$options = [];
Expand All @@ -73,6 +129,12 @@ public function getDefaultScreenOptions(): array
return $options;
}

/**
* Map each screen option to it's correspondent action.
*
* @param string $option
* @return void
*/
public function handleScreenOption(string $option): void
{
if (self::isEqualToOption($option, __('conversation.screen.next.value'))) {
Expand All @@ -86,20 +148,36 @@ public function handleScreenOption(string $option): void
}
}

/**
* Sets the current page to the next one referenced in the current page.
*
* @return void
*/
public function transitionToNextPage(): void
{
if ($this->currentPage->hasNext()) {
$this->setCurrentPage($this->currentPage->getNext());
}
}

/**
* Sets the current page to the previous page referenced in the current page.
*
* @return void
*/
public function transitionToPreviousPage(): void
{
if ($this->currentPage->hasPrevious()) {
$this->setCurrentPage($this->currentPage->getPrevious());
}
}

/**
* Creates a new page for the error message provided and set it as current page.
*
* @param string $errorMessage
* @return void
*/
public function transitionToErrorPage(string $errorMessage): void
{
$errorPage = new Page($errorMessage, [], [], $this->currentPage);
Expand All @@ -117,6 +195,15 @@ public function getCurrentPage(): Page
return $this->currentPage;
}

/**
* Compares the option chosen by the user to a previously registered option.
*
* This allows options to be case insensitive.
*
* @param string $selectedOption
* @param string $option
* @return bool
*/
public static function isEqualToOption(string $selectedOption, string $option): bool
{
return strcasecmp($selectedOption, $option) === 0;
Expand Down
10 changes: 10 additions & 0 deletions app/Messages/Outgoing/Screen/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Page
/**
* Registered options for this page's scope.
*
* Used to check is an option is supported for this page.
*
* @var array
*/
protected $pageOptions = [];
Expand Down Expand Up @@ -61,6 +63,9 @@ public function __construct(?string $text, array $screenOptions = [], array $opt
$builder->appendText($text);
$text = null;
} else {
/**
* Append as much as possible and returns the rest to use it in the other page.
*/
$text = $builder->appendExceedingText($text);
}
}
Expand All @@ -78,6 +83,9 @@ public function __construct(?string $text, array $screenOptions = [], array $opt
if ($builder->hasEnoughSpaceForText($optionText, $isTheLastPiecePendingForAttachment)) {
$builder->appendText($optionText);
} else {
/**
* Append as much as possible and returns the rest to use it in the other page.
*/
$newOptionText = $builder->appendExceedingText($optionText);
$option->text = $newOptionText;
array_unshift($options, $option);
Expand All @@ -90,7 +98,9 @@ public function __construct(?string $text, array $screenOptions = [], array $opt
$this->next = new self($text, $screenOptions, $options, $this);
}

// all the appended text, including options
$this->text = $builder->getPageContent();
// page options for this page
$this->pageOptions = $builder->getPageOptionsValues();
}

Expand Down