mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 17:56:25 +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
|
|
@ -38,4 +38,7 @@ export const IPC_CHANNELS = {
|
||||||
// Keyboard shortcut configuration
|
// Keyboard shortcut configuration
|
||||||
GET_SHORTCUTS: 'shortcuts:get',
|
GET_SHORTCUTS: 'shortcuts:get',
|
||||||
SET_SHORTCUTS: 'shortcuts:set',
|
SET_SHORTCUTS: 'shortcuts:set',
|
||||||
|
// Active search space
|
||||||
|
GET_ACTIVE_SEARCH_SPACE: 'search-space:get-active',
|
||||||
|
SET_ACTIVE_SEARCH_SPACE: 'search-space:set-active',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
readLocalFiles,
|
readLocalFiles,
|
||||||
} from '../modules/folder-watcher';
|
} from '../modules/folder-watcher';
|
||||||
import { getShortcuts, setShortcuts, type ShortcutConfig } from '../modules/shortcuts';
|
import { getShortcuts, setShortcuts, type ShortcutConfig } from '../modules/shortcuts';
|
||||||
|
import { getActiveSearchSpaceId, setActiveSearchSpaceId } from '../modules/active-search-space';
|
||||||
import { reregisterQuickAsk } from '../modules/quick-ask';
|
import { reregisterQuickAsk } from '../modules/quick-ask';
|
||||||
import { reregisterAutocomplete } from '../modules/autocomplete';
|
import { reregisterAutocomplete } from '../modules/autocomplete';
|
||||||
import { reregisterGeneralAssist } from '../modules/tray';
|
import { reregisterGeneralAssist } from '../modules/tray';
|
||||||
|
|
@ -106,6 +107,12 @@ export function registerIpcHandlers(): void {
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_SHORTCUTS, () => getShortcuts());
|
ipcMain.handle(IPC_CHANNELS.GET_SHORTCUTS, () => getShortcuts());
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.GET_ACTIVE_SEARCH_SPACE, () => getActiveSearchSpaceId());
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.SET_ACTIVE_SEARCH_SPACE, (_event, id: string) =>
|
||||||
|
setActiveSearchSpaceId(id)
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.SET_SHORTCUTS, async (_event, config: Partial<ShortcutConfig>) => {
|
ipcMain.handle(IPC_CHANNELS.SET_SHORTCUTS, async (_event, config: Partial<ShortcutConfig>) => {
|
||||||
const updated = await setShortcuts(config);
|
const updated = await setShortcuts(config);
|
||||||
if (config.generalAssist) await reregisterGeneralAssist();
|
if (config.generalAssist) await reregisterGeneralAssist();
|
||||||
|
|
|
||||||
24
surfsense_desktop/src/modules/active-search-space.ts
Normal file
24
surfsense_desktop/src/modules/active-search-space.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
const STORE_KEY = 'activeSearchSpaceId';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let store: any = null;
|
||||||
|
|
||||||
|
async function getStore() {
|
||||||
|
if (!store) {
|
||||||
|
const { default: Store } = await import('electron-store');
|
||||||
|
store = new Store({
|
||||||
|
name: 'active-search-space',
|
||||||
|
defaults: { [STORE_KEY]: null as string | null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveSearchSpaceId(): Promise<string | null> {
|
||||||
|
const s = await getStore();
|
||||||
|
return (s.get(STORE_KEY) as string | null) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setActiveSearchSpaceId(id: string): Promise<void> {
|
||||||
|
const s = await getStore();
|
||||||
|
s.set(STORE_KEY, id);
|
||||||
|
}
|
||||||
|
|
@ -2,16 +2,15 @@ import { clipboard, globalShortcut, ipcMain, screen } from 'electron';
|
||||||
import { IPC_CHANNELS } from '../../ipc/channels';
|
import { IPC_CHANNELS } from '../../ipc/channels';
|
||||||
import { getFrontmostApp, getWindowTitle, hasAccessibilityPermission, simulatePaste } from '../platform';
|
import { getFrontmostApp, getWindowTitle, hasAccessibilityPermission, simulatePaste } from '../platform';
|
||||||
import { hasScreenRecordingPermission, requestAccessibility, requestScreenRecording } from '../permissions';
|
import { hasScreenRecordingPermission, requestAccessibility, requestScreenRecording } from '../permissions';
|
||||||
import { getMainWindow } from '../window';
|
|
||||||
import { captureScreen } from './screenshot';
|
import { captureScreen } from './screenshot';
|
||||||
import { createSuggestionWindow, destroySuggestion, getSuggestionWindow } from './suggestion-window';
|
import { createSuggestionWindow, destroySuggestion, getSuggestionWindow } from './suggestion-window';
|
||||||
import { getShortcuts } from '../shortcuts';
|
import { getShortcuts } from '../shortcuts';
|
||||||
|
import { getActiveSearchSpaceId } from '../active-search-space';
|
||||||
|
|
||||||
let currentShortcut = '';
|
let currentShortcut = '';
|
||||||
let autocompleteEnabled = true;
|
let autocompleteEnabled = true;
|
||||||
let savedClipboard = '';
|
let savedClipboard = '';
|
||||||
let sourceApp = '';
|
let sourceApp = '';
|
||||||
let lastSearchSpaceId: string | null = null;
|
|
||||||
|
|
||||||
function isSurfSenseWindow(): boolean {
|
function isSurfSenseWindow(): boolean {
|
||||||
const app = getFrontmostApp();
|
const app = getFrontmostApp();
|
||||||
|
|
@ -37,21 +36,11 @@ async function triggerAutocomplete(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainWin = getMainWindow();
|
const searchSpaceId = await getActiveSearchSpaceId();
|
||||||
if (mainWin && !mainWin.isDestroyed()) {
|
if (!searchSpaceId) {
|
||||||
const mainUrl = mainWin.webContents.getURL();
|
console.warn('[autocomplete] No active search space. Select a search space first.');
|
||||||
const match = mainUrl.match(/\/dashboard\/(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
lastSearchSpaceId = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastSearchSpaceId) {
|
|
||||||
console.warn('[autocomplete] No active search space. Open a search space first.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchSpaceId = lastSearchSpaceId;
|
|
||||||
const cursor = screen.getCursorScreenPoint();
|
const cursor = screen.getCursorScreenPoint();
|
||||||
const win = createSuggestionWindow(cursor.x, cursor.y);
|
const win = createSuggestionWindow(cursor.x, cursor.y);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ import { IPC_CHANNELS } from '../ipc/channels';
|
||||||
import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform';
|
import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform';
|
||||||
import { getServerPort } from './server';
|
import { getServerPort } from './server';
|
||||||
import { getShortcuts } from './shortcuts';
|
import { getShortcuts } from './shortcuts';
|
||||||
|
import { getActiveSearchSpaceId } from './active-search-space';
|
||||||
|
|
||||||
let currentShortcut = '';
|
let currentShortcut = '';
|
||||||
let quickAskWindow: BrowserWindow | null = null;
|
let quickAskWindow: BrowserWindow | null = null;
|
||||||
let pendingText = '';
|
let pendingText = '';
|
||||||
let pendingMode = '';
|
let pendingMode = '';
|
||||||
|
let pendingSearchSpaceId: string | null = null;
|
||||||
let sourceApp = '';
|
let sourceApp = '';
|
||||||
let savedClipboard = '';
|
let savedClipboard = '';
|
||||||
|
|
||||||
|
|
@ -53,7 +55,9 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
quickAskWindow.loadURL(`http://localhost:${getServerPort()}/dashboard`);
|
const spaceId = pendingSearchSpaceId;
|
||||||
|
const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard';
|
||||||
|
quickAskWindow.loadURL(`http://localhost:${getServerPort()}${route}`);
|
||||||
|
|
||||||
quickAskWindow.once('ready-to-show', () => {
|
quickAskWindow.once('ready-to-show', () => {
|
||||||
quickAskWindow?.show();
|
quickAskWindow?.show();
|
||||||
|
|
@ -78,8 +82,9 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
|
||||||
return quickAskWindow;
|
return quickAskWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openQuickAsk(text: string): void {
|
async function openQuickAsk(text: string): Promise<void> {
|
||||||
pendingText = text;
|
pendingText = text;
|
||||||
|
pendingSearchSpaceId = await getActiveSearchSpaceId();
|
||||||
const cursor = screen.getCursorScreenPoint();
|
const cursor = screen.getCursorScreenPoint();
|
||||||
const pos = clampToScreen(cursor.x, cursor.y, 450, 750);
|
const pos = clampToScreen(cursor.x, cursor.y, 450, 750);
|
||||||
createQuickAskWindow(pos.x, pos.y);
|
createQuickAskWindow(pos.x, pos.y);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { app, BrowserWindow, shell, session } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { showErrorDialog } from './errors';
|
import { showErrorDialog } from './errors';
|
||||||
import { getServerPort } from './server';
|
import { getServerPort } from './server';
|
||||||
|
import { setActiveSearchSpaceId } from './active-search-space';
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string;
|
const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string;
|
||||||
|
|
@ -55,6 +56,16 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
|
||||||
showErrorDialog('Page failed to load', new Error(`${errorDescription} (${errorCode})\n${validatedURL}`));
|
showErrorDialog('Page failed to load', new Error(`${errorDescription} (${errorCode})\n${validatedURL}`));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-sync active search space from URL navigation
|
||||||
|
const syncSearchSpace = (url: string) => {
|
||||||
|
const match = url.match(/\/dashboard\/(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
setActiveSearchSpaceId(match[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mainWindow.webContents.on('did-navigate', (_event, url) => syncSearchSpace(url));
|
||||||
|
mainWindow.webContents.on('did-navigate-in-page', (_event, url) => syncSearchSpace(url));
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,4 +78,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
getShortcuts: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SHORTCUTS),
|
getShortcuts: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SHORTCUTS),
|
||||||
setShortcuts: (config: Record<string, string>) =>
|
setShortcuts: (config: Record<string, string>) =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.SET_SHORTCUTS, config),
|
ipcRenderer.invoke(IPC_CHANNELS.SET_SHORTCUTS, config),
|
||||||
|
|
||||||
|
// Active search space
|
||||||
|
getActiveSearchSpace: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACTIVE_SEARCH_SPACE),
|
||||||
|
setActiveSearchSpace: (id: string) =>
|
||||||
|
ipcRenderer.invoke(IPC_CHANNELS.SET_ACTIVE_SEARCH_SPACE, id),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { OnboardingTour } from "@/components/onboarding-tour";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useFolderSync } from "@/hooks/use-folder-sync";
|
import { useFolderSync } from "@/hooks/use-folder-sync";
|
||||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||||
|
import { useElectronAPI } from "@/hooks/use-platform";
|
||||||
|
|
||||||
export function DashboardClientLayout({
|
export function DashboardClientLayout({
|
||||||
children,
|
children,
|
||||||
|
|
@ -139,6 +140,8 @@ export function DashboardClientLayout({
|
||||||
refetchPreferences,
|
refetchPreferences,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const electronAPI = useElectronAPI();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeSeacrhSpaceId =
|
const activeSeacrhSpaceId =
|
||||||
typeof search_space_id === "string"
|
typeof search_space_id === "string"
|
||||||
|
|
@ -148,7 +151,16 @@ export function DashboardClientLayout({
|
||||||
: "";
|
: "";
|
||||||
if (!activeSeacrhSpaceId) return;
|
if (!activeSeacrhSpaceId) return;
|
||||||
setActiveSearchSpaceIdState(activeSeacrhSpaceId);
|
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
|
// Determine if we should show loading
|
||||||
const shouldShowLoading =
|
const shouldShowLoading =
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BrainCog, Rocket, Zap } from "lucide-react";
|
import { BrainCog, Rocket, Zap } from "lucide-react";
|
||||||
import { useCallback, 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 { 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { useElectronAPI } from "@/hooks/use-platform";
|
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() {
|
export function DesktopContent() {
|
||||||
const api = useElectronAPI();
|
const api = useElectronAPI();
|
||||||
|
|
@ -18,6 +21,9 @@ export function DesktopContent() {
|
||||||
const [shortcuts, setShortcuts] = useState(DEFAULT_SHORTCUTS);
|
const [shortcuts, setShortcuts] = useState(DEFAULT_SHORTCUTS);
|
||||||
const [shortcutsLoaded, setShortcutsLoaded] = useState(false);
|
const [shortcutsLoaded, setShortcutsLoaded] = useState(false);
|
||||||
|
|
||||||
|
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
||||||
|
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!api) {
|
if (!api) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -27,11 +33,18 @@ export function DesktopContent() {
|
||||||
|
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
||||||
Promise.all([api.getAutocompleteEnabled(), api.getShortcuts?.() ?? Promise.resolve(null)])
|
Promise.all([
|
||||||
.then(([autoEnabled, config]) => {
|
api.getAutocompleteEnabled(),
|
||||||
|
api.getShortcuts?.() ?? Promise.resolve(null),
|
||||||
|
api.getActiveSearchSpace?.() ?? Promise.resolve(null),
|
||||||
|
searchSpacesApiService.getSearchSpaces(),
|
||||||
|
])
|
||||||
|
.then(([autoEnabled, config, spaceId, spaces]) => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setEnabled(autoEnabled);
|
setEnabled(autoEnabled);
|
||||||
if (config) setShortcuts(config);
|
if (config) setShortcuts(config);
|
||||||
|
setActiveSpaceId(spaceId);
|
||||||
|
if (spaces) setSearchSpaces(spaces);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setShortcutsLoaded(true);
|
setShortcutsLoaded(true);
|
||||||
})
|
})
|
||||||
|
|
@ -84,8 +97,42 @@ export function DesktopContent() {
|
||||||
updateShortcut(key, DEFAULT_SHORTCUTS[key]);
|
updateShortcut(key, DEFAULT_SHORTCUTS[key]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSearchSpaceChange = (value: string) => {
|
||||||
|
setActiveSpaceId(value);
|
||||||
|
api.setActiveSearchSpace?.(value);
|
||||||
|
toast.success("Default search space updated");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 md:space-y-6">
|
<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 */}
|
{/* Keyboard Shortcuts */}
|
||||||
<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">
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import { Label } from "@/components/ui/label";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useElectronAPI } from "@/hooks/use-platform";
|
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";
|
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
|
||||||
|
|
||||||
const isGoogleAuth = AUTH_TYPE === "GOOGLE";
|
const isGoogleAuth = AUTH_TYPE === "GOOGLE";
|
||||||
|
|
@ -71,6 +73,19 @@ export default function DesktopLoginPage() {
|
||||||
window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`;
|
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) => {
|
const handleLocalLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoginError(null);
|
setLoginError(null);
|
||||||
|
|
@ -86,6 +101,9 @@ export default function DesktopLoginPage() {
|
||||||
sessionStorage.setItem("login_success_tracked", "true");
|
sessionStorage.setItem("login_success_tracked", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBearerToken(data.access_token);
|
||||||
|
await autoSetSearchSpace();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push(`/auth/callback?token=${data.access_token}`);
|
router.push(`/auth/callback?token=${data.access_token}`);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||||
import { getAndClearRedirectPath, setBearerToken, setRefreshToken } from "@/lib/auth-utils";
|
import { getAndClearRedirectPath, setBearerToken, setRefreshToken } from "@/lib/auth-utils";
|
||||||
|
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||||
import { trackLoginSuccess } from "@/lib/posthog/events";
|
import { trackLoginSuccess } from "@/lib/posthog/events";
|
||||||
|
|
||||||
interface TokenHandlerProps {
|
interface TokenHandlerProps {
|
||||||
|
|
@ -29,52 +30,54 @@ const TokenHandler = ({
|
||||||
useGlobalLoadingEffect(true);
|
useGlobalLoadingEffect(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only run on client-side
|
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
// Read tokens from URL at mount time — no subscription needed.
|
const run = async () => {
|
||||||
// TokenHandler only runs once after an auth redirect, so a stale read
|
const params = new URLSearchParams(window.location.search);
|
||||||
// is impossible and useSearchParams() would add a pointless subscription.
|
const token = params.get(tokenParamName);
|
||||||
// (Vercel Best Practice: rerender-defer-reads 5.2)
|
const refreshToken = params.get("refresh_token");
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const token = params.get(tokenParamName);
|
|
||||||
const refreshToken = params.get("refresh_token");
|
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
// Track login success for OAuth flows (e.g., Google)
|
const alreadyTracked = sessionStorage.getItem("login_success_tracked");
|
||||||
// Local login already tracks success before redirecting here
|
if (!alreadyTracked) {
|
||||||
const alreadyTracked = sessionStorage.getItem("login_success_tracked");
|
trackLoginSuccess("google");
|
||||||
if (!alreadyTracked) {
|
}
|
||||||
// This is an OAuth flow (Google login) - track success
|
sessionStorage.removeItem("login_success_tracked");
|
||||||
trackLoginSuccess("google");
|
|
||||||
|
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]);
|
}, [tokenParamName, storageKey, redirectPath]);
|
||||||
|
|
||||||
// Return null - the global provider handles the loading UI
|
// 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: (
|
setShortcuts: (
|
||||||
config: Partial<{ generalAssist: string; quickAsk: string; autocomplete: string }>
|
config: Partial<{ generalAssist: string; quickAsk: string; autocomplete: string }>
|
||||||
) => Promise<{ 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 {
|
declare global {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue