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>
);
}
|