Agently / web /src /components /chat /SlashCommandsDropdown.jsx
CI Deploy
Deploy 2026-05-15 14:25 UTC
7949d20
Raw
History Blame Contribute Delete
5.37 kB
import { useState, useEffect, useRef } from 'react'
const COMMANDS = [
{
id: 'summarize',
label: 'Summarize',
description: 'Summarize the entire conversation',
// icon: '📄',
},
{
id: 'deepThink',
label: 'Deep Think',
description: 'Research thoroughly from multiple sources',
// icon: '🔍',
},
]
/**
* Detect if the input value is a slash command.
* Returns the matched command and any text after the command name.
*/
export function parseSlashCommand(input) {
if (!input || !input.startsWith('/')) return null
const trimmed = input.slice(1).trim() // remove leading /
if (!trimmed) {
// Just "/" typed, showing all commands
return { partial: '', matched: null, fullText: '' }
}
// Split on first space to get command and args
const spaceIdx = trimmed.indexOf(' ')
const cmdPart = spaceIdx >= 0 ? trimmed.slice(0, spaceIdx) : trimmed
const argsPart = spaceIdx >= 0 ? trimmed.slice(spaceIdx + 1) : ''
// Find exact match
const exact = COMMANDS.find((c) => c.id === cmdPart)
if (exact) return { partial: cmdPart, matched: exact, fullText: argsPart }
// Find partial matches for filtering
return { partial: cmdPart, matched: null, fullText: '' }
}
export default function SlashCommandsDropdown({ inputValue, onSelect, onClose }) {
const [selectedIndex, setSelectedIndex] = useState(0)
const dropdownRef = useRef(null)
const parsed = parseSlashCommand(inputValue)
// Filter commands based on partial input
const filtered = parsed
? COMMANDS.filter(
(c) =>
!parsed.partial ||
c.id.startsWith(parsed.partial.toLowerCase()),
)
: []
const visible = parsed !== null && filtered.length > 0
useEffect(() => {
setSelectedIndex(0)
}, [inputValue])
useEffect(() => {
if (!visible) return
const handleKeyDown = (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
setSelectedIndex((prev) => (prev + 1) % filtered.length)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setSelectedIndex((prev) => (prev - 1 + filtered.length) % filtered.length)
} else if (e.key === 'Enter' || e.key === 'Tab') {
if (filtered[selectedIndex]) {
e.preventDefault()
onSelect(filtered[selectedIndex])
}
} else if (e.key === 'Escape') {
e.preventDefault()
onClose?.()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [visible, filtered, selectedIndex, onSelect, onClose])
if (!visible) return null
return (
<>
{/* Backdrop to detect clicks outside */}
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 90,
}}
onClick={onClose}
/>
<div
ref={dropdownRef}
className="slash-commands-dropdown"
style={{
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: 0,
background: '#ffffff',
border: '1px solid #e5e7eb',
borderRadius: '12px',
boxShadow: '0 10px 25px rgba(0,0,0,0.1)',
zIndex: 100,
overflow: 'hidden',
animation: 'slideUp 0.15s ease-out',
}}
>
<div style={{
padding: '10px 14px',
borderBottom: '1px solid #f3f4f6',
fontSize: '0.75rem',
color: '#9ca3af',
textTransform: 'uppercase',
letterSpacing: '0.05em',
fontWeight: 600,
}}>
Commands
</div>
{filtered.map((cmd, idx) => (
<button
key={cmd.id}
onClick={() => onSelect(cmd)}
onMouseEnter={() => setSelectedIndex(idx)}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
width: '100%',
padding: '10px 14px',
border: 'none',
background: idx === selectedIndex ? '#f3f4f6' : 'transparent',
cursor: 'pointer',
textAlign: 'left',
transition: 'background 0.1s ease',
fontFamily: '"Computer Modern Serif", serif',
}}
>
<span style={{ fontSize: '1.1rem', flexShrink: 0 }}>
{cmd.icon}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontWeight: 600,
fontSize: '0.9rem',
color: '#0b0b0d',
}}>
<span style={{ color: '#6b7280', fontWeight: 400 }}>/</span>
{cmd.label}
</div>
<div style={{
fontSize: '0.8rem',
color: '#6b7280',
marginTop: '2px',
}}>
{cmd.description}
</div>
</div>
<kbd style={{
fontSize: '0.7rem',
color: '#9ca3af',
background: '#f3f4f6',
padding: '2px 6px',
borderRadius: '4px',
border: '1px solid #e5e7eb',
fontFamily: '"Computer Modern Serif", serif',
}}>
</kbd>
</button>
))}
</div>
</>
)
}