CUI Kit – Building Consumer-Facing Beverage Interfaces

The Consumer User Interface Kit (CUI Kit) represents KOS’s approach to creating intuitive, hardware-integrated beverage dispensing interfaces. Rather than building generic UI components, the CUI Kit addresses the unique challenges of consumer-facing dispensers: multi-tier product selection, dynamic availability, session management, and coordinated hardware integration.

The Consumer Interface Challenge

Creating effective beverage dispensing interfaces requires solving problems that traditional web applications don’t face. Customers navigate through complex product hierarchies—brands leading to beverages leading to flavor variants—while inventory and equipment status continuously change availability. The interface must coordinate with physical dispensing hardware, manage session timeouts for high-traffic environments, and support multiple interaction modes from touchscreens to physical buttons.

The CUI Kit addresses these challenges through a React-based framework that provides type-safe state management, automatic multi-nozzle support, and comprehensive lifecycle management. The framework handles the complexity of hardware coordination while presenting developers with familiar React patterns.

Provider Architecture and Multi-Nozzle Support

The CUI Kit uses a layered provider architecture that mirrors the physical structure of dispensing systems. This architecture automatically handles the complexity of multi-nozzle dispensers without requiring manual iteration:

<DeviceAssemblyProvider>
  <CuiKitProvider>
    {(nozzleProps) => (
      // This function is called for each nozzle automatically
      <ConsumerInterface {...nozzleProps} />
    )}
  </CuiKitProvider>
</DeviceAssemblyProvider>

The critical insight here is that CuiKitProvider takes a function as its child, not a React element. This function receives context about each nozzle—including its index, path, and position relative to other nozzles—allowing the same component code to adapt automatically to different nozzle configurations.

This pattern eliminates the need for manual nozzle iteration while providing rich context for customization. Components can render differently based on whether they’re the primary nozzle, handle different layout strategies for different nozzle counts, or progressively disclose features across nozzle positions.

Type-Safe Product Data Structure

The CUI Kit extends beyond basic product catalogs by supporting custom data structures with full TypeScript safety. This enables applications to attach rich metadata to products while maintaining type checking throughout the selection flow:

interface CocaColaBrandData {
  logoUrl: string;
  marketingMessage: string;
  sustainabilityScore: number;
}
interface BeverageData {
  nutritionalInfo: NutritionFacts;
  caffeineContent: number;
  isLimitedEdition: boolean;
}
interface FlavorData {
  intensity: "light" | "medium" | "strong";
  pairingNotes: string[];
}
const [useCuiKit, CuiKitProvider] = initializeCuiKit<
  CocaColaBrandData,
  BeverageData,
  FlavorData
>({
  initialGroup: "brands",
});

This type-safe approach ensures that custom product data remains consistent across all selection interfaces while enabling rich, branded experiences that adapt to different product categories and marketing requirements.

Navigation and Selection Flow

The CUI Kit manages navigation state automatically, but provides fine-grained control over the selection flow. The navigation system understands the typical brand-beverage-flavor hierarchy while supporting customization for different deployment scenarios:

function ConsumerInterface() {
  const kit = useCuiKitContext();
  const currentGroup = kit.nav.currentGroup;
  return (
    <div className="cui-container">
      <Header
        onBack={kit.nav.back}
        onHome={kit.nav.home}
        showBack={currentGroup !== "brands"}
      />
      {currentGroup === "brands" && <BrandSelection />}
      {currentGroup === "beverages" && <BeverageSelection />}
      {currentGroup === "flavors" && <FlavorSelection />}
      <PourControls />
    </div>
  );
}

Selection components integrate seamlessly with this navigation flow. When a user selects a beverage, the framework automatically handles navigation to the flavor selection, updates the selection state, and triggers any configured analytics or side effects:

