Using The CUI Kit

Background

Consumer User Interfaces (CUIs) for the Dispense application generally follow a similar flow for selecting and pouring beverages:

Beverage Selection:

  • The user is presented with a list of “brands.” These may be physical product names (e.g., “Coke” or “Sprite”) or logical groupings (e.g., Sparkling vs. Still).
  • Upon selecting a brand, the user sees the beverages associated with that brand (for example, choosing “Coke” might reveal “Cherry Coke” and “Coke Zero”).

Optional Flavor Selection:

  • For certain dispensers (e.g., Legacy+), after a beverage is chosen, the user might have the option to add one or more flavor shots (such as “Lemon” or “Cherry”).

Pouring:

  • Once a beverage (and optionally a flavor) is selected, the “pour” button becomes active.
  • With a “Hold To Pour” strategy, the user can press to start pouring and cancel if needed.

This user flow is governed by two primary state machines:

Manages which screen is displayed (brand list, beverage list, etc.) and updates the state based on user actions. For example, selecting a brand triggers a transition to the beverage selection screen and calls the nozzle’s select endpoint.

Pouring State Machine:

Manages the pouring process by tracking button presses and updating the nozzle service accordingly. It also provides real-time progress feedback.

Additional complexity arises from the need to handle presence and inactivity timers. For example, if a user remains idle on the selection screen, the UI should prompt for confirmation and eventually return to the home screen if no response is received. A similar, perhaps shorter, delay is applied after pouring.

This complexity is a common source of implementation challenges and errors when building a CUI.

The CUI Kit

The CUI Kit, part of the Dispense SDK, was developed to address the complexities of the select-and-pour workflow. It encapsulates application state and interactions into a set of finite state machines, ensuring pour integrity without forcing a specific user experience. The kit provides:

Customizable Workflows:

Supports both a Brand → Beverage selection and a Brand → Beverage + Flavor(s) selection hierarchy.

Simplified State Management:

Manages complex states and interactions that would otherwise reside in UI components, reducing implementation time and errors.

Even for a simple UI that displays a flat list of beverages, the CUI Kit can be customized to meet your requirements.

Initializing the CUI Kit

The Dispense SDK offers an initializeCuiKit function to set up the CUI Kit for a dispenser. You can configure the scope and features via options passed to this function. For example, if your application uses a single-level navigation (only beverages), you can set the initial group to the beverage screen:

// apps/training-zero4-ui/src/app/init.ts
import {
  DISPENSE_GROUP_BEVERAGE,
  initializeCuiKit,
} from '@kosdev-code/kos-dispense-sdk';
// Initialize the CUI Kit with the initial group
// Returns a hook to use the context and the CUI Kit component
const [useCuiKitContext, CuiKit] = initializeCuiKit({
  // Initial "level" for the CUI Kit, in this case, the beverage group
  initialGroup: DISPENSE_GROUP_BEVERAGE,
  options: {},
});
export { CuiKit, useCuiKitContext };

Configured this way, the CUI Kit starts at the “beverage” screen and treats it as the home screen when navigating back. It also sets up the underlying finite state machines that manage both selection and pouring workflows.

Using the CUI Kit in Your Application

To integrate the CUI Kit, begin by importing the context provider and hook:

// main-view.tsx
// Initialize the CUI Kit for this application
import { CuiKit, useCuiKitContext } from '../../init';

Then wrap your main application content with the CuiKit provider:

// main-view.tsx
export const MainView: React.FunctionComponent = kosComponent(() => {
  return (
    <Main>
      <CuiKit>{() => <NozzleView />}</CuiKit>
    </Main>
  );
});  

This wrapping ensures that all child components (like NozzleView) have access to the CUI Kit’s state and services, even supporting multiple nozzles without complicating the UI layer.

Breaking Down the NozzleView Component

Within the NozzleView, the CUI Kit is used to drive rendering and manage the beverage selection state:

// main.tsx
const NozzleView = () => {
  const kit = useCuiKitContext();
  if (!kit) {
    return null;
  }
  const { pour, beverages, selections, nav } = kit;
  const showBeverages = nav.currentGroup === DISPENSE_GROUP_BEVERAGE;
  return (
    <NozzleContainer>
      {showBeverages ? (
        <div>Welcome to the kOS CUI Kit</div>
      ) : (
        <div>{nav.currentGroup}</div>
      )}
    </NozzleContainer>
  );
};

