wire / action picker in Composer with keyboard navigation

This commit is contained in:
CREDO23 2026-03-28 23:20:10 +02:00
parent 3be0988a7b
commit c2644aa6a2

View file

@ -57,6 +57,7 @@ import {
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { UserMessage } from "@/components/assistant-ui/user-message";
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
import { ActionPicker, type ActionPickerRef } from "@/components/new-chat/action-picker";
import {
DocumentMentionPicker,
type DocumentMentionPickerRef,
@ -298,10 +299,13 @@ const Composer: FC = () => {
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
const setSidebarDocs = useSetAtom(sidebarSelectedDocumentsAtom);
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
const [showActionPicker, setShowActionPicker] = useState(false);
const [mentionQuery, setMentionQuery] = useState("");
const [actionQuery, setActionQuery] = useState("");
const editorRef = useRef<InlineMentionEditorRef>(null);
const editorContainerRef = useRef<HTMLDivElement>(null);
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
const actionPickerRef = useRef<ActionPickerRef>(null);
const { search_space_id, chat_id } = useParams();
const aui = useAui();
const hasAutoFocusedRef = useRef(false);
@ -421,9 +425,69 @@ const Composer: FC = () => {
}
}, [showDocumentPopover]);
// Keyboard navigation for document picker (arrow keys, Enter, Escape)
// Open action picker when / is triggered
const handleActionTrigger = useCallback((query: string) => {
setShowActionPicker(true);
setActionQuery(query);
}, []);
// Close action picker and reset query
const handleActionClose = useCallback(() => {
if (showActionPicker) {
setShowActionPicker(false);
setActionQuery("");
}
}, [showActionPicker]);
// Handle action selection: prepend prompt template and auto-submit
const handleActionSelect = useCallback(
(action: { name: string; prompt: string; mode: "transform" | "explore" }) => {
setShowActionPicker(false);
setActionQuery("");
if (editorRef.current) {
const text = editorRef.current.getText();
// Remove the /query from the text
const slashIndex = text.lastIndexOf("/");
const userText = slashIndex !== -1 ? text.substring(0, slashIndex).trim() : text;
const finalPrompt = action.prompt.replace("{selection}", userText);
aui.composer().setText(finalPrompt);
aui.composer().send();
editorRef.current.clear();
setMentionedDocuments([]);
setSidebarDocs([]);
}
},
[aui, setMentionedDocuments, setSidebarDocs]
);
// Keyboard navigation for document/action picker (arrow keys, Enter, Escape)
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (showActionPicker) {
if (e.key === "ArrowDown") {
e.preventDefault();
actionPickerRef.current?.moveDown();
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
actionPickerRef.current?.moveUp();
return;
}
if (e.key === "Enter") {
e.preventDefault();
actionPickerRef.current?.selectHighlighted();
return;
}
if (e.key === "Escape") {
e.preventDefault();
setShowActionPicker(false);
setActionQuery("");
return;
}
}
if (showDocumentPopover) {
if (e.key === "ArrowDown") {
e.preventDefault();
@ -448,7 +512,7 @@ const Composer: FC = () => {
}
}
},
[showDocumentPopover]
[showDocumentPopover, showActionPicker]
);
// Submit message (blocked during streaming, document picker open, or AI responding to another user)
@ -520,6 +584,8 @@ const Composer: FC = () => {
placeholder={currentPlaceholder}
onMentionTrigger={handleMentionTrigger}
onMentionClose={handleMentionClose}
onActionTrigger={handleActionTrigger}
onActionClose={handleActionClose}
onChange={handleEditorChange}
onDocumentRemove={handleDocumentRemove}
onSubmit={handleSubmit}
@ -553,6 +619,30 @@ const Composer: FC = () => {
/>,
document.body
)}
{showActionPicker &&
typeof document !== "undefined" &&
createPortal(
<ActionPicker
ref={actionPickerRef}
onSelect={handleActionSelect}
onDone={() => {
setShowActionPicker(false);
setActionQuery("");
}}
externalSearch={actionQuery}
containerStyle={{
position: "fixed",
bottom: editorContainerRef.current
? `${window.innerHeight - editorContainerRef.current.getBoundingClientRect().top + 8}px`
: "200px",
left: editorContainerRef.current
? `${editorContainerRef.current.getBoundingClientRect().left}px`
: "50%",
zIndex: 50,
}}
/>,
document.body
)}
<ComposerAction isBlockedByOtherUser={isBlockedByOtherUser} />
<ConnectorIndicator showTrigger={false} />
<ConnectToolsBanner isThreadEmpty={isThreadEmpty} />