function BeverageSelection() {
  const kit = useCuiKitContext();
  const { models, handlers } = kit.beverages;
  const selectedBeverage = kit.selections.beverages;
  return (
    <div className="beverage-grid">
      {models.map((beverage) => (
        <BeverageCard
          key={beverage.id}
          beverage={beverage}
          isSelected={selectedBeverage?.id === beverage.id}
          onSelect={() => handlers.onSelected(beverage)}
        />
      ))}
    </div>
  );
}

The selection handlers manage all the coordination—updating internal state, triggering navigation, and publishing events—while components focus purely on presentation and user interaction.

Hardware-Integrated Pour Control

Pour control represents where the CUI Kit’s hardware integration becomes most apparent. The framework coordinates pre-pour validation, handles hardware communication, and provides real-time feedback during dispensing operations. Pour progress comes from the hardware through the pour status, not from local calculations:

function PourControls() {
  const kit = useCuiKitContext();
  const { canPour, startPour, stopPour, isPouring, status } = kit.pour;
  // Add custom validation before pour
  useEffect(() => {
    kit.updateOnBeforePour(async () => {
      const ageVerified = await checkAgeVerification();
      const paymentValid = await validatePayment();
      if (!ageVerified) {
        showAgeVerificationModal();
        return false;
      }
      if (!paymentValid) {
        showPaymentModal();
        return false;
      }
      return true;
    });
  }, [kit]);
  // Pour progress comes from hardware status
  const pourProgress = status?.progress || 0;
  return (
    <div className="pour-controls">
      <button
        className="pour-button"
        disabled={!canPour}
        onMouseDown={startPour}
        onMouseUp={stopPour}
        onTouchStart={startPour}
        onTouchEnd={stopPour}
      >
        {isPouring ? `Dispensing... ${Math.round(pourProgress)}%` : "Press and Hold to Pour"}
      </button>
      {isPouring && (
        <div className="progress-bar">
          <div 
            className="progress-fill"
            style={{ width: `${pourProgress}%` }}
          />
        </div>
      )}
    </div>
  );
}

The pour progress value comes from pour.status.progress, which receives real-time updates from the dispensing hardware through the nozzle model’s futureHandler. This ensures that progress indication reflects actual dispensing progress rather than estimated timers, providing accurate feedback to users and enabling precise portion control.

Session Management and Lifecycle

Consumer-facing interfaces require sophisticated session management to handle high-traffic environments and varying usage patterns. The CUI Kit provides comprehensive timer management that adapts to different operational phases:

function ConsumerInterfaceWithTimers() {
  const kit = useCuiKitContext();
  const {
    showAreYouThere,
    areYouThereCountdown,
    clearAreYouThere,
    showAmbientScreen,
    showAttractScreen,
  } = useDefaultCuiKitLifecycle({
    kit,
    inactivity: {
      idleTimeout: 60, // Clear selections after 60 seconds
      areYouThereTimeoutOffset: 15, // Show prompt at 45 seconds
    },
    pour: {
      idleTimeout: 30, // Shorter timeout after pour
      areYouThereTimeoutOffset: 10,
    },
    screens: {
      attractTimeout: 300, // Show attract after 5 minutes
      ambientTimeoutOffset: 120, // Show ambient 2 minutes before attract
    },
  });
  if (showAttractScreen) {
    return <AttractLoop onTouch={() => {}} />;
  }
  if (showAmbientScreen) {
    return <AmbientDisplay onTouch={() => {}} />;
  }
  return (
    <>
      <ConsumerInterface />
      {showAreYouThere && (
        <AreYouThereModal
          countdown={areYouThereCountdown}
          onStayActive={clearAreYouThere}
        />
      )}
    </>
  );
}

This lifecycle management adapts timeout behavior based on the current phase of interaction. Selection browsing allows longer timeouts than active dispensing, while the system can display different screen savers based on venue requirements and traffic patterns.

Event-Driven Analytics and Integration

