UNPKG

@voice-ai-workforce/react

Version:

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

175 lines (174 loc) 23.2 kB
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;