Examples

This guide covers React-specific implementation patterns for Voice+ custom commands using the useTouchpointCustomCommand hook and TouchpointContext system.

React Setup

TouchpointContext Implementation

Create a TouchpointContext to manage the Touchpoint instance and custom commands:

import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
import { create, type TouchpointConfiguration, BidirectionalCustomCommand, type TouchpointInstance } from '@nlxai/touchpoint-ui';

interface TouchpointContextType {
  isInitialized: boolean;
  isListening: boolean;
  conversationId: string;
  touchpointInstance: TouchpointInstance | null;
}

const TouchpointContext = createContext<TouchpointContextType | undefined>(undefined);
const TouchpointCustomCommandsCtx = createContext<
  Map<string, BidirectionalCustomCommand>
>(new Map());

// Global command handlers registry
const customCommandHandlers = new Map<string, (payload: any) => void>();

// Configuration constants
const NLX_APP_URL = 'https://bots.dev.studio.nlx.ai/c/YOUR_APPLICATION_URL';
const NLX_APP_API_KEY = 'your-api-key';
const DEFAULT_LANGUAGE = 'en-US';

export const TouchpointProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [isInitialized, setIsInitialized] = useState(false);
  const [isListening] = useState(false);
  const [conversationId] = useState(() => crypto.randomUUID());
  const [touchpointInstance, setTouchpointInstance] = useState<TouchpointInstance | null>(null);

  const initializedRef = useRef(false);

  useEffect(() => {
    if (initializedRef.current) return;
    initializedRef.current = true;

    let cleanup: (() => void) | undefined;

    (async () => {
      try {
        const headers = { 'nlx-api-key': NLX_APP_API_KEY };
        const touchpointConfig: TouchpointConfiguration = {
          config: {
            applicationUrl: NLX_APP_URL,
            headers,
            languageCode: DEFAULT_LANGUAGE,
          },
          input: 'voiceMini',
          bidirectional: {
            custom: handleCustomCommand,
          },
        };
        
        const touchpoint = await create(touchpointConfig);
        setTouchpointInstance(touchpoint);
        setIsInitialized(true);

        cleanup = () => {
          try {
            touchpoint?.teardown?.();
            setTouchpointInstance(null);
          } catch {}
        };
      } catch (e) {
        console.error('Failed to initialize Voice Plus:', e);
      }
    })();

    return () => cleanup?.();
  }, [conversationId]);

  const handleCustomCommand = (action: string, payload: any) => {
    console.log('Custom command:', action, payload);
    
    const handler = customCommandHandlers.get(action);
    if (handler) {
      handler(payload);
    } else {
      console.warn(`No handler registered for custom command: ${action}`);
    }
  };

  return (
    <TouchpointContext.Provider value={{
      isInitialized,
      isListening,
      conversationId,
      touchpointInstance
    }}>
      {children}
    </TouchpointContext.Provider>
  );
};

export const useTouchpoint = () => {
  const context = useContext(TouchpointContext);
  if (!context) {
    throw new Error('useTouchpoint must be used within a TouchpointProvider');
  }
  return context;
};

The useTouchpointCustomCommand Hook

The core hook for registering custom commands in React:

export const useTouchpointCustomCommand = (
  command: BidirectionalCustomCommand & { handler?: (payload: any) => void },
): void => {
  const commands = useContext(TouchpointCustomCommandsCtx);
  const { touchpointInstance } = useTouchpoint();
  
  useEffect(() => {
    // Register the command with NLX
    commands.set(command.action, command);
    
    // Register the handler if provided
    if (command.handler) {
      customCommandHandlers.set(command.action, command.handler);
    }
    
    if (touchpointInstance != null) {
      // This is debounced so safe to call multiple times during a single render
      touchpointInstance.setCustomBidirectionalCommands([...commands.values()]);
    }
    
    return () => {
      commands.delete(command.action);
      customCommandHandlers.delete(command.action);
    };
  }, [command, commands, touchpointInstance]);
};

React Component Examples

Counter Component

import { useState, useCallback } from 'react';
import { useTouchpointCustomCommand } from '../contexts/TouchpointContext';
import * as z from 'zod/v4';

const INCREMENT_ACTION = 'incrementCounter';
const DECREMENT_ACTION = 'decrementCounter';
const RESET_ACTION = 'resetCounter';
const INITIAL_COUNT = 0;

const counterSchema = z.object({
  amount: z.number().optional().describe('The amount to increment or decrement by'),
});

const resetSchema = z.object({});

export default function CounterComponent() {
  const [count, setCount] = useState(INITIAL_COUNT);

  const handleIncrement = useCallback((payload) => {
    const incrementAmount = payload?.amount || 1;
    setCount(prev => prev + incrementAmount);
    console.log(`Incremented by ${incrementAmount}`);
  }, []);

  const handleDecrement = useCallback((payload) => {
    const decrementAmount = payload?.amount || 1;
    setCount(prev => prev - decrementAmount);
    console.log(`Decremented by ${decrementAmount}`);
  }, []);

  const handleReset = useCallback(() => {
    setCount(INITIAL_COUNT);
    console.log('Counter reset');
  }, []);

  // Register multiple commands
  useTouchpointCustomCommand({
    action: INCREMENT_ACTION,
    description: 'Increment the counter by a specified amount or by 1',
    schema: counterSchema,
    handler: handleIncrement
  });

  useTouchpointCustomCommand({
    action: DECREMENT_ACTION,
    description: 'Decrement the counter by a specified amount or by 1',
    schema: counterSchema,
    handler: handleDecrement
  });

  useTouchpointCustomCommand({
    action: RESET_ACTION,
    description: 'Reset the counter to zero',
    schema: resetSchema,
    handler: handleReset
  });

  return (
    <div>
      <h3>Voice Counter: {count}</h3>
      <p>Try saying: "increment counter", "decrement by 5", "reset counter"</p>
    </div>
  );
}

Form Input Component

import { useState, useCallback } from 'react';
import { useTouchpointCustomCommand } from '../contexts/TouchpointContext';
import * as z from 'zod/v4';

const FILL_NAME_ACTION = 'fillName';
const FILL_EMAIL_ACTION = 'fillEmail';
const CLEAR_FORM_ACTION = 'clearForm';
const SUBMIT_FORM_ACTION = 'submitForm';

const fillNameSchema = z.object({
  name: z.string().min(1).describe('The name to fill in the name field'),
});

const fillEmailSchema = z.object({
  email: z.string().email().describe('The email address to fill in the email field'),
});

export default function FormComponent() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleFillName = useCallback((payload) => {
    const nameValue = payload?.name || '';
    setName(nameValue);
    console.log('Name filled via voice:', nameValue);
  }, []);

  const handleFillEmail = useCallback((payload) => {
    const emailValue = payload?.email || '';
    setEmail(emailValue);
    console.log('Email filled via voice:', emailValue);
  }, []);

  const handleClearForm = useCallback(() => {
    setName('');
    setEmail('');
    console.log('Form cleared via voice');
  }, []);

  const handleSubmitForm = useCallback(() => {
    if (name && email) {
      console.log('Form submitted via voice:', { name, email });
    } else {
      console.log('Cannot submit: missing required fields');
    }
  }, [name, email]);

  useTouchpointCustomCommand({
    action: FILL_NAME_ACTION,
    description: 'Fill the name field with a specified name',
    schema: fillNameSchema,
    handler: handleFillName
  });

  useTouchpointCustomCommand({
    action: FILL_EMAIL_ACTION,
    description: 'Fill the email field with a specified email address',
    schema: fillEmailSchema,
    handler: handleFillEmail
  });

  useTouchpointCustomCommand({
    action: CLEAR_FORM_ACTION,
    description: 'Clear all form fields',
    schema: z.object({}),
    handler: handleClearForm
  });

  useTouchpointCustomCommand({
    action: SUBMIT_FORM_ACTION,
    description: 'Submit the form if all required fields are filled',
    schema: z.object({}),
    handler: handleSubmitForm
  });

  return (
    <form>
      <div>
        <label>Name:</label>
        <input 
          type="text" 
          value={name} 
          onChange={(e) => setName(e.target.value)}
          placeholder="Enter your name" 
        />
      </div>
      <div>
        <label>Email:</label>
        <input 
          type="email" 
          value={email} 
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter your email" 
        />
      </div>
      <p>Try saying: "fill name with John Doe", "fill email with [email protected]", "clear form", "submit form"</p>
    </form>
  );
}

React Quick Start

Installation

npm install @nlxai/touchpoint-ui zod react

App Setup

1. Create TouchpointContext (contexts/TouchpointContext.tsx): Use the TouchpointContext implementation shown above.

2. Create a component with custom commands (components/VoiceCounter.tsx):

import { useState, useCallback } from 'react';
import { useTouchpointCustomCommand } from '../contexts/TouchpointContext';
import * as z from 'zod/v4';

export default function VoiceCounter() {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback((payload) => {
    const amount = payload?.amount || 1;
    setCount(prev => prev + amount);
  }, []);

  useTouchpointCustomCommand({
    action: 'incrementCounter',
    description: 'Increment the counter by a specified amount',
    schema: z.object({
      amount: z.number().optional().describe('Amount to increment by'),
    }),
    handler: handleIncrement
  });

  return (
    <div>
      <h2>Voice Counter: {count}</h2>
      <button onClick={() => setCount(prev => prev + 1)}>
        Manual +1
      </button>
      <p>Try saying: "increment counter" or "increment by 5"</p>
    </div>
  );
}

3. Set up your main App.tsx:

import { TouchpointProvider } from './contexts/TouchpointContext';
import VoiceCounter from './components/VoiceCounter';

function App() {
  return (
    <TouchpointProvider>
      <div className="App">
        <h1>Voice+ Custom Commands Demo</h1>
        <VoiceCounter />
      </div>
    </TouchpointProvider>
  );
}

export default App;

React Best Practices

1. Use Constants for Action Names

const BUTTON_CLICK_ACTION = 'buttonClick';
const INCREMENT_ACTION = 'incrementCounter';
const SUBMIT_FORM_ACTION = 'submitForm';

2. Use useCallback for Handlers

const handleCommand = useCallback((payload) => {
  // Handler logic here
}, [dependencies]);

3. Validate Form Data in Handlers

const handleSubmitForm = useCallback((payload) => {
  if (!name || !email) {
    console.error('Cannot submit: missing required fields');
    return;
  }
  
  // Process form submission
  submitForm({ name, email });
}, [name, email]);

4. Provide Clear Voice Command Examples

<div className="voice-commands">
  <strong>Voice Commands:</strong> 
  "click the button", "increment by 5", "fill name with John"
</div>

5. Error Handling in React Components

const handleCommand = useCallback((payload) => {
  try {
    // Validate payload
    if (!payload || !payload.requiredField) {
      throw new Error('Missing required field');
    }
    
    // Execute command logic
    performAction(payload);
    
  } catch (error) {
    console.error('Command failed:', error.message);
    // Show user-friendly error message
    showErrorMessage('Voice command could not be completed');
  }
}, []);

Last updated