UNPKG

@voice-ai-workforce/react

Version:

React components with 3-tier interface modes for voice-controlled workforce applications

194 lines (193 loc) 6.66 kB
import { jsx as _jsx } from "react/jsx-runtime"; // packages/react/src/components/VoiceProvider.tsx import React, { createContext, useContext, useMemo } from 'react'; import { useVoiceVisibility } from '../hooks/useVoiceVisibility'; // Default theme const defaultTheme = { colors: { primary: '#2563eb', secondary: '#64748b', success: '#059669', warning: '#d97706', error: '#dc2626', surface: '#ffffff', background: '#f8fafc', border: '#e2e8f0', text: { primary: '#1e293b', secondary: '#475569', muted: '#94a3b8', inverse: '#ffffff' }, status: { online: '#059669', offline: '#64748b', listening: '#2563eb', processing: '#d97706' } }, shadows: { none: 'none', sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)', md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', '2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)' }, spacing: { xs: '0.25rem', sm: '0.5rem', md: '1rem', lg: '1.5rem', xl: '2rem', '2xl': '2.5rem', '3xl': '3rem' }, borderRadius: { none: '0', sm: '0.25rem', md: '0.375rem', lg: '0.5rem', xl: '0.75rem', full: '9999px' }, // @ts-ignore typography: { fontFamily: 'system-ui, -apple-system, sans-serif', fontSize: { xs: '0.75rem', sm: '0.875rem', md: '1rem', lg: '1.125rem', xl: '1.25rem', '2xl': '1.5rem' }, fontWeight: { normal: '400', medium: '500', semibold: '600', bold: '700' } } }; const VoiceContext = createContext(null); // Deep merge helper function deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source.hasOwnProperty(key)) { const sourceValue = source[key]; const targetValue = result[key]; if (typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)) { result[key] = deepMerge(targetValue, sourceValue); } else if (sourceValue !== undefined) { result[key] = sourceValue; } } } return result; } export const VoiceProvider = ({ children, theme: initialTheme, config, mode: initialMode, visibilityOverrides, customLabels: initialCustomLabels }) => { // Theme state const [customTheme, setCustomTheme] = React.useState(initialTheme || {}); // Voice configuration state const [voiceConfig, setVoiceConfig] = React.useState(config); const [currentMode, setCurrentMode] = React.useState(initialMode); const [visibilityConfig, setVisibilityConfig] = React.useState(visibilityOverrides || {}); const [labelsConfig, setLabelsConfig] = React.useState(initialCustomLabels || {}); // Merge default theme with custom theme const mergedTheme = useMemo(() => { return deepMerge(defaultTheme, customTheme); }, [customTheme]); // Resolve visibility and labels based on current config and mode const { visibility, labels } = useVoiceVisibility(voiceConfig || config, currentMode, visibilityConfig); // Merge provider labels with resolved labels const effectiveLabels = useMemo(() => ({ voiceButton: { ...labels.voiceButton, ...labelsConfig.voiceButton }, status: { ...labels.status, ...labelsConfig.status }, providers: { ...labels.providers, ...labelsConfig.providers }, errors: { ...labels.errors, ...labelsConfig.errors } }), [labels, labelsConfig]); // Theme management functions const updateTheme = React.useCallback((newTheme) => { setCustomTheme(prev => deepMerge(prev, newTheme)); }, []); const resetTheme = React.useCallback(() => { setCustomTheme(initialTheme || {}); }, [initialTheme]); // Voice configuration management functions const updateConfig = React.useCallback((newConfig) => { setVoiceConfig(prev => prev ? { ...prev, ...newConfig } : newConfig); }, []); const setMode = React.useCallback((mode) => { setCurrentMode(mode); }, []); const updateVisibility = React.useCallback((newVisibility) => { setVisibilityConfig(prev => ({ ...prev, ...newVisibility })); }, []); const updateLabels = React.useCallback((newLabels) => { setLabelsConfig(prev => ({ ...prev, ...newLabels })); }, []); const contextValue = useMemo(() => ({ // Theme context theme: mergedTheme, updateTheme, resetTheme, // Voice configuration context config: voiceConfig || config, mode: currentMode, visibility, labels: effectiveLabels, updateConfig, setMode, updateVisibility, updateLabels }), [ mergedTheme, updateTheme, resetTheme, voiceConfig, config, currentMode, visibility, effectiveLabels, updateConfig, setMode, updateVisibility, updateLabels ]); return (_jsx(VoiceContext.Provider, { value: contextValue, children: children })); }; // Hook to use the combined context export const useVoiceContext = () => { const context = useContext(VoiceContext); if (!context) { throw new Error('useVoiceContext must be used within a VoiceProvider'); } return context; }; // Legacy hook for theme-only usage (backward compatibility) export const useVoiceThemeContext = () => { const context = useVoiceContext(); return { theme: context.theme, updateTheme: context.updateTheme, resetTheme: context.resetTheme }; }; // Hook to get component theme (merges context + component-level theme) export const useComponentTheme = (componentTheme) => { const { theme: contextTheme } = useVoiceContext(); return useMemo(() => { if (!componentTheme) return contextTheme; return deepMerge(contextTheme, componentTheme); }, [contextTheme, componentTheme]); }; export default VoiceProvider;