diff --git a/dashboard/components/SimpleInput.js b/dashboard/components/SimpleInput.js index de0bb37..c900da3 100644 --- a/dashboard/components/SimpleInput.js +++ b/dashboard/components/SimpleInput.js @@ -1,4 +1,4 @@ -import { html, useState, useRef, useCallback } from '../lib/preact.js'; +import { html, useState, useRef, useCallback, useMemo, useEffect } from '../lib/preact.js'; import { getStatusMeta } from '../utils/status.js'; export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = null }) { @@ -6,7 +6,11 @@ export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = 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); // Detect if cursor is at a trigger position for autocomplete @@ -35,6 +39,67 @@ export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = return null; }, [autocompleteConfig]); + // Filter skills based on user input after trigger + const filteredSkills = useMemo(() => { + if (!autocompleteConfig || !triggerInfo) return []; + + const { skills } = autocompleteConfig; + const { filterText } = triggerInfo; + + let filtered = skills; + if (filterText) { + filtered = skills.filter(s => + s.name.toLowerCase().includes(filterText) + ); + } + + // Server pre-sorts, but re-sort after filtering for stability + return filtered.sort((a, b) => a.name.localeCompare(b.name)); + }, [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]); + + // 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(); @@ -66,15 +131,54 @@ export function SimpleInput({ sessionId, status, onRespond, autocompleteConfig = `}
+