UNPKG

@voice-ai-workforce/react

Version:

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

205 lines (204 loc) 12.8 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; // packages/react/src/components/VoiceStatusIndicator.tsx import { useEffect, useState } from 'react'; import { useVoiceAI } from '../hooks/useVoiceAI'; import { useComponentTheme } from './VoiceProvider'; import { getStatusColor } from '../utils/theme'; import { useVoiceVisibility } from '../hooks/useVoiceVisibility'; // Icons with proper SVG props const WifiIcon = ({ className, ...props }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", ...props, children: _jsx("path", { d: "M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.65-4.35-1.65-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.86 9.14 5 13z" }) })); const MicrophoneIcon = ({ className, ...props }) => (_jsxs("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", ...props, children: [_jsx("path", { d: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" }), _jsx("path", { d: "M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" })] })); const LoadingSpinner = ({ className, ...props }) => (_jsxs("svg", { className: `animate-spin ${className}`, fill: "none", viewBox: "0 0 24 24", ...props, children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] })); const AlertIcon = ({ className, ...props }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", ...props, children: _jsx("path", { d: "M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" }) })); const CheckIcon = ({ className, ...props }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", ...props, children: _jsx("path", { d: "M9 16.17L4.83 12l-1.42 1.42L9 19 21 7l-1.42-1.42z" }) })); const CloudOffIcon = ({ className, ...props }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", ...props, children: _jsx("path", { d: "M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM3 5.27l2.28 2.28C6.15 6.69 8.83 6 12 6c1.66 0 3.14.69 4.22 1.78L19 5l1.41 1.41L3.41 23.41 2 22 3 5.27z" }) })); export const VoiceStatusIndicator = ({ config, variant = 'badge', autoUpdate = true, updateInterval = 5000, onClick, tooltip = true, theme: customTheme, size = 'md', className = '', style, // Legacy props - now controlled by visibility showProvider, showConnection, showLabel, // NEW: Mode support props mode, visibilityOverrides, customLabels: propCustomLabels, ...props }) => { const theme = useComponentTheme(customTheme); const [connectionQuality, setConnectionQuality] = useState('good'); const [lastUpdate, setLastUpdate] = useState(new Date()); // NEW: Resolve visibility and labels based on mode const { visibility, labels } = useVoiceVisibility(config, mode, visibilityOverrides); // Merge prop labels with resolved labels const effectiveLabels = { voiceButton: { ...labels.voiceButton, ...propCustomLabels?.voiceButton }, status: { ...labels.status, ...propCustomLabels?.status }, providers: { ...labels.providers, ...propCustomLabels?.providers }, errors: { ...labels.errors, ...propCustomLabels?.errors } }; // Determine what to show based on mode and legacy props const shouldShowProvider = showProvider !== undefined ? showProvider : visibility.showProviders; const shouldShowConnection = showConnection !== undefined ? showConnection : visibility.showProviderStatus; const shouldShowLabel = showLabel !== undefined ? showLabel : !visibility.useGenericLabels; // Voice AI state const { isListening, isProcessing, isAvailable, error, getState } = useVoiceAI({ config, autoStart: false }); const state = getState(); // Determine current status const getCurrentStatus = () => { if (error) return 'error'; if (isProcessing) return 'processing'; if (isListening) return 'listening'; if (isAvailable) return 'online'; return 'offline'; }; const currentStatus = getCurrentStatus(); // Check connection quality const checkConnectionQuality = async () => { if (!navigator.onLine) { return 'offline'; } try { const start = performance.now(); await fetch('https://www.google.com/favicon.ico', { mode: 'no-cors', cache: 'no-cache' }); const responseTime = performance.now() - start; if (responseTime < 100) return 'excellent'; if (responseTime < 300) return 'good'; if (responseTime < 1000) return 'poor'; return 'poor'; } catch { return 'offline'; } }; // Auto-update connection quality useEffect(() => { let interval; const updateConnection = async () => { const quality = await checkConnectionQuality(); setConnectionQuality(quality); setLastUpdate(new Date()); }; if (autoUpdate) { updateConnection(); // Initial check interval = setInterval(updateConnection, updateInterval); } return () => { if (interval) clearInterval(interval); }; }, [autoUpdate, updateInterval]); // Get status display info using effective labels const getStatusInfo = () => { switch (currentStatus) { case 'online': return { color: getStatusColor('online', theme), icon: CheckIcon, label: effectiveLabels.status.online || 'Online', description: 'Voice AI is ready' }; case 'listening': return { color: getStatusColor('listening', theme), icon: MicrophoneIcon, label: effectiveLabels.status.listening || 'Listening', description: 'Listening for voice input' }; case 'processing': return { color: getStatusColor('processing', theme), icon: LoadingSpinner, label: effectiveLabels.status.processing || 'Processing', description: 'Processing voice command' }; case 'error': return { color: theme.colors.error, icon: AlertIcon, label: effectiveLabels.status.error || 'Error', description: visibility.showTechnicalErrors ? (error || 'Voice AI error') : effectiveLabels.errors.generic }; case 'offline': default: return { color: getStatusColor('offline', theme), icon: CloudOffIcon, label: effectiveLabels.status.offline || 'Offline', description: 'Voice AI is not available' }; } }; // Get connection display info const getConnectionInfo = () => { switch (connectionQuality) { case 'excellent': return { color: theme.colors.success, bars: 4, label: 'Excellent', description: 'Strong connection' }; case 'good': return { color: theme.colors.success, bars: 3, label: 'Good', description: 'Good connection' }; case 'poor': return { color: theme.colors.warning, bars: 2, label: 'Poor', description: 'Weak connection' }; case 'offline': default: return { color: theme.colors.error, bars: 0, label: 'Offline', description: 'No connection' }; } }; const statusInfo = getStatusInfo(); const connectionInfo = getConnectionInfo(); const StatusIcon = statusInfo.icon; // Render based on variant const renderContent = () => { switch (variant) { case 'dot': return (_jsx("div", { className: `w-3 h-3 rounded-full ${className}`, style: { backgroundColor: statusInfo.color, ...style }, title: tooltip ? `${statusInfo.label}: ${statusInfo.description}` : undefined })); case 'minimal': return (_jsxs("div", { className: `flex items-center space-x-1 ${className}`, style: style, children: [_jsx("div", { className: "w-2 h-2 rounded-full", style: { backgroundColor: statusInfo.color } }), shouldShowLabel && (_jsx("span", { className: "text-xs font-medium", style: { color: theme.colors.text.secondary }, children: statusInfo.label }))] })); case 'badge': return (_jsxs("div", { className: `inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${className}`, style: { backgroundColor: `${statusInfo.color}20`, color: statusInfo.color, border: `1px solid ${statusInfo.color}40`, ...style }, title: tooltip ? statusInfo.description : undefined, children: [_jsx(StatusIcon, { className: "w-3 h-3 mr-1", style: { color: statusInfo.color } }), shouldShowLabel && statusInfo.label] })); case 'full': default: return (_jsxs("div", { className: `bg-white rounded-lg border p-3 shadow-sm ${className}`, style: { backgroundColor: theme.colors.surface, borderColor: theme.colors.border, ...style }, children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsxs("div", { className: "relative", children: [_jsx(StatusIcon, { className: "w-5 h-5", style: { color: statusInfo.color } }), currentStatus === 'processing' && (_jsx("div", { className: "absolute inset-0 rounded-full border-2 border-current opacity-30 animate-ping" }))] }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-sm font-medium", style: { color: theme.colors.text.primary }, children: statusInfo.label }), _jsx("div", { className: "text-xs", style: { color: theme.colors.text.secondary }, children: statusInfo.description })] })] }), shouldShowProvider && state.activeProvider && (_jsx("div", { className: "mt-2 pt-2 border-t", style: { borderColor: theme.colors.border }, children: _jsx("div", { className: "text-xs", style: { color: theme.colors.text.muted }, children: visibility.showProviders ? `Provider: ${state.activeProvider}` : effectiveLabels.providers.generic }) })), shouldShowConnection && (_jsxs("div", { className: "mt-2 pt-2 border-t flex items-center justify-between", style: { borderColor: theme.colors.border }, children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(WifiIcon, { className: "w-4 h-4", style: { color: connectionInfo.color } }), _jsx("span", { className: "text-xs", style: { color: theme.colors.text.muted }, children: connectionInfo.label })] }), _jsx("div", { className: "flex items-end space-x-0.5", children: [1, 2, 3, 4].map((bar) => (_jsx("div", { className: `w-1 bg-current transition-opacity ${bar <= connectionInfo.bars ? 'opacity-100' : 'opacity-20'}`, style: { height: `${bar * 2 + 2}px`, color: connectionInfo.color } }, bar))) })] })), !visibility.useGenericLabels && (_jsxs("div", { className: "mt-2 text-xs text-center", style: { color: theme.colors.text.muted }, children: ["Updated ", lastUpdate.toLocaleTimeString()] }))] })); } }; return (_jsx("div", { className: onClick ? 'cursor-pointer' : '', onClick: onClick, ...props, children: renderContent() })); }; export default VoiceStatusIndicator;