@voice-ai-workforce/react
Version:
React components with 3-tier interface modes for voice-controlled workforce applications
175 lines (174 loc) • 23.2 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
// packages/react/src/components/VoiceHistoryPanel.tsx
import React, { useState, useMemo } from 'react';
import { useVoiceAI } from '../hooks/useVoiceAI';
import { useVoiceVisibility } from '../hooks/useVoiceVisibility';
import { useVoiceHistory } from '../hooks/useVoiceHistory';
import { useComponentTheme } from './VoiceProvider';
import { SIZE_CLASSES } from '../utils/theme';
// Icons
const SearchIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" }) }));
const FilterIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" }) }));
const PlayIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M8 5v14l11-7z" }) }));
const DeleteIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" }) }));
const ExportIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z" }) }));
const CalendarIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z" }) }));
const StatsIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z" }) }));
const ClearIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }));
const CheckIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M9 16.17L4.83 12l-1.42 1.42L9 19 21 7l-1.42-1.42z" }) }));
const CloseIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }));
const HistoryIcon = ({ className }) => (_jsx("svg", { className: className, fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z" }) }));
export const VoiceHistoryPanel = ({ config, maxItems = 50, onCommandReplay, onHistoryChange, theme: customTheme, size = 'md', className = '', style,
// Legacy props - now controlled by visibility
showFilters, showSearch, showStats, showExport,
// NEW: Mode support props
mode, visibilityOverrides, customLabels: propCustomLabels, ...props }) => {
const theme = useComponentTheme(customTheme);
// 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 features to show based on mode and legacy props
const shouldShowFilters = showFilters !== undefined ? showFilters : visibility.showAdvancedSettings;
const shouldShowSearch = showSearch !== undefined ? showSearch : true; // Always show search
const shouldShowStats = showStats !== undefined ? showStats : visibility.showAnalytics;
const shouldShowExport = showExport !== undefined ? showExport : visibility.showExportOptions;
// Available tabs based on visibility
const availableTabs = [
{ id: 'history', label: 'History', icon: HistoryIcon, visible: true },
{ id: 'stats', label: 'Analytics', icon: StatsIcon, visible: shouldShowStats }
].filter(tab => tab.visible);
const [activeTab, setActiveTab] = useState(availableTabs[0]?.id || 'history');
const [showFilterPanel, setShowFilterPanel] = useState(false);
// Voice AI for command processing
const { processText } = useVoiceAI({ config, autoStart: false });
// History hook
const { filteredHistory, stats, removeCommand, clearHistory, exportHistory, filters, setFilters, clearFilters, searchQuery, setSearchQuery, replayCommand, } = useVoiceHistory({ maxHistoryItems: maxItems });
// Notify parent of history changes
React.useEffect(() => {
onHistoryChange?.(filteredHistory.length);
}, [filteredHistory.length, onHistoryChange]);
// Handle command replay
const handleReplayCommand = async (commandId) => {
try {
const command = replayCommand(commandId);
if (command) {
await processText(command.rawText);
onCommandReplay?.(commandId);
}
}
catch (error) {
console.error('Failed to replay command:', error);
}
};
// Handle export
const handleExport = (format) => {
const data = exportHistory(format);
const blob = new Blob([data], {
type: format === 'json' ? 'application/json' : 'text/csv'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `voice-history.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Format relative time
const formatRelativeTime = (date) => {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1)
return 'Just now';
if (diffMins < 60)
return `${diffMins}m ago`;
if (diffHours < 24)
return `${diffHours}h ago`;
if (diffDays < 7)
return `${diffDays}d ago`;
return date.toLocaleDateString();
};
// Get confidence color
const getConfidenceColor = (confidence) => {
if (confidence >= 0.8)
return theme.colors.success;
if (confidence >= 0.6)
return theme.colors.warning;
return theme.colors.error;
};
// Group history by date
const groupedHistory = useMemo(() => {
const groups = {};
filteredHistory.forEach(entry => {
const dateKey = entry.timestamp.toDateString();
if (!groups[dateKey]) {
groups[dateKey] = [];
}
groups[dateKey].push(entry);
});
return groups;
}, [filteredHistory]);
const panelClasses = [
'bg-white rounded-lg border shadow-sm',
SIZE_CLASSES.panel?.[size] || 'w-96 max-h-96',
className
].filter(Boolean).join(' ');
return (_jsxs("div", { className: panelClasses, style: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border,
...style
}, ...props, children: [_jsxs("div", { className: "flex items-center justify-between p-4 border-b", style: { borderColor: theme.colors.border }, children: [_jsx("h3", { className: "text-lg font-semibold", style: { color: theme.colors.text.primary }, children: visibility.useGenericLabels ? 'Command History' : 'Voice History' }), _jsxs("div", { className: "flex items-center space-x-2", children: [availableTabs.length > 1 && (_jsx(_Fragment, { children: availableTabs.map(tab => (_jsxs("button", { onClick: () => setActiveTab(tab.id), className: `flex items-center space-x-1 px-3 py-1 text-sm rounded transition-colors ${activeTab === tab.id ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-100'}`, children: [_jsx(tab.icon, { className: "w-4 h-4" }), _jsx("span", { children: tab.label })] }, tab.id))) })), shouldShowExport && (_jsxs("div", { className: "relative group", children: [_jsx("button", { className: "p-2 rounded hover:bg-gray-100 transition-colors", style: { color: theme.colors.text.secondary }, title: "Export History", children: _jsx(ExportIcon, { className: "w-4 h-4" }) }), _jsxs("div", { className: "absolute right-0 top-full mt-1 w-32 bg-white rounded-lg shadow-lg border opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-10", style: { backgroundColor: theme.colors.surface, borderColor: theme.colors.border }, children: [_jsx("button", { onClick: () => handleExport('json'), className: "w-full text-left px-3 py-2 text-sm hover:bg-gray-50 first:rounded-t-lg", children: "Export JSON" }), _jsx("button", { onClick: () => handleExport('csv'), className: "w-full text-left px-3 py-2 text-sm hover:bg-gray-50 last:rounded-b-lg", children: "Export CSV" })] })] })), _jsx("button", { onClick: clearHistory, className: "p-2 rounded hover:bg-gray-100 transition-colors", style: { color: theme.colors.text.secondary }, title: "Clear History", children: _jsx(DeleteIcon, { className: "w-4 h-4" }) })] })] }), (shouldShowSearch || shouldShowFilters) && activeTab === 'history' && (_jsxs("div", { className: "p-4 border-b space-y-3", style: { borderColor: theme.colors.border }, children: [shouldShowSearch && (_jsxs("div", { className: "relative", children: [_jsx(SearchIcon, { className: "absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4",
//@ts-ignore
style: { color: theme.colors.text.muted } }), _jsx("input", { type: "text", placeholder: "Search commands...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full pl-10 pr-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500", style: {
backgroundColor: theme.colors.background,
borderColor: theme.colors.border,
color: theme.colors.text.primary
} }), searchQuery && (_jsx("button", { onClick: () => setSearchQuery(''), className: "absolute right-3 top-1/2 transform -translate-y-1/2", children: _jsx(ClearIcon, { className: "w-4 h-4", style: { color: theme.colors.text.muted } }) }))] })), shouldShowFilters && (_jsxs("div", { className: "flex items-center space-x-2", children: [_jsxs("button", { onClick: () => setShowFilterPanel(!showFilterPanel), className: `flex items-center space-x-1 px-3 py-1 text-sm rounded transition-colors ${showFilterPanel ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-100'}`, children: [_jsx(FilterIcon, { className: "w-4 h-4" }), _jsx("span", { children: "Filters" })] }), (filters.dateFrom || filters.dateTo || filters.commandType || filters.success !== undefined || filters.provider) && (_jsxs("button", { onClick: clearFilters, className: "flex items-center space-x-1 px-2 py-1 text-xs bg-red-100 text-red-700 rounded", children: [_jsx(ClearIcon, { className: "w-3 h-3" }), _jsx("span", { children: "Clear" })] }))] })), shouldShowFilters && showFilterPanel && (_jsxs("div", { className: "grid grid-cols-2 gap-3 p-3 bg-gray-50 rounded-lg", style: { backgroundColor: theme.colors.background }, children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium mb-1", style: { color: theme.colors.text.secondary }, children: "From Date" }), _jsx("input", { type: "date", value: filters.dateFrom ? filters.dateFrom.toISOString().split('T')[0] : '', onChange: (e) => setFilters({
dateFrom: e.target.value ? new Date(e.target.value) : undefined
}), className: "w-full px-2 py-1 text-xs border rounded", style: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border
} })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium mb-1", style: { color: theme.colors.text.secondary }, children: "To Date" }), _jsx("input", { type: "date", value: filters.dateTo ? filters.dateTo.toISOString().split('T')[0] : '', onChange: (e) => setFilters({
dateTo: e.target.value ? new Date(e.target.value) : undefined
}), className: "w-full px-2 py-1 text-xs border rounded", style: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border
} })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium mb-1", style: { color: theme.colors.text.secondary }, children: "Status" }), _jsxs("select", { value: filters.success === undefined ? 'all' : filters.success ? 'success' : 'failed', onChange: (e) => setFilters({
success: e.target.value === 'all' ? undefined : e.target.value === 'success'
}), className: "w-full px-2 py-1 text-xs border rounded", style: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border
}, children: [_jsx("option", { value: "all", children: "All" }), _jsx("option", { value: "success", children: "Successful" }), _jsx("option", { value: "failed", children: "Failed" })] })] }), visibility.showProviders && (_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium mb-1", style: { color: theme.colors.text.secondary }, children: "Provider" }), _jsxs("select", { value: filters.provider || 'all', onChange: (e) => setFilters({
provider: e.target.value === 'all' ? undefined : e.target.value
}), className: "w-full px-2 py-1 text-xs border rounded", style: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border
}, children: [_jsx("option", { value: "all", children: "All Providers" }), _jsx("option", { value: "openai", children: "OpenAI" }), _jsx("option", { value: "anthropic", children: "Anthropic" }), _jsx("option", { value: "google", children: "Google" }), _jsx("option", { value: "keywords", children: "Keywords" })] })] }))] }))] })), _jsx("div", { className: "flex-1 overflow-hidden", children: activeTab === 'history' ? (_jsx("div", { className: "h-full overflow-y-auto p-4", children: filteredHistory.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center h-32 text-center", children: [_jsx("div", { className: "text-gray-400 mb-2", children: _jsx(SearchIcon, { className: "w-8 h-8 mx-auto" }) }), _jsx("div", { className: "text-sm", style: { color: theme.colors.text.muted }, children: searchQuery || Object.keys(filters).length > 0
? 'No commands match your search'
: visibility.useGenericLabels
? 'No commands yet'
: 'No voice commands yet' })] })) : (_jsx("div", { className: "space-y-4", children: Object.entries(groupedHistory).map(([dateKey, entries]) => (_jsxs("div", { children: [_jsx("div", { className: "sticky top-0 bg-gray-50 px-2 py-1 rounded text-xs font-medium mb-2", style: { backgroundColor: theme.colors.background, color: theme.colors.text.secondary }, children: dateKey === new Date().toDateString() ? 'Today' : dateKey }), _jsx("div", { className: "space-y-2", children: entries.map((entry) => (_jsx("div", { className: "p-3 rounded-lg border hover:bg-gray-50 transition-colors group", style: {
backgroundColor: theme.colors.surface,
borderColor: theme.colors.border
}, children: _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center space-x-2 mb-1", children: [_jsx("span", { className: "font-medium text-sm", style: { color: theme.colors.text.primary }, children: entry.intent }), visibility.showConfidenceScores && (_jsxs("span", { className: "px-2 py-0.5 text-xs rounded-full", style: {
backgroundColor: `${getConfidenceColor(entry.confidence)}20`,
color: getConfidenceColor(entry.confidence)
}, children: [Math.round(entry.confidence * 100), "%"] })), entry.success ? (
// @ts-ignore
_jsx(CheckIcon, { className: "w-3 h-3", style: { color: theme.colors.success } })) : (
// @ts-ignore
_jsx(CloseIcon, { className: "w-3 h-3", style: { color: theme.colors.error } }))] }), _jsxs("div", { className: "text-sm mb-1", style: { color: theme.colors.text.secondary }, children: ["\"", entry.rawText, "\""] }), _jsxs("div", { className: "flex items-center space-x-3 text-xs", style: { color: theme.colors.text.muted }, children: [_jsx("span", { children: formatRelativeTime(entry.timestamp) }), entry.provider && visibility.showProviders && (_jsxs("span", { children: ["via ", entry.provider] })), entry.duration && visibility.showProcessingTimes && (_jsxs("span", { children: [entry.duration, "ms"] }))] })] }), _jsxs("div", { className: "flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [_jsx("button", { onClick: () => handleReplayCommand(entry.id), className: "p-1 rounded hover:bg-gray-200 transition-colors", title: "Replay Command", children: _jsx(PlayIcon, { className: "w-4 h-4", style: { color: theme.colors.text.secondary } }) }), visibility.showAdvancedSettings && (_jsx("button", { onClick: () => removeCommand(entry.id), className: "p-1 rounded hover:bg-gray-200 transition-colors", title: "Remove", children: _jsx(DeleteIcon, { className: "w-4 h-4", style: { color: theme.colors.text.secondary } }) }))] })] }) }, entry.id))) })] }, dateKey))) })) })) : (
// Stats Tab - only shown if analytics are enabled
shouldShowStats && (_jsxs("div", { className: "p-4 space-y-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "text-center p-3 bg-gray-50 rounded-lg", style: { backgroundColor: theme.colors.background }, children: [_jsx("div", { className: "text-2xl font-bold", style: { color: theme.colors.text.primary }, children: stats.totalCommands }), _jsx("div", { className: "text-sm", style: { color: theme.colors.text.secondary }, children: "Total Commands" })] }), _jsxs("div", { className: "text-center p-3 bg-gray-50 rounded-lg", style: { backgroundColor: theme.colors.background }, children: [_jsxs("div", { className: "text-2xl font-bold", style: { color: theme.colors.success }, children: [Math.round((stats.successfulCommands / stats.totalCommands) * 100) || 0, "%"] }), _jsx("div", { className: "text-sm", style: { color: theme.colors.text.secondary }, children: "Success Rate" })] }), visibility.showConfidenceScores && (_jsxs("div", { className: "text-center p-3 bg-gray-50 rounded-lg", style: { backgroundColor: theme.colors.background }, children: [_jsxs("div", { className: "text-2xl font-bold", style: { color: theme.colors.text.primary }, children: [Math.round(stats.averageConfidence * 100), "%"] }), _jsx("div", { className: "text-sm", style: { color: theme.colors.text.secondary }, children: "Avg Confidence" })] })), visibility.showProcessingTimes && (_jsxs("div", { className: "text-center p-3 bg-gray-50 rounded-lg", style: { backgroundColor: theme.colors.background }, children: [_jsxs("div", { className: "text-2xl font-bold", style: { color: theme.colors.text.primary }, children: [Math.round(stats.averageResponseTime), "ms"] }), _jsx("div", { className: "text-sm", style: { color: theme.colors.text.secondary }, children: "Avg Response Time" })] }))] }), stats.mostUsedCommands.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "font-medium mb-2", style: { color: theme.colors.text.primary }, children: "Most Used Commands" }), _jsx("div", { className: "space-y-2", children: stats.mostUsedCommands.slice(0, 5).map((command, index) => (_jsxs("div", { className: "flex items-center justify-between py-1", children: [_jsxs("span", { className: "text-sm", style: { color: theme.colors.text.secondary }, children: [index + 1, ". ", command.intent] }), _jsx("span", { className: "text-sm font-medium", style: { color: theme.colors.text.primary }, children: command.count })] }, command.intent))) })] })), visibility.showProviders && Object.keys(stats.commandsByProvider).length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "font-medium mb-2", style: { color: theme.colors.text.primary }, children: "By Provider" }), _jsx("div", { className: "space-y-2", children: Object.entries(stats.commandsByProvider).map(([provider, count]) => (_jsxs("div", { className: "flex items-center justify-between py-1", children: [_jsx("span", { className: "text-sm capitalize", style: { color: theme.colors.text.secondary }, children: provider }), _jsxs("span", { className: "text-sm font-medium", style: { color: theme.colors.text.primary }, children: [count, " (", Math.round((count / stats.totalCommands) * 100), "%)"] })] }, provider))) })] }))] }))) })] }));
};
export default VoiceHistoryPanel;