File size: 2,636 Bytes
f59fbe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import type { SlashCommandInfo } from "shared/legacy-sdk";

interface SlashCommandMenuProps {
  commands: SlashCommandInfo[];
  query: string;
  selectedIndex: number;
  onSelect: (name: string) => void;
  onHover: (index: number) => void;
}

function highlightMatch(text: string, query: string): React.ReactNode {
  if (!query) {
    return text;
  }

  const lowerText = text.toLowerCase();
  const lowerQuery = query.toLowerCase();
  const parts: React.ReactNode[] = [];
  let lastIdx = 0;
  let qi = 0;

  for (let i = 0; i < text.length && qi < lowerQuery.length; i++) {
    if (lowerText[i] === lowerQuery[qi]) {
      if (i > lastIdx) {
        parts.push(text.slice(lastIdx, i));
      }
      parts.push(
        <span key={i} className="text-foreground font-semibold">
          {text[i]}
        </span>,
      );
      lastIdx = i + 1;
      qi++;
    }
  }

  if (lastIdx < text.length) {
    parts.push(text.slice(lastIdx));
  }
  return parts.length > 0 ? parts : text;
}

export function SlashCommandMenu({ commands, query, selectedIndex, onSelect, onHover }: SlashCommandMenuProps) {
  const selectedRef = useRef<HTMLButtonElement>(null);
  const hoverSelectionRef = useRef<number | null>(null);

  useEffect(() => {
    if (hoverSelectionRef.current === selectedIndex) {
      hoverSelectionRef.current = null;
      return;
    }
    hoverSelectionRef.current = null;
    selectedRef.current?.scrollIntoView({ block: "nearest" });
  }, [selectedIndex]);

  const handleHover = (index: number) => {
    hoverSelectionRef.current = index;
    onHover(index);
  };

  if (commands.length === 0) {
    return <div className="rounded-md border bg-popover shadow-md p-3 text-xs text-muted-foreground text-center">No commands found</div>;
  }

  return (
    <div className="rounded-md border bg-popover shadow-md overflow-hidden">
      <div className="max-h-70 overflow-y-auto">
        {commands.map((cmd, idx) => (
          <button
            key={cmd.name}
            ref={idx === selectedIndex ? selectedRef : null}
            onClick={() => onSelect(cmd.name)}
            onMouseMove={() => handleHover(idx)}
            className={cn("w-full px-2 py-1.5 text-left flex items-center justify-between gap-3", idx === selectedIndex ? "bg-accent" : "hover:bg-accent/50")}
          >
            <span className="text-xs shrink-0">{highlightMatch(`/${cmd.name}`, query)}</span>
            <span className="text-[10px] text-muted-foreground truncate">{cmd.description}</span>
          </button>
        ))}
      </div>
    </div>
  );
}