規則選單

實作 /command 自動完成選單以便快速存取規則。

實作一個 /rule 指令選單,讓使用者可以透過自動完成和鍵盤導航快速觸發預定義的規則。

概觀

當使用者輸入 / 時,顯示可用規則列表並支援搜尋過濾和鍵盤導航。

取得規則

首先,從 Config API 取得規則列表:

取得規則
const [rules, setRules] = useState<Rule[]>([]);

useEffect(() => {
  fetch("/api/lens/agent/config")
    .then((res) => res.json())
    .then((data) => {
      if (data.success) {
        setRules(data.config.rules || []);
      }
    });
}, []);

規則選單狀態

狀態管理
const [showRulesMenu, setShowRulesMenu] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const [input, setInput] = useState("");

過濾邏輯

過濾規則
// Get search string after /
const slashQuery = input.startsWith("/")
  ? input.slice(1).toLowerCase()
  : "";

// Filter rules
const filteredRules = showRulesMenu
  ? rules.filter(
      (rule) =>
        rule.name.toLowerCase().includes(slashQuery) ||
        (rule.displayName || "").toLowerCase().includes(slashQuery)
    )
  : [];

輸入變更處理器

處理輸入變更
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  const value = e.target.value;
  setInput(value);

  // Show menu when starting with "/" and no space
  if (value.startsWith("/") && !value.includes(" ")) {
    setShowRulesMenu(true);
    setSelectedIndex(0);
  } else {
    setShowRulesMenu(false);
  }
};

鍵盤導航

鍵盤處理
const handleKeyDown = (e: React.KeyboardEvent) => {
  if (showRulesMenu && filteredRules.length > 0) {
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        setSelectedIndex((prev) => (prev + 1) % filteredRules.length);
        return;

      case "ArrowUp":
        e.preventDefault();
        setSelectedIndex(
          (prev) => (prev - 1 + filteredRules.length) % filteredRules.length
        );
        return;

      case "Enter":
      case "Tab":
        e.preventDefault();
        selectRule(filteredRules[selectedIndex]);
        return;

      case "Escape":
        e.preventDefault();
        setShowRulesMenu(false);
        return;
    }
  }

  // Normal Enter to send
  if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
    e.preventDefault();
    handleSend();
  }
};

選擇規則

規則選擇
const selectRule = (rule: Rule) => {
  // Set input to "/ruleName "
  setInput(`/${rule.name} `);
  setShowRulesMenu(false);
  inputRef.current?.focus();
};

渲染選單

UI 渲染
{/* Rules Menu */}
{showRulesMenu && filteredRules.length > 0 && (
  <div className="absolute bottom-full left-0 right-0 mb-2 bg-white border rounded-lg shadow-lg max-h-64 overflow-auto">
    {/* Keyboard hints */}
    <div className="px-3 py-2 text-xs text-gray-400 border-b flex gap-4">
      <span>↑↓ 選擇</span>
      <span>Enter 確認</span>
      <span>Esc 關閉</span>
    </div>

    {/* Rules list */}
    {filteredRules.map((rule, index) => (
      <div
        key={rule.id}
        className={`px-3 py-2 cursor-pointer transition ${
          index === selectedIndex
            ? "bg-blue-50 text-blue-700"
            : "hover:bg-gray-50"
        }`}
        onClick={() => selectRule(rule)}
        onMouseEnter={() => setSelectedIndex(index)}
      >
        <div className="flex items-center gap-2">
          <span className="font-mono text-sm text-blue-500">
            /{rule.name}
          </span>
          <span className="text-gray-700">
            {rule.displayName || rule.name}
          </span>
        </div>
        {rule.description && (
          <p className="text-xs text-gray-500 mt-1">
            {rule.description}
          </p>
        )}
      </div>
    ))}
  </div>
)}

{/* No results */}
{showRulesMenu && filteredRules.length === 0 && (
  <div className="absolute bottom-full left-0 right-0 mb-2 bg-white border rounded-lg shadow-lg p-4 text-center text-gray-400">
    沒有符合的規則
  </div>
)}

規則型別

型別定義
interface Rule {
  id: string;
  name: string;
  displayName?: string;
  description?: string;
  prompt: string;
}

Tip

規則在 Lens OS 主控台中設定,並透過 Config API 自動同步到你的應用程式。