mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat: add active search space management to Electron API and UI
- Introduced IPC channels for getting and setting the active search space, enhancing user experience across the application. - Updated the preload script to expose new API methods for active search space management. - Modified the main window and quick ask functionalities to sync the active search space based on user navigation. - Enhanced the desktop and web applications to allow users to select and manage their default search space seamlessly. - Implemented automatic synchronization of the active search space during login and navigation events.
This commit is contained in:
parent
b74ac8a608
commit
7c6e52a0a5
12 changed files with 189 additions and 62 deletions
|
|
@ -19,6 +19,7 @@ import { OnboardingTour } from "@/components/onboarding-tour";
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useFolderSync } from "@/hooks/use-folder-sync";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
|
||||
export function DashboardClientLayout({
|
||||
children,
|
||||
|
|
@ -139,6 +140,8 @@ export function DashboardClientLayout({
|
|||
refetchPreferences,
|
||||
]);
|
||||
|
||||
const electronAPI = useElectronAPI();
|
||||
|
||||
useEffect(() => {
|
||||
const activeSeacrhSpaceId =
|
||||
typeof search_space_id === "string"
|
||||
|
|
@ -148,7 +151,16 @@ export function DashboardClientLayout({
|
|||
: "";
|
||||
if (!activeSeacrhSpaceId) return;
|
||||
setActiveSearchSpaceIdState(activeSeacrhSpaceId);
|
||||
}, [search_space_id, setActiveSearchSpaceIdState]);
|
||||
|
||||
// Sync to Electron store if stored value is null (first navigation)
|
||||
if (electronAPI?.setActiveSearchSpace) {
|
||||
electronAPI.getActiveSearchSpace?.().then((stored) => {
|
||||
if (!stored) {
|
||||
electronAPI.setActiveSearchSpace!(activeSeacrhSpaceId);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}, [search_space_id, setActiveSearchSpaceIdState, electronAPI]);
|
||||
|
||||
// Determine if we should show loading
|
||||
const shouldShowLoading =
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import { BrainCog, Rocket, Zap } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import type { SearchSpace } from "@/contracts/types/search-space.types";
|
||||
|
||||
export function DesktopContent() {
|
||||
const api = useElectronAPI();
|
||||
|
|
@ -18,6 +21,9 @@ export function DesktopContent() {
|
|||
const [shortcuts, setShortcuts] = useState(DEFAULT_SHORTCUTS);
|
||||
const [shortcutsLoaded, setShortcutsLoaded] = useState(false);
|
||||
|
||||
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
||||
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) {
|
||||
setLoading(false);
|
||||
|
|
@ -27,11 +33,18 @@ export function DesktopContent() {
|
|||
|
||||
let mounted = true;
|
||||
|
||||
Promise.all([api.getAutocompleteEnabled(), api.getShortcuts?.() ?? Promise.resolve(null)])
|
||||
.then(([autoEnabled, config]) => {
|
||||
Promise.all([
|
||||
api.getAutocompleteEnabled(),
|
||||
api.getShortcuts?.() ?? Promise.resolve(null),
|
||||
api.getActiveSearchSpace?.() ?? Promise.resolve(null),
|
||||
searchSpacesApiService.getSearchSpaces(),
|
||||
])
|
||||
.then(([autoEnabled, config, spaceId, spaces]) => {
|
||||
if (!mounted) return;
|
||||
setEnabled(autoEnabled);
|
||||
if (config) setShortcuts(config);
|
||||
setActiveSpaceId(spaceId);
|
||||
if (spaces) setSearchSpaces(spaces);
|
||||
setLoading(false);
|
||||
setShortcutsLoaded(true);
|
||||
})
|
||||
|
|
@ -84,8 +97,42 @@ export function DesktopContent() {
|
|||
updateShortcut(key, DEFAULT_SHORTCUTS[key]);
|
||||
};
|
||||
|
||||
const handleSearchSpaceChange = (value: string) => {
|
||||
setActiveSpaceId(value);
|
||||
api.setActiveSearchSpace?.(value);
|
||||
toast.success("Default search space updated");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Default Search Space */}
|
||||
<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">Default Search Space</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
Choose which search space General Assist, Quick Assist, and Extreme Assist operate against.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 md:px-6 pb-3 md:pb-6">
|
||||
{searchSpaces.length > 0 ? (
|
||||
<Select value={activeSpaceId ?? undefined} onValueChange={handleSearchSpaceChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a search space" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{searchSpaces.map((space) => (
|
||||
<SelectItem key={space.id} value={String(space.id)}>
|
||||
{space.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No search spaces found. Create one first.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Keyboard Shortcuts */}
|
||||
<Card>
|
||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { Label } from "@/components/ui/label";
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { setBearerToken } from "@/lib/auth-utils";
|
||||
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
|
||||
|
||||
const isGoogleAuth = AUTH_TYPE === "GOOGLE";
|
||||
|
|
@ -71,6 +73,19 @@ export default function DesktopLoginPage() {
|
|||
window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`;
|
||||
};
|
||||
|
||||
const autoSetSearchSpace = async () => {
|
||||
try {
|
||||
const stored = await api?.getActiveSearchSpace?.();
|
||||
if (stored) return;
|
||||
const spaces = await searchSpacesApiService.getSearchSpaces();
|
||||
if (spaces?.length) {
|
||||
await api?.setActiveSearchSpace?.(String(spaces[0].id));
|
||||
}
|
||||
} catch {
|
||||
// non-critical — dashboard-sync will catch it later
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocalLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoginError(null);
|
||||
|
|
@ -86,6 +101,9 @@ export default function DesktopLoginPage() {
|
|||
sessionStorage.setItem("login_success_tracked", "true");
|
||||
}
|
||||
|
||||
setBearerToken(data.access_token);
|
||||
await autoSetSearchSpace();
|
||||
|
||||
setTimeout(() => {
|
||||
router.push(`/auth/callback?token=${data.access_token}`);
|
||||
}, 300);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useEffect } from "react";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { getAndClearRedirectPath, setBearerToken, setRefreshToken } from "@/lib/auth-utils";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { trackLoginSuccess } from "@/lib/posthog/events";
|
||||
|
||||
interface TokenHandlerProps {
|
||||
|
|
@ -29,52 +30,54 @@ const TokenHandler = ({
|
|||
useGlobalLoadingEffect(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// Read tokens from URL at mount time — no subscription needed.
|
||||
// TokenHandler only runs once after an auth redirect, so a stale read
|
||||
// is impossible and useSearchParams() would add a pointless subscription.
|
||||
// (Vercel Best Practice: rerender-defer-reads 5.2)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get(tokenParamName);
|
||||
const refreshToken = params.get("refresh_token");
|
||||
const run = async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get(tokenParamName);
|
||||
const refreshToken = params.get("refresh_token");
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
// Track login success for OAuth flows (e.g., Google)
|
||||
// Local login already tracks success before redirecting here
|
||||
const alreadyTracked = sessionStorage.getItem("login_success_tracked");
|
||||
if (!alreadyTracked) {
|
||||
// This is an OAuth flow (Google login) - track success
|
||||
trackLoginSuccess("google");
|
||||
if (token) {
|
||||
try {
|
||||
const alreadyTracked = sessionStorage.getItem("login_success_tracked");
|
||||
if (!alreadyTracked) {
|
||||
trackLoginSuccess("google");
|
||||
}
|
||||
sessionStorage.removeItem("login_success_tracked");
|
||||
|
||||
localStorage.setItem(storageKey, token);
|
||||
setBearerToken(token);
|
||||
|
||||
if (refreshToken) {
|
||||
setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
// Auto-set active search space in desktop if not already set
|
||||
if (window.electronAPI?.getActiveSearchSpace) {
|
||||
try {
|
||||
const stored = await window.electronAPI.getActiveSearchSpace();
|
||||
if (!stored) {
|
||||
const spaces = await searchSpacesApiService.getSearchSpaces();
|
||||
if (spaces?.length) {
|
||||
await window.electronAPI.setActiveSearchSpace?.(String(spaces[0].id));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
|
||||
const savedRedirectPath = getAndClearRedirectPath();
|
||||
const finalRedirectPath = savedRedirectPath || redirectPath;
|
||||
window.location.href = finalRedirectPath;
|
||||
} catch (error) {
|
||||
console.error("Error storing token in localStorage:", error);
|
||||
window.location.href = redirectPath;
|
||||
}
|
||||
// Clear the flag for future logins
|
||||
sessionStorage.removeItem("login_success_tracked");
|
||||
|
||||
// Store access token in localStorage using both methods for compatibility
|
||||
localStorage.setItem(storageKey, token);
|
||||
setBearerToken(token);
|
||||
|
||||
// Store refresh token if provided
|
||||
if (refreshToken) {
|
||||
setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
// Check if there's a saved redirect path from before the auth flow
|
||||
const savedRedirectPath = getAndClearRedirectPath();
|
||||
|
||||
// Use the saved path if available, otherwise use the default redirectPath
|
||||
const finalRedirectPath = savedRedirectPath || redirectPath;
|
||||
|
||||
// Redirect to the appropriate path
|
||||
window.location.href = finalRedirectPath;
|
||||
} catch (error) {
|
||||
console.error("Error storing token in localStorage:", error);
|
||||
// Even if there's an error, try to redirect to the default path
|
||||
window.location.href = redirectPath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
}, [tokenParamName, storageKey, redirectPath]);
|
||||
|
||||
// Return null - the global provider handles the loading UI
|
||||
|
|
|
|||
3
surfsense_web/types/window.d.ts
vendored
3
surfsense_web/types/window.d.ts
vendored
|
|
@ -93,6 +93,9 @@ interface ElectronAPI {
|
|||
setShortcuts: (
|
||||
config: Partial<{ generalAssist: string; quickAsk: string; autocomplete: string }>
|
||||
) => Promise<{ generalAssist: string; quickAsk: string; autocomplete: string }>;
|
||||
// Active search space
|
||||
getActiveSearchSpace: () => Promise<string | null>;
|
||||
setActiveSearchSpace: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue