θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import { useTranslation } from "react-i18next";
import PublishEntityModal from "@/components/CommunityHub/PublishEntityModal";

export const CMD_REGEX = new RegExp(/[^a-zA-Z0-9_-]/g);
export default function SlashPresets({ setShowing, sendCommand, promptRef }) {
export default function SlashPresets({
setShowing,
sendCommand,
promptRef,
highlightedSlashCommand,
presets,
setPresets,
}) {
const { t } = useTranslation();
const isActiveAgentSession = useIsAgentSessionActive();
const {
Expand All @@ -29,7 +36,7 @@ export default function SlashPresets({ setShowing, sendCommand, promptRef }) {
openModal: openPublishModal,
closeModal: closePublishModal,
} = useModal();
const [presets, setPresets] = useState([]);
// const [presets, setPresets] = useState([]);
const [selectedPreset, setSelectedPreset] = useState(null);
const [presetToPublish, setPresetToPublish] = useState(null);
const [searchParams] = useSearchParams();
Expand All @@ -49,7 +56,7 @@ export default function SlashPresets({ setShowing, sendCommand, promptRef }) {
!isAddModalOpen
)
openAddModal();
}, []);
}, [isAddModalOpen, openAddModal, searchParams]);

if (isActiveAgentSession) return null;

