web: keyboard shortcuts
This commit is contained in:
@@ -40,6 +40,10 @@ Default dev URL: `http://localhost:5173`
|
||||
- Composer adapts to the active item:
|
||||
- Chat sends `POST /v1/chat-completions/stream` (SSE).
|
||||
- Search sends `POST /v1/searches/:searchId/run/stream` (SSE).
|
||||
- Keyboard shortcuts:
|
||||
- `Cmd/Ctrl+J`: start a new chat.
|
||||
- `Shift+Cmd/Ctrl+J`: start a new search.
|
||||
- `Cmd/Ctrl+Up/Down`: move through the sidebar list.
|
||||
|
||||
Client API contract docs:
|
||||
- `../docs/api/rest.md`
|
||||
|
||||
@@ -1008,6 +1008,10 @@ export default function App() {
|
||||
if (selectedSearchSummary) return `${getSearchTitle(selectedSearchSummary)} — Sybil`;
|
||||
return "Sybil";
|
||||
}, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearch, selectedSearchSummary]);
|
||||
const primaryShortcutModifier = useMemo(() => {
|
||||
if (typeof navigator === "undefined") return "Ctrl";
|
||||
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform) ? "Cmd" : "Ctrl";
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = pageTitle;
|
||||
@@ -1035,6 +1039,56 @@ export default function App() {
|
||||
setIsMobileSidebarOpen(false);
|
||||
};
|
||||
|
||||
const selectAdjacentSidebarItem = (direction: -1 | 1) => {
|
||||
if (!filteredSidebarItems.length) return;
|
||||
|
||||
setError(null);
|
||||
setContextMenu(null);
|
||||
setDraftKind(null);
|
||||
setIsMobileSidebarOpen(false);
|
||||
setSelectedItem((current) => {
|
||||
const currentIndex = current
|
||||
? filteredSidebarItems.findIndex((item) => item.kind === current.kind && item.id === current.id)
|
||||
: -1;
|
||||
const fallbackIndex = direction > 0 ? 0 : filteredSidebarItems.length - 1;
|
||||
const nextIndex =
|
||||
currentIndex < 0
|
||||
? fallbackIndex
|
||||
: Math.min(filteredSidebarItems.length - 1, Math.max(0, currentIndex + direction));
|
||||
const nextItem = filteredSidebarItems[nextIndex];
|
||||
return { kind: nextItem.kind, id: nextItem.id };
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const hasPrimaryModifier = event.metaKey || event.ctrlKey;
|
||||
if (!hasPrimaryModifier || event.altKey) return;
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
if (key === "j") {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
handleCreateSearch();
|
||||
} else {
|
||||
handleCreateChat();
|
||||
}
|
||||
focusComposer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
selectAdjacentSidebarItem(event.key === "ArrowUp" ? -1 : 1);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [filteredSidebarItems, isAuthenticated]);
|
||||
|
||||
const openContextMenu = (event: MouseEvent, item: SidebarSelection) => {
|
||||
event.preventDefault();
|
||||
const menuWidth = 160;
|
||||
@@ -1632,10 +1686,16 @@ export default function App() {
|
||||
<Button className="h-11 w-full justify-start gap-3 text-[15px]" onClick={handleCreateChat}>
|
||||
<Plus className="h-4 w-4" />
|
||||
New chat
|
||||
<span className="ml-auto rounded-md border border-violet-100/12 bg-white/5 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/52">
|
||||
{primaryShortcutModifier} J
|
||||
</span>
|
||||
</Button>
|
||||
<Button className="h-10 w-full justify-start gap-3" variant="secondary" onClick={handleCreateSearch}>
|
||||
<Search className="h-4 w-4" />
|
||||
New search
|
||||
<span className="ml-auto rounded-md border border-violet-100/10 bg-white/[0.035] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/44">
|
||||
Shift {primaryShortcutModifier} J
|
||||
</span>
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-200/58" />
|
||||
|
||||
Reference in New Issue
Block a user