import { html, useState, useRef, useCallback, useMemo, useEffect } from '../lib/preact.js'; import { getStatusMeta } from '../utils/status.js'; import { getTriggerInfo as _getTriggerInfo, filteredSkills as _filteredSkills } from '../utils/autocomplete.js'; export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = null }) { const [text, setText] = useState(''); const [focused, setFocused] = useState(false); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const [triggerInfo, setTriggerInfo] = useState(null); const [showAutocomplete, setShowAutocomplete] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const textareaRef = useRef(null); const autocompleteRef = useRef(null); const meta = getStatusMeta(status); const getTriggerInfo = useCallback((value, cursorPos) => { return _getTriggerInfo(value, cursorPos, autocompleteConfig); }, [autocompleteConfig]); const filteredSkills = useMemo(() => { return _filteredSkills(autocompleteConfig, triggerInfo); }, [autocompleteConfig, triggerInfo]); // Show/hide autocomplete based on trigger detection useEffect(() => { const shouldShow = triggerInfo !== null; setShowAutocomplete(shouldShow); // Reset selection when dropdown opens if (shouldShow) { setSelectedIndex(0); } }, [triggerInfo]); // Clamp selectedIndex when filtered list changes useEffect(() => { if (filteredSkills.length > 0 && selectedIndex >= filteredSkills.length) { setSelectedIndex(filteredSkills.length - 1); } }, [filteredSkills.length, selectedIndex]); // Click outside dismisses dropdown useEffect(() => { if (!showAutocomplete) return; const handleClickOutside = (e) => { if (autocompleteRef.current && !autocompleteRef.current.contains(e.target) && textareaRef.current && !textareaRef.current.contains(e.target)) { setShowAutocomplete(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [showAutocomplete]); // Scroll selected item into view when navigating with arrow keys useEffect(() => { if (showAutocomplete && autocompleteRef.current) { const container = autocompleteRef.current; const selectedEl = container.children[selectedIndex]; if (selectedEl) { selectedEl.scrollIntoView({ block: 'nearest' }); } } }, [selectedIndex, showAutocomplete]); // Insert a selected skill into the text const insertSkill = useCallback((skill) => { if (!triggerInfo || !autocompleteConfig) return; const { trigger } = autocompleteConfig; const { replaceStart, replaceEnd } = triggerInfo; const before = text.slice(0, replaceStart); const after = text.slice(replaceEnd); const inserted = `${trigger}${skill.name} `; setText(before + inserted + after); setShowAutocomplete(false); setTriggerInfo(null); // Move cursor after inserted text const newCursorPos = replaceStart + inserted.length; setTimeout(() => { if (textareaRef.current) { textareaRef.current.selectionStart = newCursorPos; textareaRef.current.selectionEnd = newCursorPos; textareaRef.current.focus(); } }, 0); }, [text, triggerInfo, autocompleteConfig]); const handleSubmit = async (e) => { e.preventDefault(); e.stopPropagation(); if (text.trim() && !sending) { setSending(true); setError(null); try { await onRespond(sessionId, text.trim(), true, 0); setText(''); } catch (err) { setError('Failed to send message'); console.error('SimpleInput send error:', err); } finally { setSending(false); // Refocus the textarea after submission // Use setTimeout to ensure React has re-rendered with disabled=false setTimeout(() => { textareaRef.current?.focus(); }, 0); } } }; return html`
`; }