@voice-ai-workforce/react
Version:
React components with 3-tier interface modes for voice-controlled workforce applications
205 lines (204 loc) • 12.8 kB
JavaScript
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;