mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-20 23:21:06 +02:00
wire / action picker in Composer with keyboard navigation
This commit is contained in:
parent
3be0988a7b
commit
c2644aa6a2
1 changed files with 92 additions and 2 deletions
|
|
@ -57,6 +57,7 @@ import {
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { UserMessage } from "@/components/assistant-ui/user-message";
|
import { UserMessage } from "@/components/assistant-ui/user-message";
|
||||||
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
|
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel";
|
||||||
|
import { ActionPicker, type ActionPickerRef } from "@/components/new-chat/action-picker";
|
||||||
import {
|
import {
|
||||||
DocumentMentionPicker,
|
DocumentMentionPicker,
|
||||||
type DocumentMentionPickerRef,
|
type DocumentMentionPickerRef,
|
||||||
|
|
@ -298,10 +299,13 @@ const Composer: FC = () => {
|
||||||
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
|
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
|
||||||
const setSidebarDocs = useSetAtom(sidebarSelectedDocumentsAtom);
|
const setSidebarDocs = useSetAtom(sidebarSelectedDocumentsAtom);
|
||||||
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
|
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
|
||||||
|
const [showActionPicker, setShowActionPicker] = useState(false);
|
||||||
const [mentionQuery, setMentionQuery] = useState("");
|
const [mentionQuery, setMentionQuery] = useState("");
|
||||||
|
const [actionQuery, setActionQuery] = useState("");
|
||||||
const editorRef = useRef<InlineMentionEditorRef>(null);
|
const editorRef = useRef<InlineMentionEditorRef>(null);
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
|
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
|
||||||
|
const actionPickerRef = useRef<ActionPickerRef>(null);
|
||||||
const { search_space_id, chat_id } = useParams();
|
const { search_space_id, chat_id } = useParams();
|
||||||
const aui = useAui();
|
const aui = useAui();
|
||||||
const hasAutoFocusedRef = useRef(false);
|
const hasAutoFocusedRef = useRef(false);
|
||||||
|
|
@ -421,9 +425,69 @@ const Composer: FC = () => {
|
||||||
}
|
}
|
||||||
}, [showDocumentPopover]);
|
}, [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(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(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 (showDocumentPopover) {
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
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)
|
// Submit message (blocked during streaming, document picker open, or AI responding to another user)
|
||||||
|
|
@ -520,6 +584,8 @@ const Composer: FC = () => {
|
||||||
placeholder={currentPlaceholder}
|
placeholder={currentPlaceholder}
|
||||||
onMentionTrigger={handleMentionTrigger}
|
onMentionTrigger={handleMentionTrigger}
|
||||||
onMentionClose={handleMentionClose}
|
onMentionClose={handleMentionClose}
|
||||||
|
onActionTrigger={handleActionTrigger}
|
||||||
|
onActionClose={handleActionClose}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
onDocumentRemove={handleDocumentRemove}
|
onDocumentRemove={handleDocumentRemove}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
|
@ -553,6 +619,30 @@ const Composer: FC = () => {
|
||||||
/>,
|
/>,
|
||||||
document.body
|
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} />
|
<ComposerAction isBlockedByOtherUser={isBlockedByOtherUser} />
|
||||||
<ConnectorIndicator showTrigger={false} />
|
<ConnectorIndicator showTrigger={false} />
|
||||||
<ConnectToolsBanner isThreadEmpty={isThreadEmpty} />
|
<ConnectToolsBanner isThreadEmpty={isThreadEmpty} />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue