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