You can start to see how the CUI Kit can reduce complexity and help manage the entire Select and Pour flow.

const kit = useCuiKitContext();

Using the useCuiKitContext function from from the init module returns the instance of the CUI Kit.

const { pour, beverages, selections, nav } = kit;

The CUI Kit returns a few different modules that can be used in the application to expose different parts of the underlying state.

If we break down the various modules that make up the CUI Kit further

Beverages

The beverages modules provides the state and handlers required to manage the set of beverages for the current brand selection:

beverages.models – The list of beverages for the current brand selection. As the user selects brands in the UI then this list of beverages will be updated to reflect new selection.

If there are no brands defined in the brandset then this will always return the full list of beverages.

The models will be instances of AvailabilityModel<T> with any data mappings (shown in the next section) already applied to the data property on the model.

Since these are kOS models, any changes to the model state will be automatically reflected in any components consuming them.

beverages.companions – Not covered in this tutorial, but the kOS framework allows you to define additional models that will get created alongside the “beverage” AvailabilityModels instances.

This is useful in more advanced cases where new functionality beyond what is exposed in the AvailabilityModel is require. For example, if in your application, you need Beverages to be able to fetch nutritional information on selection then you could create a companion model that will get created for each AvailabilityModel and expose the methods and data to retrieve this information.

Using these companions as a form of extension, this field will return the corresponding set of companion models that represent your custom beverages.

beverages.handlers.onSelected – When a user select a beverage, this handler can be called with the selected AvailabilityModel to ensure that the CUI Kit state is kept up to date.

beverages.clear – Calling this function will clear the selected beverage in the CUI Kit. This updates the state both locally and in the backend by clearing the selection in the Dispense SDK NozzleModel. This is a good example of the value of both the CUI Kit and the Dispense SDK in that all of the integration state management can be managed with very little code required in the application space.

NOTE – There are similar modules for brands and flavors that similarly keep track of the groupings of AvailabilityModels as the user moves through the UI.

Selections

The current selection across the brands/beverages/flavors are maintained in the selections modules.

beverages -Holds the currently selected AvailabilityModel beverage. When the onSelected handler for the beverages is invoked, this value will be updated in state. Any UI that tracks this value will automatically re-render when the user selects or clears a beverage. As the user calls the onSelected handler when selecting brands in the UI, this state property will reflect the selection automatically.

brands – Not applicable in this tutorial but behaves similarly to the beverages selection state.

flavors – In many cases, when using flavors it is valid for a user to select multiple flavors i.e. cherry and vanilla. This parameter will always contain an array of AvailabilityModels or an empty array if no flavors are selected.

nav.currentGroup – The state property will always reflect the current navigation state. When a user performs actions and selections in the UI this property will update to allow the UI to display new data. For example, when a user selects a brand then the CUI Kit understands that the next state transition is to display the list of beverages for the selected brand. As a result, on brand selection, the currentGroup will be updated to beverages and the UI will be able to show or hide components based on this value. Again, the UI doesn’t need to maintain complex logic for this state as this has been delegated to the CUI Kit.

nav.back – This navigation handler, when called will transition the currentGroup to the previously selection group.

nav.home – When this navigation handlers is called, the`currentGroup` will be updated to the initial state. So if the CUI Kit is initialized to start on beverages such as in this tutorial then calling this function will return the CUI Kit state back to it’s inital view.

After pulling the values from the CUI Kit we can start to use the state to drive rendering of the UI

const showBeverages = nav.currentGroup === DISPENSE_GROUP_BEVERAGE;

The component sets a flag (showBeverages) based on the current navigation group. It then conditionally renders a welcome message or the current group name.

{showBeverages ? (
        <div>Welcome to the kOS CUI Kit</div>
      ) : (
        <div>{nav.currentGroup}</div>
      )}

Results so far

If the dev server is running you should be able to point your browser to http://localhost:4200?host=http://localhost:8081# to confirm that the CUI Kit is properly initialized and ready to begin implementing the visual components:

CUI Kit Initialized