The CUI Kit publishes detailed events throughout the user journey, enabling comprehensive analytics and external system integration. The useDispenseLifecycle hook provides clean event handling without cluttering the main interface code:

function AnalyticsTracker() {
  useDispenseLifecycle({
    onBeverageSelected: (beverage) => {
      analytics.track("BeverageSelected", {
        beverageId: beverage.id,
        beverageName: beverage.name,
        timestamp: Date.now(),
      });
      updateRecommendations(beverage);
    },
    onPour: () => {
      analytics.track("PourStarted");
    },
    onPourCancel: () => {
      analytics.track("PourCompleted");
    },
    onIdleTimeout: () => {
      analytics.track("SessionIdleTimeout");
    },
    onAreYouThereTimeout: () => {
      analytics.track("AreYouTherePromptShown");
    },
  });
  return null;
}

This event system enables rich analytics, external system integration, and custom business logic while keeping the core interface components focused on user interaction. Events include both user actions and system state changes, providing complete visibility into the customer journey.

State Management Philosophy

The CUI Kit follows a critical principle: it should drive application state, not consume external state. This approach ensures that hardware events, backend availability changes, and timer-based lifecycle events remain the authoritative source of truth:

// Correct pattern: CUI Kit drives state, router reflects it
function App() {
  const navigate = useNavigate();
  useDispenseLifecycle({
    onBrandSelected: (brand) => {
      navigate(`/brands/${brand.id}`, { replace: true });
    },
    onBeverageSelected: (beverage) => {
      navigate(`/beverages/${beverage.id}`, { replace: true });
    },
    onReturnToHome: () => {
      navigate("/", { replace: true });
    },
  });
  return (
    <Router>
      <Routes>
        <Route
          path="*"
          element={
            <CuiKitProvider>{() => <ConsumerInterface />}</CuiKitProvider>
          }
        />
      </Routes>
    </Router>
  );
}

This approach prevents conflicts between hardware events and external state sources while ensuring that the interface remains responsive to the physical dispensing environment.

Companion Model Integration

The CUI Kit integrates seamlessly with KOS companion models, enabling rich business logic and custom data enrichment alongside the standard product catalog:

const [useCuiKit, CuiKitProvider] = initializeCuiKit({
  initialGroup: "brands",
  options: {
    companionTypes: {
      brand: BrandCompanion.type,
      beverage: BeverageCompanion.type,
    },
  },
});
function BrandSelection() {
  const kit = useCuiKitContext();
  const { companions: brandCompanions } = kit.brands;
  return (
    <div className="brand-grid">
      {brandCompanions?.map((companion) => (
        <div key={companion.id} className="brand-card">
          <h3>{companion.brandName}</h3>
          <p>{companion.beverages.length} beverages</p>
          {companion.free && (
            <span className="free-indicator">Has Free Options</span>
          )}
          <button
            onClick={() =>
              kit.brands.handlers.onSelected(companion.availability)
            }
          >
            Select Brand
          </button>
        </div>
      ))}
    </div>
  );
}

Companion models extend the basic product data with calculated properties, business rules, and aggregated information while maintaining the reactive updates that keep interfaces responsive to changing conditions.

Performance and Optimization

The CUI Kit leverages KOS’s observability framework to ensure optimal performance in consumer-facing environments. Components automatically re-render only when consumed properties change, while the framework handles event batching and lazy loading to maintain responsive interactions even with large product catalogs.

The framework’s integration with the hardware layer ensures that progress updates, availability changes, and dispensing status reflect real-time device state rather than estimated or simulated values. This accuracy proves essential for creating trustworthy consumer experiences and maintaining operational efficiency.

The CUI Kit represents a mature approach to consumer interface development that addresses the specific challenges of beverage dispensing while maintaining the flexibility and developer experience of modern React development. By handling hardware integration, state management, and lifecycle concerns at the framework level, it enables teams to focus on creating compelling user experiences rather than managing infrastructure complexity.