Select bulk opponent

This commit is contained in:
2025-11-03 16:40:59 -08:00
parent bfbd9d35bc
commit 0db5b7a8f1
7 changed files with 413 additions and 12 deletions

View File

@@ -0,0 +1,191 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { ChevronDown, Search } from "lucide-react";
import clsx from "clsx";
import styles from "@/styles/AgentSelector.module.css";
interface AgentSelectorProps {
agents: string[];
selectedAgent: string;
onSelect: (agent: string) => void;
placeholder?: string;
}
export function AgentSelector({
agents,
selectedAgent,
onSelect,
placeholder = "Select an agent..."
}: AgentSelectorProps) {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [highlightedIndex, setHighlightedIndex] = useState(0);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const searchRef = useRef<HTMLInputElement>(null);
const filteredAgents = agents.filter(agent =>
agent.toLowerCase().includes(searchTerm.toLowerCase())
);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
setSearchTerm("");
}
}
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
useEffect(() => {
if (isOpen && searchRef.current) {
searchRef.current.focus();
}
}, [isOpen]);
useEffect(() => {
setHighlightedIndex(0);
}, [searchTerm]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!isOpen) {
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
e.preventDefault();
updateDropdownPosition();
setIsOpen(true);
}
return;
}
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setHighlightedIndex(prev =>
prev < filteredAgents.length - 1 ? prev + 1 : 0
);
break;
case "ArrowUp":
e.preventDefault();
setHighlightedIndex(prev =>
prev > 0 ? prev - 1 : filteredAgents.length - 1
);
break;
case "Enter":
e.preventDefault();
if (filteredAgents[highlightedIndex]) {
onSelect(filteredAgents[highlightedIndex]);
setIsOpen(false);
setSearchTerm("");
}
break;
case "Escape":
e.preventDefault();
setIsOpen(false);
setSearchTerm("");
break;
}
};
const handleSelect = (agent: string) => {
onSelect(agent);
setIsOpen(false);
setSearchTerm("");
};
const updateDropdownPosition = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + 2,
left: rect.left,
width: rect.width
});
}
};
const handleOpen = () => {
updateDropdownPosition();
setIsOpen(!isOpen);
};
return (
<div className={styles.agentSelector} ref={dropdownRef}>
<button
ref={triggerRef}
className={clsx(styles.trigger, isOpen && styles.triggerOpen)}
type="button"
onClick={handleOpen}
onKeyDown={handleKeyDown}
aria-expanded={isOpen}
aria-haspopup="listbox"
>
<span className={styles.triggerText}>
{selectedAgent || placeholder}
</span>
<ChevronDown
size={16}
className={clsx(styles.chevron, isOpen && styles.chevronOpen)}
/>
</button>
{isOpen && (
<div
className={styles.dropdown}
style={{
top: dropdownPosition.top,
left: dropdownPosition.left,
width: Math.max(dropdownPosition.width, 200)
}}
>
<div className={styles.searchContainer}>
<Search size={16} className={styles.searchIcon} />
<input
ref={searchRef}
type="text"
className={styles.searchInput}
placeholder="Search agents..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<ul className={styles.agentList} role="listbox">
{filteredAgents.length === 0 ? (
<li className={styles.noResults}>No agents found</li>
) : (
filteredAgents.map((agent, index) => (
<li
key={agent}
className={clsx(
styles.agentOption,
agent === selectedAgent && styles.selected,
index === highlightedIndex && styles.highlighted
)}
onClick={() => handleSelect(agent)}
role="option"
aria-selected={agent === selectedAgent}
>
{agent}
</li>
))
)}
</ul>
</div>
)}
</div>
);
}