File size: 4,461 Bytes
c09f67c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"use client";

import { useChatActions, useChatId } from "@ai-sdk-tools/store";
import { AnimatedSizeContainer } from "@midday/ui/animated-size-container";
import { cn } from "@midday/ui/cn";
import { Icons } from "@midday/ui/icons";
import { type RefObject, useEffect, useRef } from "react";
import { useOnClickOutside } from "usehooks-ts";
import { useChatInterface } from "@/hooks/use-chat-interface";
import { useChatStore } from "@/store/chat";

export function CommandMenu() {
  const commandListRef = useRef<HTMLDivElement>(null);
  const {
    filteredCommands,
    selectedCommandIndex,
    showCommands,
    handleCommandSelect,
    resetCommandState,
    setInput,
    setShowCommands,
  } = useChatStore();

  const { sendMessage } = useChatActions();
  const chatId = useChatId();
  const { setChatId } = useChatInterface();

  // Close command menu when clicking outside (but not on the toggle button or input toolbar buttons)
  useOnClickOutside(commandListRef as RefObject<HTMLElement>, (event) => {
    if (showCommands) {
      const target = event.target as Element;
      const isToggleButton = target.closest("[data-suggested-actions-toggle]");
      // Don't close if clicking on buttons within the PromptInput toolbar
      // Check if the clicked element is a button or inside a button
      const clickedButton = target.closest("button");
      const isToolbarButton =
        clickedButton !== null &&
        (clickedButton.closest("form") !== null ||
          clickedButton.type === "button" ||
          clickedButton.type === "submit");

      // Only close if it's not the toggle button or toolbar buttons
      if (!isToggleButton && !isToolbarButton) {
        setShowCommands(false);
      }
    }
  });

  const handleCommandExecution = (command: any) => {
    if (!chatId) return;

    setChatId(chatId);

    sendMessage({
      role: "user",
      parts: [{ type: "text", text: command.title }],
      metadata: {
        toolCall: {
          toolName: command.toolName,
          toolParams: command.toolParams,
        },
      },
    });

    setInput("");
    resetCommandState();
  };

  // Scroll selected command into view
  useEffect(() => {
    if (commandListRef.current && showCommands) {
      const selectedElement = commandListRef.current.querySelector(
        `[data-index="${selectedCommandIndex}"]`,
      );
      if (selectedElement) {
        selectedElement.scrollIntoView({ block: "nearest" });
      }
    }
  }, [selectedCommandIndex, showCommands]);

  if (!showCommands || filteredCommands.length === 0) return null;

  return (
    <div
      ref={commandListRef}
      data-command-menu
      className="absolute bottom-full left-0 right-0 mb-2 w-full z-30"
    >
      <AnimatedSizeContainer
        height
        className="bg-[#f7f7f7]/85 dark:bg-[#171717]/85 backdrop-blur-lg max-h-80 overflow-y-auto"
        transition={{
          type: "spring",
          duration: 0.2,
          bounce: 0.1,
          ease: "easeOut",
        }}
        style={{
          transformOrigin: "bottom center",
        }}
      >
        <div className="p-2">
          {filteredCommands.map((command, index) => {
            const isActive = selectedCommandIndex === index;
            return (
              <div
                key={`${command.command}-${index}`}
                className={cn(
                  "px-2 py-2 text-sm cursor-pointer transition-colors flex items-center justify-between group",
                  isActive
                    ? "bg-black/5 dark:bg-white/5"
                    : "hover:bg-black/5 dark:hover:bg-white/5",
                )}
                onMouseDown={(e) => {
                  // Prevent input from losing focus when clicking on command
                  e.preventDefault();
                }}
                onClick={() => handleCommandExecution(command)}
                data-index={index}
              >
                <div>
                  <span className="text-[#666] ml-2">{command.title}</span>
                </div>
                {isActive && (
                  <span className="material-icons-outlined text-sm opacity-50 group-hover:opacity-100 text-gray-600 dark:text-gray-400 group-hover:text-black dark:group-hover:text-white">
                    <Icons.ArrowForward />
                  </span>
                )}
              </div>
            );
          })}
        </div>
      </AnimatedSizeContainer>
    </div>
  );
}