Expand All @@ -60,7 +67,7 @@ export default function SlashPresets({ setShowing, sendCommand, promptRef }) {

const handleSavePreset = async (preset) => {
const { error } = await System.createSlashCommandPreset(preset);
if (!!error) {
if (error) {
showToast(error, "error");
return false;
}
Expand All @@ -81,7 +88,7 @@ export default function SlashPresets({ setShowing, sendCommand, promptRef }) {
updatedPreset
);

if (!!error) {
if (error) {
showToast(error, "error");
return;
}
Expand Down Expand Up @@ -113,10 +120,11 @@ export default function SlashPresets({ setShowing, sendCommand, promptRef }) {

return (
<>
{presets.map((preset) => (
{presets.map((preset, index) => (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not do highlight operation with preset.id?

<PresetItem
key={preset.id}
preset={preset}
isHighlighted={highlightedSlashCommand === index + 1}
onUse={() => {
setShowing(false);
sendCommand({ text: `${preset.command} ` });
Expand Down Expand Up @@ -161,10 +169,18 @@ export default function SlashPresets({ setShowing, sendCommand, promptRef }) {
);
}

function PresetItem({ preset, onUse, onEdit, onPublish }) {
function PresetItem({ preset, onUse, onEdit, onPublish, isHighlighted }) {
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef(null);
const menuButtonRef = useRef(null);
const slashCommandItemRef = useRef(null);

// Scroll to the highlighted slash command
useEffect(() => {
if (slashCommandItemRef.current && isHighlighted) {
slashCommandItemRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [isHighlighted]);

useEffect(() => {
const handleClickOutside = (event) => {
Expand All @@ -186,8 +202,9 @@ function PresetItem({ preset, onUse, onEdit, onPublish }) {

return (
<button
ref={slashCommandItemRef}
onClick={onUse}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-row justify-start items-center relative"
className={`border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-row justify-start items-center relative ${isHighlighted ? "bg-theme-action-menu-item-hover" : ""}`}
>
<div className="flex-col text-left flex pointer-events-none flex-1 min-w-0">
<div className="text-theme-text-primary text-sm font-bold truncate">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
);
}

export function SlashCommands({ showing, setShowing, sendCommand, promptRef }) {
export function SlashCommands({
showing,
setShowing,
sendCommand,
promptRef,
highlightedSlashCommand,
slashCommands,
setSlashCommands,
}) {
const cmdRef = useRef(null);
useEffect(() => {
function listenForOutsideClick() {
Expand All @@ -56,20 +64,22 @@ export function SlashCommands({ showing, setShowing, sendCommand, promptRef }) {
ref={cmdRef}
className="w-[600px] bg-theme-action-menu-bg rounded-2xl flex shadow flex-col justify-start items-start gap-2.5 p-2 overflow-y-auto max-h-[300px] no-scroll"
>
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
<ResetCommand
isHighlighted={highlightedSlashCommand === 0}
sendCommand={sendCommand}
setShowing={setShowing}
/>
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
<SlashPresets
sendCommand={sendCommand}
setShowing={setShowing}
promptRef={promptRef}
highlightedSlashCommand={highlightedSlashCommand}
presets={slashCommands}
setPresets={setSlashCommands}
/>
</div>
</div>
</div>
);
}

export function useSlashCommands() {
const [showSlashCommand, setShowSlashCommand] = useState(false);
return { showSlashCommand, setShowSlashCommand };
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import React, { useEffect, useRef } from "react";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
import { useTranslation } from "react-i18next";

export default function ResetCommand({ setShowing, sendCommand }) {
export default function ResetCommand({
setShowing,
sendCommand,
isHighlighted,
}) {
const { t } = useTranslation();
const isActiveAgentSession = useIsAgentSessionActive();
const slashCommandItemRef = useRef(null);
// Scroll to the highlighted slash command
useEffect(() => {
if (isHighlighted) {
slashCommandItemRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [isHighlighted]);

if (isActiveAgentSession) return null; // cannot reset during active agent chat

return (
<button
ref={slashCommandItemRef}
onClick={() => {
setShowing(false);
sendCommand({ text: "/reset", autoSubmit: true });
}}
className="border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-col justify-start"
className={`border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-col justify-start ${isHighlighted ? "bg-theme-action-menu-item-hover" : ""}`}
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm font-bold">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useRef, useEffect } from "react";
import SlashCommandsButton, {
SlashCommands,
useSlashCommands,
// useSlashCommands,
} from "./SlashCommands";
import debounce from "lodash.debounce";
import { PaperPlaneRight } from "@phosphor-icons/react";
Expand All @@ -24,6 +24,7 @@ import {
import useTextSize from "@/hooks/useTextSize";
import { useTranslation } from "react-i18next";
import Appearance from "@/models/appearance";
import { useSlashCommands } from "@/hooks/useSlashCommands";

export const PROMPT_INPUT_ID = "primary-prompt-input";
export const PROMPT_INPUT_EVENT = "set_prompt_input";
Expand All @@ -40,10 +41,17 @@ export default function PromptInput({
const { isDisabled } = useIsDisabled();
const [promptInput, setPromptInput] = useState("");
const { showAgents, setShowAgents } = useAvailableAgents();
const { showSlashCommand, setShowSlashCommand } = useSlashCommands();
const {
showSlashCommand,
setShowSlashCommand,
handleKeyDown,
highlightedSlashCommand,
watchForSlash,
textareaRef,
slashCommands,
setSlashCommands,
} = useSlashCommands({ sendCommand });
const formRef = useRef(null);
const textareaRef = useRef(null);
const [_, setFocused] = useState(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed?

const undoStack = useRef([]);
const redoStack = useRef([]);
const { textSizeClass } = useTextSize();
Expand All @@ -61,8 +69,7 @@ export default function PromptInput({
}

useEffect(() => {
if (!!window)
window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
if (window) window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
return () =>
window?.removeEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);
}, []);
Expand All @@ -88,7 +95,6 @@ export default function PromptInput({
const debouncedSaveState = debounce(saveCurrentState, 250);

function handleSubmit(e) {
setFocused(false);
submit(e);
}

Expand All @@ -97,14 +103,6 @@ export default function PromptInput({
textareaRef.current.style.height = "auto";
}

function checkForSlash(e) {
const input = e.target.value;
if (input === "/") setShowSlashCommand(true);
if (showSlashCommand) setShowSlashCommand(false);
return;
}
const watchForSlash = debounce(checkForSlash, 300);

function checkForAt(e) {
const input = e.target.value;
if (input === "@") return setShowAgents(true);
Expand All @@ -121,6 +119,8 @@ export default function PromptInput({
// Is simple enter key press w/o shift key
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
// Don't submit if slash commands are showing - let the slash command handler take care of it
if (showSlashCommand) return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch

if (isStreaming || isDisabled) return; // Prevent submission if streaming or disabled
return submit(event);
}
Expand Down Expand Up @@ -243,12 +243,18 @@ export default function PromptInput({
}

return (
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center">
<div
onKeyDown={handleKeyDown}
className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center"
>
<SlashCommands
showing={showSlashCommand}
setShowing={setShowSlashCommand}
sendCommand={sendCommand}
promptRef={textareaRef}
highlightedSlashCommand={highlightedSlashCommand}
slashCommands={slashCommands}
setSlashCommands={setSlashCommands}
/>
<AvailableAgents
showing={showAgents}
Expand All @@ -274,9 +280,7 @@ export default function PromptInput({
handlePasteEvent(e);
}}
required={true}
onFocus={() => setFocused(true)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed?

onBlur={(e) => {
setFocused(false);
adjustTextArea(e);
}}
value={promptInput}
Expand Down
100 changes: 100 additions & 0 deletions frontend/src/hooks/useSlashCommands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import System from "@/models/system";
import debounce from "lodash.debounce";
import { useEffect, useRef, useState } from "react";

export function useSlashCommands({ sendCommand }) {
const [showSlashCommand, setShowSlashCommand] = useState(false);
const [highlightedSlashCommand, setHighlightedSlashCommand] = useState(1);
const [slashCommands, setSlashCommands] = useState([]);
// Ref to the textarea element, this is needed to apply focus to the textarea when slash commands are triggered by clicking the slash command button
const textareaRef = useRef(null);

// Check for slash command to show slash commands
function checkForSlash(e) {
const input = e.target.value;
if (input === "/") {
setShowSlashCommand(true);
setHighlightedSlashCommand(0); // Start with reset command highlighted
}
if (showSlashCommand) setShowSlashCommand(false);
return;
}
const watchForSlash = debounce(checkForSlash, 300);
const fetchSlashCommands = async () => {
const presets = await System.getSlashCommandPresets();
setSlashCommands(presets);
};

useEffect(() => {
fetchSlashCommands();
}, []);

// Focus the textarea when slash commands are shown, this is needed to enable keyboard navigation
useEffect(() => {
if (showSlashCommand) {
if (textareaRef.current) {
textareaRef.current.focus();
}
}
}, [showSlashCommand]);

// Handle key down events for slash command navigation and selection
function handleKeyDown(e) {
// Only handle arrow keys when slash commands are showing
if (!showSlashCommand) return;

if (e.key === "ArrowUp") {
e.preventDefault();
setHighlightedSlashCommand((prev) => {
if (prev <= 0) {
return slashCommands.length;
}
return prev - 1;
});
}
if (e.key === "ArrowDown") {
e.preventDefault();
setHighlightedSlashCommand((prev) => {
if (prev >= slashCommands.length) {
return 0;
}
return prev + 1;
});
}
if (e.key === "Enter" && showSlashCommand) {
e.preventDefault();

if (highlightedSlashCommand === 0) {
setShowSlashCommand(false);
sendCommand({ text: "/reset", autoSubmit: true });
// Reset command is selected
return;
}

// Check if it's a preset selection (index 1 and above)
const presetIndex = highlightedSlashCommand - 1;
if (presetIndex >= 0 && presetIndex < slashCommands.length) {
const preset = slashCommands[presetIndex];
setShowSlashCommand(false);
sendCommand({ text: `${preset.command} ` });
textareaRef?.current?.focus();
}
}

if (e.key === "Escape" && showSlashCommand) {
e.preventDefault();
setShowSlashCommand(false);
}
}
return {
slashCommands,
setSlashCommands,
showSlashCommand,
setShowSlashCommand,
highlightedSlashCommand,
setHighlightedSlashCommand,
watchForSlash,
handleKeyDown,
textareaRef,
};
}