mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat(settings): add DesktopShortcutsContent component for managing hotkeys and update user settings dialog
This commit is contained in:
parent
84145566e3
commit
fb2aecea46
4 changed files with 127 additions and 83 deletions
|
|
@ -1,9 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BrainCog, Power, Rocket, Zap } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,9 +22,6 @@ export function DesktopContent() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [enabled, setEnabled] = useState(true);
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
|
||||||
const [shortcuts, setShortcuts] = useState(DEFAULT_SHORTCUTS);
|
|
||||||
const [shortcutsLoaded, setShortcutsLoaded] = useState(false);
|
|
||||||
|
|
||||||
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
||||||
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
|
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -37,7 +32,6 @@ export function DesktopContent() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!api) {
|
if (!api) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setShortcutsLoaded(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,15 +42,13 @@ export function DesktopContent() {
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
api.getAutocompleteEnabled(),
|
api.getAutocompleteEnabled(),
|
||||||
api.getShortcuts?.() ?? Promise.resolve(null),
|
|
||||||
api.getActiveSearchSpace?.() ?? Promise.resolve(null),
|
api.getActiveSearchSpace?.() ?? Promise.resolve(null),
|
||||||
searchSpacesApiService.getSearchSpaces(),
|
searchSpacesApiService.getSearchSpaces(),
|
||||||
hasAutoLaunchApi ? api.getAutoLaunch() : Promise.resolve(null),
|
hasAutoLaunchApi ? api.getAutoLaunch() : Promise.resolve(null),
|
||||||
])
|
])
|
||||||
.then(([autoEnabled, config, spaceId, spaces, autoLaunch]) => {
|
.then(([autoEnabled, spaceId, spaces, autoLaunch]) => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setEnabled(autoEnabled);
|
setEnabled(autoEnabled);
|
||||||
if (config) setShortcuts(config);
|
|
||||||
setActiveSpaceId(spaceId);
|
setActiveSpaceId(spaceId);
|
||||||
if (spaces) setSearchSpaces(spaces);
|
if (spaces) setSearchSpaces(spaces);
|
||||||
if (autoLaunch) {
|
if (autoLaunch) {
|
||||||
|
|
@ -65,12 +57,10 @@ export function DesktopContent() {
|
||||||
setAutoLaunchSupported(autoLaunch.supported);
|
setAutoLaunchSupported(autoLaunch.supported);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setShortcutsLoaded(true);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setShortcutsLoaded(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -101,24 +91,6 @@ export function DesktopContent() {
|
||||||
await api.setAutocompleteEnabled(checked);
|
await api.setAutocompleteEnabled(checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateShortcut = (
|
|
||||||
key: "generalAssist" | "quickAsk" | "autocomplete",
|
|
||||||
accelerator: string
|
|
||||||
) => {
|
|
||||||
setShortcuts((prev) => {
|
|
||||||
const updated = { ...prev, [key]: accelerator };
|
|
||||||
api.setShortcuts?.({ [key]: accelerator }).catch(() => {
|
|
||||||
toast.error("Failed to update shortcut");
|
|
||||||
});
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
toast.success("Shortcut updated");
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetShortcut = (key: "generalAssist" | "quickAsk" | "autocomplete") => {
|
|
||||||
updateShortcut(key, DEFAULT_SHORTCUTS[key]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAutoLaunchToggle = async (checked: boolean) => {
|
const handleAutoLaunchToggle = async (checked: boolean) => {
|
||||||
if (!autoLaunchSupported || !api.setAutoLaunch) {
|
if (!autoLaunchSupported || !api.setAutoLaunch) {
|
||||||
toast.error("Please update the desktop app to configure launch on startup");
|
toast.error("Please update the desktop app to configure launch on startup");
|
||||||
|
|
@ -196,7 +168,6 @@ export function DesktopContent() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||||
<CardTitle className="text-base md:text-lg flex items-center gap-2">
|
<CardTitle className="text-base md:text-lg flex items-center gap-2">
|
||||||
<Power className="h-4 w-4" />
|
|
||||||
Launch on Startup
|
Launch on Startup
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs md:text-sm">
|
<CardDescription className="text-xs md:text-sm">
|
||||||
|
|
@ -245,56 +216,6 @@ export function DesktopContent() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Keyboard Shortcuts */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
|
||||||
<CardTitle className="text-base md:text-lg">Keyboard Shortcuts</CardTitle>
|
|
||||||
<CardDescription className="text-xs md:text-sm">
|
|
||||||
Customize the global keyboard shortcuts for desktop features.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-3 md:px-6 pb-3 md:pb-6">
|
|
||||||
{shortcutsLoaded ? (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<ShortcutRecorder
|
|
||||||
value={shortcuts.generalAssist}
|
|
||||||
onChange={(accel) => updateShortcut("generalAssist", accel)}
|
|
||||||
onReset={() => resetShortcut("generalAssist")}
|
|
||||||
defaultValue={DEFAULT_SHORTCUTS.generalAssist}
|
|
||||||
label="General Assist"
|
|
||||||
description="Launch SurfSense instantly from any application"
|
|
||||||
icon={Rocket}
|
|
||||||
/>
|
|
||||||
<ShortcutRecorder
|
|
||||||
value={shortcuts.quickAsk}
|
|
||||||
onChange={(accel) => updateShortcut("quickAsk", accel)}
|
|
||||||
onReset={() => resetShortcut("quickAsk")}
|
|
||||||
defaultValue={DEFAULT_SHORTCUTS.quickAsk}
|
|
||||||
label="Quick Assist"
|
|
||||||
description="Select text anywhere, then ask AI to explain, rewrite, or act on it"
|
|
||||||
icon={Zap}
|
|
||||||
/>
|
|
||||||
<ShortcutRecorder
|
|
||||||
value={shortcuts.autocomplete}
|
|
||||||
onChange={(accel) => updateShortcut("autocomplete", accel)}
|
|
||||||
onReset={() => resetShortcut("autocomplete")}
|
|
||||||
defaultValue={DEFAULT_SHORTCUTS.autocomplete}
|
|
||||||
label="Extreme Assist"
|
|
||||||
description="AI drafts text using your screen context and knowledge base"
|
|
||||||
icon={BrainCog}
|
|
||||||
/>
|
|
||||||
<p className="text-[11px] text-muted-foreground">
|
|
||||||
Click a shortcut and press a new key combination to change it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-center py-4">
|
|
||||||
<Spinner size="sm" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Extreme Assist Toggle */}
|
{/* Extreme Assist Toggle */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { BrainCog, Info, Rocket, Zap } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { useElectronAPI } from "@/hooks/use-platform";
|
||||||
|
|
||||||
|
export function DesktopShortcutsContent() {
|
||||||
|
const api = useElectronAPI();
|
||||||
|
const [shortcuts, setShortcuts] = useState(DEFAULT_SHORTCUTS);
|
||||||
|
const [shortcutsLoaded, setShortcutsLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
setShortcutsLoaded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mounted = true;
|
||||||
|
(api.getShortcuts?.() ?? Promise.resolve(null))
|
||||||
|
.then((config) => {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (config) setShortcuts(config);
|
||||||
|
setShortcutsLoaded(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!mounted) return;
|
||||||
|
setShortcutsLoaded(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">Hotkeys are only available in the SurfSense desktop app.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateShortcut = (
|
||||||
|
key: "generalAssist" | "quickAsk" | "autocomplete",
|
||||||
|
accelerator: string
|
||||||
|
) => {
|
||||||
|
setShortcuts((prev) => {
|
||||||
|
const updated = { ...prev, [key]: accelerator };
|
||||||
|
api.setShortcuts?.({ [key]: accelerator }).catch(() => {
|
||||||
|
toast.error("Failed to update shortcut");
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
toast.success("Shortcut updated");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetShortcut = (key: "generalAssist" | "quickAsk" | "autocomplete") => {
|
||||||
|
updateShortcut(key, DEFAULT_SHORTCUTS[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
shortcutsLoaded ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Alert className="bg-muted/50 py-3 md:py-4">
|
||||||
|
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||||
|
<AlertDescription className="text-xs md:text-sm">
|
||||||
|
<p>Click a shortcut and press a new key combination to change it.</p>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
<ShortcutRecorder
|
||||||
|
value={shortcuts.generalAssist}
|
||||||
|
onChange={(accel) => updateShortcut("generalAssist", accel)}
|
||||||
|
onReset={() => resetShortcut("generalAssist")}
|
||||||
|
defaultValue={DEFAULT_SHORTCUTS.generalAssist}
|
||||||
|
label="General Assist"
|
||||||
|
description="Launch SurfSense instantly from any application"
|
||||||
|
icon={Rocket}
|
||||||
|
/>
|
||||||
|
<ShortcutRecorder
|
||||||
|
value={shortcuts.quickAsk}
|
||||||
|
onChange={(accel) => updateShortcut("quickAsk", accel)}
|
||||||
|
onReset={() => resetShortcut("quickAsk")}
|
||||||
|
defaultValue={DEFAULT_SHORTCUTS.quickAsk}
|
||||||
|
label="Quick Assist"
|
||||||
|
description="Select text anywhere, then ask AI to explain, rewrite, or act on it"
|
||||||
|
icon={Zap}
|
||||||
|
/>
|
||||||
|
<ShortcutRecorder
|
||||||
|
value={shortcuts.autocomplete}
|
||||||
|
onChange={(accel) => updateShortcut("autocomplete", accel)}
|
||||||
|
onReset={() => resetShortcut("autocomplete")}
|
||||||
|
defaultValue={DEFAULT_SHORTCUTS.autocomplete}
|
||||||
|
label="Extreme Assist"
|
||||||
|
description="AI drafts text using your screen context and knowledge base"
|
||||||
|
icon={BrainCog}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -152,7 +152,7 @@ export default function DesktopLoginPage() {
|
||||||
{shortcutsLoaded ? (
|
{shortcutsLoaded ? (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
Keyboard Shortcuts
|
Hotkeys
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<ShortcutRecorder
|
<ShortcutRecorder
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { Brain, CircleUser, Globe, KeyRound, Monitor, ReceiptText, Sparkles } from "lucide-react";
|
import { Brain, CircleUser, Globe, Keyboard, KeyRound, Monitor, ReceiptText, Sparkles } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
@ -51,6 +51,13 @@ const DesktopContent = dynamic(
|
||||||
),
|
),
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
const DesktopShortcutsContent = dynamic(
|
||||||
|
() =>
|
||||||
|
import("@/app/dashboard/[search_space_id]/user-settings/components/DesktopShortcutsContent").then(
|
||||||
|
(m) => ({ default: m.DesktopShortcutsContent })
|
||||||
|
),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
const MemoryContent = dynamic(
|
const MemoryContent = dynamic(
|
||||||
() =>
|
() =>
|
||||||
import("@/app/dashboard/[search_space_id]/user-settings/components/MemoryContent").then(
|
import("@/app/dashboard/[search_space_id]/user-settings/components/MemoryContent").then(
|
||||||
|
|
@ -93,7 +100,14 @@ export function UserSettingsDialog() {
|
||||||
icon: <ReceiptText className="h-4 w-4" />,
|
icon: <ReceiptText className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
...(isDesktop
|
...(isDesktop
|
||||||
? [{ value: "desktop", label: "Desktop", icon: <Monitor className="h-4 w-4" /> }]
|
? [
|
||||||
|
{ value: "desktop", label: "Desktop", icon: <Monitor className="h-4 w-4" /> },
|
||||||
|
{
|
||||||
|
value: "desktop-shortcuts",
|
||||||
|
label: "Hotkeys",
|
||||||
|
icon: <Keyboard className="h-4 w-4" />,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
[t, isDesktop]
|
[t, isDesktop]
|
||||||
|
|
@ -116,6 +130,7 @@ export function UserSettingsDialog() {
|
||||||
{state.initialTab === "memory" && <MemoryContent />}
|
{state.initialTab === "memory" && <MemoryContent />}
|
||||||
{state.initialTab === "purchases" && <PurchaseHistoryContent />}
|
{state.initialTab === "purchases" && <PurchaseHistoryContent />}
|
||||||
{state.initialTab === "desktop" && <DesktopContent />}
|
{state.initialTab === "desktop" && <DesktopContent />}
|
||||||
|
{state.initialTab === "desktop-shortcuts" && <DesktopShortcutsContent />}
|
||||||
</div>
|
</div>
|
||||||
</SettingsDialog>
|
</SettingsDialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue