import React, { useState, KeyboardEvent, useEffect } from 'react'; type InputMode = 'ASCII' | 'HEX'; interface InputBarProps { onSend: (data: Uint8Array) => void; disabled: boolean; } const InputBar: React.FC = ({ onSend, disabled }) => { const [text, setText] = useState(''); const [stx, setStx] = useState(''); const [etx, setEtx] = useState(''); const [mode, setMode] = useState('ASCII'); // Error states for validation const [errors, setErrors] = useState({ stx: false, text: false, etx: false }); // Command History State const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); // Clear errors when mode changes useEffect(() => { setErrors({ stx: false, text: false, etx: false }); }, [mode]); const isValidHex = (str: string): boolean => { // Allow empty string, hex digits, and spaces. // Checks if there is any character that is NOT 0-9, a-f, A-F, or whitespace return !/[^0-9A-Fa-f\s]/.test(str); }; const parseString = (str: string, mode: InputMode): Uint8Array => { if (mode === 'HEX') { // Remove spaces and non-hex characters (cleaning is still done for safe parsing) const clean = str.replace(/[^0-9A-Fa-f]/g, ''); if (clean.length === 0) return new Uint8Array(0); const bytes = new Uint8Array(Math.ceil(clean.length / 2)); for (let i = 0; i < bytes.length; i++) { const hexPair = clean.slice(i * 2, i * 2 + 2).padEnd(2, '0'); bytes[i] = parseInt(hexPair, 16); } return bytes; } else { // ASCII Mode with Escape Sequence Parsing const unescaped = str .replace(/\\r/g, '\r') .replace(/\\n/g, '\n') .replace(/\\t/g, '\t') .replace(/\\0/g, '\0') .replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))); return new TextEncoder().encode(unescaped); } }; const handleSend = () => { // Validation for HEX mode if (mode === 'HEX') { const stxValid = isValidHex(stx); const textValid = isValidHex(text); const etxValid = isValidHex(etx); setErrors({ stx: !stxValid, text: !textValid, etx: !etxValid }); if (!stxValid || !textValid || !etxValid) { return; // Stop sending if validation fails } } try { const stxBytes = stx ? parseString(stx, mode) : new Uint8Array(0); const bodyBytes = text ? parseString(text, mode) : new Uint8Array(0); const etxBytes = etx ? parseString(etx, mode) : new Uint8Array(0); if (stxBytes.length === 0 && bodyBytes.length === 0 && etxBytes.length === 0) return; const totalLength = stxBytes.length + bodyBytes.length + etxBytes.length; const result = new Uint8Array(totalLength); result.set(stxBytes, 0); result.set(bodyBytes, stxBytes.length); result.set(etxBytes, stxBytes.length + bodyBytes.length); onSend(result); // Add to history if (text.trim() !== '') { setHistory(prev => { const newHistory = [...prev]; if (newHistory[newHistory.length - 1] !== text) { newHistory.push(text); } if (newHistory.length > 50) return newHistory.slice(-50); return newHistory; }); } setText(''); setHistoryIndex(-1); } catch (e) { console.error("Parse error", e); alert("Error parsing input data."); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { handleSend(); } else if (e.key === 'ArrowUp') { e.preventDefault(); if (history.length === 0) return; setHistoryIndex(prevIndex => { const newIndex = prevIndex === -1 ? history.length - 1 : Math.max(0, prevIndex - 1); setText(history[newIndex]); // Clear text error when recalling history setErrors(prev => ({ ...prev, text: false })); return newIndex; }); } else if (e.key === 'ArrowDown') { e.preventDefault(); if (history.length === 0 || historyIndex === -1) return; setHistoryIndex(prevIndex => { if (prevIndex >= history.length - 1) { setText(''); return -1; } else { const newIndex = prevIndex + 1; setText(history[newIndex]); // Clear text error when recalling history setErrors(prev => ({ ...prev, text: false })); return newIndex; } }); } }; const getBorderClass = (hasError: boolean) => { return hasError ? "border-red-500 focus:ring-red-500 text-red-300 placeholder-red-300/50" : "border-gray-700 focus:ring-indigo-500 text-white placeholder-gray-700"; }; return (
{/* Mode Toggle */}
{/* STX Input */} { setStx(e.target.value); if (errors.stx) setErrors(prev => ({ ...prev, stx: false })); }} disabled={disabled} placeholder="STX" className={`w-14 bg-gray-900 text-xs border rounded px-2 py-2 focus:ring-1 outline-none text-center font-mono transition-colors ${ errors.stx ? "border-red-500 focus:ring-red-500 text-red-400 placeholder-red-400/50" : "border-gray-700 focus:ring-indigo-500 text-amber-200 placeholder-gray-700" }`} title="Start Byte (e.g. \x02 or 02)" /> {/* Main Input */} { setText(e.target.value); setHistoryIndex(-1); if (errors.text) setErrors(prev => ({ ...prev, text: false })); }} onKeyDown={handleKeyDown} disabled={disabled} placeholder={disabled ? "Disconnected..." : (mode === 'HEX' ? (errors.text ? "Invalid HEX!" : "AA BB CC...") : "Type message (supports \\r\\n)...")} className={`flex-1 bg-gray-900 text-sm border rounded px-3 py-2 focus:ring-2 outline-none disabled:opacity-50 font-mono transition-colors ${getBorderClass(errors.text)}`} /> {/* ETX Input */} { setEtx(e.target.value); if (errors.etx) setErrors(prev => ({ ...prev, etx: false })); }} disabled={disabled} placeholder="ETX" className={`w-14 bg-gray-900 text-xs border rounded px-2 py-2 focus:ring-1 outline-none text-center font-mono transition-colors ${ errors.etx ? "border-red-500 focus:ring-red-500 text-red-400 placeholder-red-400/50" : "border-gray-700 focus:ring-indigo-500 text-amber-200 placeholder-gray-700" }`} title="End Byte (e.g. \r\n or 0D 0A)" />
); }; export default InputBar;