From 7ccb52dc76a45348e965369ad36e1e2314e7a0c2 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:18:28 +0530 Subject: [PATCH 01/12] feat: add general settings management to search space settings - Introduced a new GeneralSettingsManager component for managing search space name and description. - Updated the settings navigation to include a general section with an icon and description. - Modified the SettingsPage to default to the general settings section. - Enhanced the CreateSearchSpaceDialog for improved UI and accessibility. - Updated English and Chinese translations for new settings labels and descriptions. --- .../[search_space_id]/settings/page.tsx | 13 +- .../ui/dialogs/CreateSearchSpaceDialog.tsx | 37 ++-- .../settings/general-settings-manager.tsx | 200 ++++++++++++++++++ surfsense_web/messages/en.json | 14 +- surfsense_web/messages/zh.json | 14 +- 5 files changed, 258 insertions(+), 20 deletions(-) create mode 100644 surfsense_web/components/settings/general-settings-manager.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx index fb2f49317..6d5787ed9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx @@ -10,11 +10,13 @@ import { MessageSquare, Settings, X, + FileText, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useState } from "react"; +import { GeneralSettingsManager } from "@/components/settings/general-settings-manager"; import { LLMRoleManager } from "@/components/settings/llm-role-manager"; import { ModelConfigManager } from "@/components/settings/model-config-manager"; import { PromptConfigManager } from "@/components/settings/prompt-config-manager"; @@ -30,6 +32,12 @@ interface SettingsNavItem { } const settingsNavItems: SettingsNavItem[] = [ + { + id: "general", + labelKey: "nav_general", + descriptionKey: "nav_general_desc", + icon: FileText, + }, { id: "models", labelKey: "nav_agent_configs", @@ -262,6 +270,9 @@ function SettingsContent({ ease: [0.4, 0, 0.2, 1], }} > + {activeSection === "general" && ( + + )} {activeSection === "models" && } {activeSection === "roles" && } {activeSection === "prompts" && } @@ -277,7 +288,7 @@ export default function SettingsPage() { const router = useRouter(); const params = useParams(); const searchSpaceId = Number(params.search_space_id); - const [activeSection, setActiveSection] = useState("models"); + const [activeSection, setActiveSection] = useState("general"); const [isSidebarOpen, setIsSidebarOpen] = useState(false); // Track settings section view diff --git a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx index 7e962536f..427cbd31b 100644 --- a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx +++ b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx @@ -82,29 +82,31 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac return ( - - -
-
- + + +
+
+
-
- {t("create_title")} - {t("create_description")} +
+ {t("create_title")} + + {t("create_description")} +
- + ( - {t("name_label")} + {t("name_label")} - + @@ -116,38 +118,39 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac name="description" render={({ field }) => ( - + {t("description_label")}{" "} ({tCommon("optional")}) - + )} /> - + - + +
+ + {hasChanges && ( + + + + {t("general_unsaved_changes")} + + + )} +
+ ); +} + diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 8ca382669..108d93262 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -723,12 +723,24 @@ "searchSpaceSettings": { "title": "Search Space Settings", "back_to_app": "Back to app", + "nav_general": "General", + "nav_general_desc": "Name, description & basic info", "nav_agent_configs": "Agent Configs", "nav_agent_configs_desc": "LLM models with prompts & citations", "nav_role_assignments": "Role Assignments", "nav_role_assignments_desc": "Assign configs to agent roles", "nav_system_instructions": "System Instructions", - "nav_system_instructions_desc": "SearchSpace-wide AI instructions" + "nav_system_instructions_desc": "SearchSpace-wide AI instructions", + "general_name_label": "Name", + "general_name_placeholder": "Enter search space name", + "general_name_description": "A unique name for your search space.", + "general_description_label": "Description", + "general_description_placeholder": "Enter search space description", + "general_description_description": "A brief description of what this search space will be used for.", + "general_reset": "Reset Changes", + "general_save": "Save Changes", + "general_saving": "Saving", + "general_unsaved_changes": "You have unsaved changes. Click \"Save Changes\" to apply them." }, "homepage": { "hero_title_part1": "The AI Workspace", diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index 3769d9dfc..51e378bb2 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -708,12 +708,24 @@ "searchSpaceSettings": { "title": "搜索空间设置", "back_to_app": "返回应用", + "nav_general": "常规", + "nav_general_desc": "名称、描述和基本信息", "nav_agent_configs": "代理配置", "nav_agent_configs_desc": "LLM 模型配置提示词和引用", "nav_role_assignments": "角色分配", "nav_role_assignments_desc": "为代理角色分配配置", "nav_system_instructions": "系统指令", - "nav_system_instructions_desc": "搜索空间级别的 AI 指令" + "nav_system_instructions_desc": "搜索空间级别的 AI 指令", + "general_name_label": "名称", + "general_name_placeholder": "输入搜索空间名称", + "general_name_description": "您的搜索空间的唯一名称。", + "general_description_label": "描述", + "general_description_placeholder": "输入搜索空间描述", + "general_description_description": "此搜索空间用途的简要描述。", + "general_reset": "重置更改", + "general_save": "保存更改", + "general_saving": "保存中...", + "general_unsaved_changes": "您有未保存的更改。点击\"保存更改\"以应用它们。" }, "homepage": { "hero_title_part1": "AI 工作空间", From a51fe2ee692aefe919b38be49288897c349ea01b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:17:35 +0530 Subject: [PATCH 02/12] feat: implement chat archiving functionality in LayoutDataProvider - Added the ability to archive and unarchive chat threads, enhancing user interaction with chat items. - Introduced a new `handleChatArchive` function to manage the archiving state and provide user feedback via toast notifications. - Updated the `ChatItem` type to include an `archived` property for tracking the archive status. - Modified relevant components to support the new archiving feature, including `ChatListItem`, `Sidebar`, and `MobileSidebar`. --- .../layout/providers/LayoutDataProvider.tsx | 28 ++++++++++- .../components/layout/types/layout.types.ts | 1 + .../layout/ui/shell/LayoutShell.tsx | 4 ++ .../layout/ui/sidebar/ChatListItem.tsx | 49 ++++++++++++++----- .../layout/ui/sidebar/MobileSidebar.tsx | 3 ++ .../components/layout/ui/sidebar/Sidebar.tsx | 6 +++ .../layout/ui/sidebar/SidebarHeader.tsx | 4 +- 7 files changed, 81 insertions(+), 14 deletions(-) diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 9e3f55c97..86d7082ee 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -7,6 +7,7 @@ import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useTheme } from "next-themes"; import { useCallback, useMemo, useState } from "react"; +import { toast } from "sonner"; import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; @@ -21,7 +22,7 @@ import { } from "@/components/ui/dialog"; import { useInbox } from "@/hooks/use-inbox"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; -import { deleteThread, fetchThreads } from "@/lib/chat/thread-persistence"; +import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-persistence"; import { cleanupElectric } from "@/lib/electric/client"; import { resetUser, trackLogout } from "@/lib/posthog/events"; import { cacheKeys } from "@/lib/query-client/cache-keys"; @@ -56,6 +57,7 @@ export function LayoutDataProvider({ }: LayoutDataProviderProps) { const t = useTranslations("dashboard"); const tCommon = useTranslations("common"); + const tSidebar = useTranslations("sidebar"); const router = useRouter(); const params = useParams(); const pathname = usePathname(); @@ -154,6 +156,7 @@ export function LayoutDataProvider({ url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`, visibility: thread.visibility, isOwnThread: thread.is_own_thread, + archived: thread.archived, }; // Split based on visibility, not ownership: @@ -304,6 +307,28 @@ export function LayoutDataProvider({ setShowDeleteChatDialog(true); }, []); + const handleChatArchive = useCallback( + async (chat: ChatItem) => { + const newArchivedState = !chat.archived; + const successMessage = newArchivedState + ? tSidebar("chat_archived") || "Chat archived" + : tSidebar("chat_unarchived") || "Chat restored"; + + try { + await updateThread(chat.id, { archived: newArchivedState }); + toast.success(successMessage); + // Invalidate queries to refresh UI (React Query will only refetch active queries) + queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); + queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); + queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); + } catch (error) { + console.error("Error archiving thread:", error); + toast.error(tSidebar("error_archiving_chat") || "Failed to archive chat"); + } + }, + [queryClient, searchSpaceId, tSidebar] + ); + const handleSettings = useCallback(() => { router.push(`/dashboard/${searchSpaceId}/settings`); }, [router, searchSpaceId]); @@ -391,6 +416,7 @@ export function LayoutDataProvider({ onNewChat={handleNewChat} onChatSelect={handleChatSelect} onChatDelete={handleChatDelete} + onChatArchive={handleChatArchive} onViewAllSharedChats={handleViewAllSharedChats} onViewAllPrivateChats={handleViewAllPrivateChats} user={{ diff --git a/surfsense_web/components/layout/types/layout.types.ts b/surfsense_web/components/layout/types/layout.types.ts index 3eac64e60..c58fb5b54 100644 --- a/surfsense_web/components/layout/types/layout.types.ts +++ b/surfsense_web/components/layout/types/layout.types.ts @@ -30,6 +30,7 @@ export interface ChatItem { isActive?: boolean; visibility?: "PRIVATE" | "SEARCH_SPACE"; isOwnThread?: boolean; + archived?: boolean; } export interface PageUsage { diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx index b68719cde..37e4d8ada 100644 --- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx +++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx @@ -26,6 +26,7 @@ interface LayoutShellProps { onNewChat: () => void; onChatSelect: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void; + onChatArchive?: (chat: ChatItem) => void; onViewAllSharedChats?: () => void; onViewAllPrivateChats?: () => void; user: User; @@ -59,6 +60,7 @@ export function LayoutShell({ onNewChat, onChatSelect, onChatDelete, + onChatArchive, onViewAllSharedChats, onViewAllPrivateChats, user, @@ -107,6 +109,7 @@ export function LayoutShell({ onNewChat={onNewChat} onChatSelect={onChatSelect} onChatDelete={onChatDelete} + onChatArchive={onChatArchive} onViewAllSharedChats={onViewAllSharedChats} onViewAllPrivateChats={onViewAllPrivateChats} user={user} @@ -155,6 +158,7 @@ export function LayoutShell({ onNewChat={onNewChat} onChatSelect={onChatSelect} onChatDelete={onChatDelete} + onChatArchive={onChatArchive} onViewAllSharedChats={onViewAllSharedChats} onViewAllPrivateChats={onViewAllPrivateChats} user={user} diff --git a/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx b/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx index 5dd9c2cfa..12def741b 100644 --- a/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx +++ b/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx @@ -1,12 +1,13 @@ "use client"; -import { MessageSquare, MoreHorizontal } from "lucide-react"; +import { ArchiveIcon, MessageSquare, MoreHorizontal, RotateCcwIcon, Trash2 } from "lucide-react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; @@ -14,11 +15,13 @@ import { cn } from "@/lib/utils"; interface ChatListItemProps { name: string; isActive?: boolean; + archived?: boolean; onClick?: () => void; + onArchive?: () => void; onDelete?: () => void; } -export function ChatListItem({ name, isActive, onClick, onDelete }: ChatListItemProps) { +export function ChatListItem({ name, isActive, archived, onClick, onArchive, onDelete }: ChatListItemProps) { const t = useTranslations("sidebar"); return ( @@ -48,15 +51,39 @@ export function ChatListItem({ name, isActive, onClick, onDelete }: ChatListItem - { - e.stopPropagation(); - onDelete?.(); - }} - className="text-destructive focus:text-destructive" - > - {t("delete")} - + {onArchive && ( + { + e.stopPropagation(); + onArchive(); + }} + > + {archived ? ( + <> + + {t("unarchive") || "Restore"} + + ) : ( + <> + + {t("archive") || "Archive"} + + )} + + )} + {onArchive && onDelete && } + {onDelete && ( + { + e.stopPropagation(); + onDelete(); + }} + className="text-destructive focus:text-destructive" + > + + {t("delete")} + + )}
diff --git a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx index 7dd01d75a..85f907611 100644 --- a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx @@ -25,6 +25,7 @@ interface MobileSidebarProps { onNewChat: () => void; onChatSelect: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void; + onChatArchive?: (chat: ChatItem) => void; onViewAllSharedChats?: () => void; onViewAllPrivateChats?: () => void; user: User; @@ -64,6 +65,7 @@ export function MobileSidebar({ onNewChat, onChatSelect, onChatDelete, + onChatArchive, onViewAllSharedChats, onViewAllPrivateChats, user, @@ -141,6 +143,7 @@ export function MobileSidebar({ }} onChatSelect={handleChatSelect} onChatDelete={onChatDelete} + onChatArchive={onChatArchive} onViewAllSharedChats={onViewAllSharedChats} onViewAllPrivateChats={onViewAllPrivateChats} user={user} diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx index f3452749f..d05f21096 100644 --- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx @@ -27,6 +27,7 @@ interface SidebarProps { onNewChat: () => void; onChatSelect: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void; + onChatArchive?: (chat: ChatItem) => void; onViewAllSharedChats?: () => void; onViewAllPrivateChats?: () => void; user: User; @@ -52,6 +53,7 @@ export function Sidebar({ onNewChat, onChatSelect, onChatDelete, + onChatArchive, onViewAllSharedChats, onViewAllPrivateChats, user, @@ -175,7 +177,9 @@ export function Sidebar({ key={chat.id} name={chat.name} isActive={chat.id === activeChatId} + archived={chat.archived} onClick={() => onChatSelect(chat)} + onArchive={() => onChatArchive?.(chat)} onDelete={() => onChatDelete?.(chat)} /> ))} @@ -216,7 +220,9 @@ export function Sidebar({ key={chat.id} name={chat.name} isActive={chat.id === activeChatId} + archived={chat.archived} onClick={() => onChatSelect(chat)} + onArchive={() => onChatArchive?.(chat)} onDelete={() => onChatDelete?.(chat)} /> ))} diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx index 5c8c89152..28c359e64 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronsUpDown, ScrollText, Settings, Users } from "lucide-react"; +import { ChevronsUpDown, Logs, Settings, Users } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; @@ -57,7 +57,7 @@ export function SidebarHeader({ {t("manage_members")} router.push(`/dashboard/${searchSpaceId}/logs`)}> - + {t("logs")} From bba3cb1cf9a48c525aabcb4b14d8324881fafa6b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:03:10 +0530 Subject: [PATCH 03/12] feat: enhance ElectricProvider and Tooltip components for improved user experience - Updated ElectricProvider to check for user authentication via bearer token before rendering, preventing unnecessary loading screens for unauthenticated users. - Modified TooltipProvider to include a new disableHoverableContent prop, enhancing tooltip behavior and usability. --- surfsense_web/components/providers/ElectricProvider.tsx | 7 ++++++- surfsense_web/components/ui/tooltip.tsx | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx index e31885973..f187d10c1 100644 --- a/surfsense_web/components/providers/ElectricProvider.tsx +++ b/surfsense_web/components/providers/ElectricProvider.tsx @@ -3,6 +3,7 @@ import { useAtomValue } from "jotai"; import { useEffect, useRef, useState } from "react"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { getBearerToken } from "@/lib/auth-utils"; import { cleanupElectric, type ElectricClient, @@ -105,9 +106,13 @@ export function ElectricProvider({ children }: ElectricProviderProps) { }; }, [user?.id, isUserLoaded, electricClient]); + // Check if user is authenticated first (has bearer token) + // This prevents showing loading screen for unauthenticated users on homepage + const hasToken = typeof window !== "undefined" && !!getBearerToken(); + // For non-authenticated pages (like landing page), render immediately with null context // Also render immediately if user query failed (e.g., token expired) - if (!isUserLoaded || !user?.id || isUserError) { + if (!hasToken || !isUserLoaded || !user?.id || isUserError) { return {children}; } diff --git a/surfsense_web/components/ui/tooltip.tsx b/surfsense_web/components/ui/tooltip.tsx index 871ce9ed5..356467a8a 100644 --- a/surfsense_web/components/ui/tooltip.tsx +++ b/surfsense_web/components/ui/tooltip.tsx @@ -7,12 +7,14 @@ import { cn } from "@/lib/utils"; function TooltipProvider({ delayDuration = 0, + disableHoverableContent = true, ...props }: React.ComponentProps) { return ( ); @@ -42,7 +44,7 @@ function TooltipContent({ data-slot="tooltip-content" sideOffset={sideOffset} className={cn( - "bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-balance", + "bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-balance pointer-events-none", className )} {...props} From 22bd5e0f392db6d91a724c8901779c66c2f61e24 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:42:07 +0530 Subject: [PATCH 04/12] feat: implement unified loading screens across various components - Introduced a new UnifiedLoadingScreen component for consistent loading indicators in the application. - Replaced existing loading implementations in LoginPage, AuthCallbackPage, DashboardLayout, and other components with the new unified loading screen. - Updated translations for loading messages to enhance user experience and clarity. - Improved loading states in the ElectricProvider and TokenHandler components to utilize the new loading screen, ensuring a cohesive look and feel during loading processes. --- surfsense_web/app/(home)/login/page.tsx | 37 +++------- surfsense_web/app/auth/callback/loading.tsx | 19 +++++ surfsense_web/app/auth/callback/page.tsx | 29 ++++---- .../[search_space_id]/client-layout.tsx | 23 ++---- .../new-chat/[[...chat_id]]/page.tsx | 8 ++- surfsense_web/app/dashboard/layout.tsx | 19 ++--- surfsense_web/app/dashboard/loading.tsx | 21 ++++++ surfsense_web/app/dashboard/page.tsx | 39 ++-------- surfsense_web/components/TokenHandler.tsx | 7 +- .../new-chat/source-detail-panel.tsx | 15 ++-- .../components/providers/ElectricProvider.tsx | 7 +- .../components/ui/unified-loading-screen.tsx | 72 +++++++++++++++++++ surfsense_web/messages/en.json | 19 +++-- surfsense_web/messages/zh.json | 17 +++-- 14 files changed, 191 insertions(+), 141 deletions(-) create mode 100644 surfsense_web/app/auth/callback/loading.tsx create mode 100644 surfsense_web/app/dashboard/loading.tsx create mode 100644 surfsense_web/components/ui/unified-loading-screen.tsx diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx index 7aade8427..0026fdcd0 100644 --- a/surfsense_web/app/(home)/login/page.tsx +++ b/surfsense_web/app/(home)/login/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { Loader2 } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { Suspense, useEffect, useState } from "react"; import { toast } from "sonner"; import { Logo } from "@/components/Logo"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { AUTH_TYPE } from "@/lib/env-config"; import { AmbientBackground } from "./AmbientBackground"; @@ -59,7 +59,11 @@ function LoginContent() { }); // Show toast with conditional retry action - const toastOptions: any = { + const toastOptions: { + description: string; + duration: number; + action?: { label: string; onClick: () => void }; + } = { description: errorDescription, duration: 6000, }; @@ -90,18 +94,7 @@ function LoginContent() { // Show loading state while determining auth type if (isLoading) { - return ( -
- -
- -
- - {tCommon("loading")} -
-
-
- ); + return ; } if (authType === "GOOGLE") { @@ -182,23 +175,9 @@ function LoginContent() { ); } -// Loading fallback for Suspense -const LoadingFallback = () => ( -
- -
- -
- - Loading... -
-
-
-); - export default function LoginPage() { return ( - }> + }> ); diff --git a/surfsense_web/app/auth/callback/loading.tsx b/surfsense_web/app/auth/callback/loading.tsx new file mode 100644 index 000000000..24d1891b4 --- /dev/null +++ b/surfsense_web/app/auth/callback/loading.tsx @@ -0,0 +1,19 @@ +import { useTranslations } from "next-intl"; +import { Spinner } from "@/components/ui/spinner"; + +export default function AuthCallbackLoading() { + const t = useTranslations("auth"); + return ( +
+
+
+ +
+ + {t("processing_authentication")} + +
+
+ ); +} + diff --git a/surfsense_web/app/auth/callback/page.tsx b/surfsense_web/app/auth/callback/page.tsx index da868c316..920581654 100644 --- a/surfsense_web/app/auth/callback/page.tsx +++ b/surfsense_web/app/auth/callback/page.tsx @@ -1,23 +1,20 @@ +"use client"; + import { Suspense } from "react"; +import { useTranslations } from "next-intl"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import TokenHandler from "@/components/TokenHandler"; export default function AuthCallbackPage() { + const t = useTranslations("auth"); + return ( -
-

Authentication Callback

- -
-
- } - > - -
- + }> + + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index bbafa9703..0c0b0cbc2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -1,7 +1,6 @@ "use client"; import { useAtomValue, useSetAtom } from "jotai"; -import { Loader2 } from "lucide-react"; import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import type React from "react"; @@ -19,6 +18,7 @@ import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; import { LayoutDataProvider } from "@/components/layout"; import { OnboardingTour } from "@/components/onboarding-tour"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; export function DashboardClientLayout({ children, @@ -153,23 +153,10 @@ export function DashboardClientLayout({ isAutoConfiguring ) { return ( -
- - - - {isAutoConfiguring ? "Setting up AI..." : t("loading_config")} - - - {isAutoConfiguring - ? "Auto-configuring with available settings" - : t("checking_llm_prefs")} - - - - - - -
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 59e7878c4..4509a44a7 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -9,6 +9,7 @@ import { import { useQueryClient } from "@tanstack/react-query"; import { useAtomValue, useSetAtom } from "jotai"; import { useParams, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { z } from "zod"; @@ -34,6 +35,7 @@ import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview"; import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage"; import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory"; +import { Spinner } from "@/components/ui/spinner"; import { useChatSessionStateSync } from "@/hooks/use-chat-session-state"; import { useMessagesElectric } from "@/hooks/use-messages-electric"; // import { WriteTodosToolUI } from "@/components/tool-ui/write-todos"; @@ -236,6 +238,7 @@ interface ThinkingStepData { } export default function NewChatPage() { + const t = useTranslations("dashboard"); const params = useParams(); const queryClient = useQueryClient(); const [isInitializing, setIsInitializing] = useState(true); @@ -1475,8 +1478,9 @@ export default function NewChatPage() { // Show loading state only when loading an existing thread if (isInitializing) { return ( -
-
Loading chat...
+
+ +
{t("loading_chat")}
); } diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index 71cd6275f..8ffef229c 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -1,8 +1,8 @@ "use client"; -import { Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useTranslations } from "next-intl"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; interface DashboardLayoutProps { @@ -10,6 +10,7 @@ interface DashboardLayoutProps { } export default function DashboardLayout({ children }: DashboardLayoutProps) { + const t = useTranslations("dashboard"); const [isCheckingAuth, setIsCheckingAuth] = useState(true); useEffect(() => { @@ -25,19 +26,7 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { // Show loading screen while checking authentication if (isCheckingAuth) { - return ( -
- - - Loading Dashboard - Checking authentication... - - - - - -
- ); + return ; } return ( diff --git a/surfsense_web/app/dashboard/loading.tsx b/surfsense_web/app/dashboard/loading.tsx new file mode 100644 index 000000000..b18c5dd75 --- /dev/null +++ b/surfsense_web/app/dashboard/loading.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { Spinner } from "@/components/ui/spinner"; + +export default function DashboardLoading() { + const t = useTranslations("common"); + return ( +
+
+
+ +
+ + {t("loading")} + +
+
+ ); +} + diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index 767ce5201..440f37123 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useAtomValue } from "jotai"; -import { AlertCircle, Loader2, Plus, Search } from "lucide-react"; +import { AlertCircle, Plus, Search } from "lucide-react"; import { motion } from "motion/react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -18,37 +18,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; - -function LoadingScreen() { - const t = useTranslations("dashboard"); - return ( -
- - - - {t("loading")} - {t("fetching_spaces")} - - - - - - - - {t("may_take_moment")} - - - -
- ); -} +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; function ErrorScreen({ message }: { message: string }) { const t = useTranslations("dashboard"); @@ -121,6 +91,7 @@ export default function DashboardPage() { const router = useRouter(); const [showCreateDialog, setShowCreateDialog] = useState(false); + const t = useTranslations("dashboard"); const { data: searchSpaces = [], isLoading, error } = useAtomValue(searchSpacesAtom); useEffect(() => { @@ -131,11 +102,11 @@ export default function DashboardPage() { } }, [isLoading, searchSpaces, router]); - if (isLoading) return ; + if (isLoading) return ; if (error) return ; if (searchSpaces.length > 0) { - return ; + return ; } return ( diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx index b4ca36298..3f98451ef 100644 --- a/surfsense_web/components/TokenHandler.tsx +++ b/surfsense_web/components/TokenHandler.tsx @@ -1,7 +1,9 @@ "use client"; import { useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useEffect } from "react"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils"; import { trackLoginSuccess } from "@/lib/posthog/events"; @@ -25,6 +27,7 @@ const TokenHandler = ({ tokenParamName = "token", storageKey = "surfsense_bearer_token", }: TokenHandlerProps) => { + const t = useTranslations("auth"); const searchParams = useSearchParams(); useEffect(() => { @@ -67,9 +70,7 @@ const TokenHandler = ({ }, [searchParams, tokenParamName, storageKey, redirectPath]); return ( -
-

Processing authentication...

-
+ ); }; diff --git a/surfsense_web/components/new-chat/source-detail-panel.tsx b/surfsense_web/components/new-chat/source-detail-panel.tsx index 08cff8380..1cac73a01 100644 --- a/surfsense_web/components/new-chat/source-detail-panel.tsx +++ b/surfsense_web/components/new-chat/source-detail-panel.tsx @@ -7,11 +7,11 @@ import { ExternalLink, FileText, Hash, - Loader2, Sparkles, X, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { useTranslations } from "next-intl"; import type React from "react"; import { forwardRef, type ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; @@ -20,6 +20,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Spinner } from "@/components/ui/spinner"; import type { GetDocumentByChunkResponse, GetSurfsenseDocsByChunkResponse, @@ -63,7 +64,7 @@ interface ChunkCardProps { } const ChunkCard = forwardRef( - ({ chunk, index, totalChunks, isCited, isActive, disableLayoutAnimation }, ref) => { + ({ chunk, index, totalChunks, isCited }, ref) => { return (
(null); const hasScrolledRef = useRef(false); // Use ref to avoid stale closures const [summaryOpen, setSummaryOpen] = useState(false); const [activeChunkIndex, setActiveChunkIndex] = useState(null); const [mounted, setMounted] = useState(false); - const [hasScrolledToCited, setHasScrolledToCited] = useState(false); + const [_hasScrolledToCited, setHasScrolledToCited] = useState(false); const shouldReduceMotion = useReducedMotion(); useEffect(() => { @@ -382,11 +384,8 @@ export function SourceDetailPanel({ animate={{ opacity: 1, scale: 1 }} className="flex flex-col items-center gap-4" > -
-
- -
-

Loading document

+ +

{t("loading_document")}

)} diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx index f187d10c1..68263b036 100644 --- a/surfsense_web/components/providers/ElectricProvider.tsx +++ b/surfsense_web/components/providers/ElectricProvider.tsx @@ -1,8 +1,10 @@ "use client"; import { useAtomValue } from "jotai"; +import { useTranslations } from "next-intl"; import { useEffect, useRef, useState } from "react"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import { getBearerToken } from "@/lib/auth-utils"; import { cleanupElectric, @@ -28,6 +30,7 @@ interface ElectricProviderProps { * 5. Provides client via context - hooks should use useElectricClient() */ export function ElectricProvider({ children }: ElectricProviderProps) { + const t = useTranslations("common"); const [electricClient, setElectricClient] = useState(null); const [error, setError] = useState(null); const { @@ -120,9 +123,7 @@ export function ElectricProvider({ children }: ElectricProviderProps) { if (!electricClient && !error) { return ( -
-
Initializing...
-
+
); } diff --git a/surfsense_web/components/ui/unified-loading-screen.tsx b/surfsense_web/components/ui/unified-loading-screen.tsx new file mode 100644 index 000000000..7ed6b83d9 --- /dev/null +++ b/surfsense_web/components/ui/unified-loading-screen.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { Logo } from "@/components/Logo"; +import { Spinner } from "@/components/ui/spinner"; +import { AmbientBackground } from "@/app/(home)/login/AmbientBackground"; + +interface UnifiedLoadingScreenProps { + /** Optional message to display below the spinner */ + message?: string; + /** Visual style variant */ + variant?: "login" | "default"; +} + +export function UnifiedLoadingScreen({ + message, + variant = "default", +}: UnifiedLoadingScreenProps) { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + // Fixed-size container to prevent layout shifts + const spinnerContainer = ( +
+ +
+ ); + + const content = variant === "login" ? ( +
+ +
+ +
+ {spinnerContainer} + {message && ( + + {message} + + )} +
+
+
+ ) : ( +
+
+ {spinnerContainer} + {message && ( + + {message} + + )} +
+
+ ); + + // Render inline during SSR, use portal after mounting + // This prevents the black flash during initial render + if (!mounted) { + return content; + } + + return createPortal(content, document.body); +} + diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 108d93262..59b9cf56a 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -2,7 +2,8 @@ "common": { "app_name": "SurfSense", "welcome": "Welcome", - "loading": "Loading...", + "loading": "Loading", + "initializing": "Initializing", "save": "Save", "cancel": "Cancel", "delete": "Delete", @@ -76,9 +77,10 @@ "passwords_no_match": "Passwords do not match", "password_mismatch": "Password Mismatch", "passwords_no_match_desc": "The passwords you entered do not match", - "creating_account": "Creating your account...", - "creating_account_btn": "Creating account...", - "redirecting_login": "Redirecting to login page..." + "creating_account": "Creating your account", + "creating_account_btn": "Creating account", + "redirecting_login": "Redirecting to login page", + "processing_authentication": "Processing authentication" }, "searchSpace": { "create_title": "Create Search Space", @@ -143,12 +145,15 @@ "api_keys": "API Keys", "profile": "Profile", "loading_dashboard": "Loading Dashboard", - "checking_auth": "Checking authentication...", + "checking_auth": "Checking authentication", "loading_config": "Loading Configuration", - "checking_llm_prefs": "Checking your LLM preferences...", + "checking_llm_prefs": "Checking your LLM preferences", + "setting_up_ai": "Setting up AI", "config_error": "Configuration Error", "failed_load_llm_config": "Failed to load your LLM configuration", "error_loading_chats": "Error loading chats", + "loading_chat": "Loading chat", + "loading_document": "Loading document", "no_recent_chats": "No recent chats", "error_loading_space": "Error loading search space", "unknown_search_space": "Unknown Search Space", @@ -165,7 +170,7 @@ "create_search_space": "Create Search Space", "add_new_search_space": "Add New Search Space", "loading": "Loading", - "fetching_spaces": "Fetching your search spaces...", + "fetching_spaces": "Fetching your search spaces", "may_take_moment": "This may take a moment", "error": "Error", "something_wrong": "Something went wrong", diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index 51e378bb2..3c4b6cf34 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -3,6 +3,7 @@ "app_name": "SurfSense", "welcome": "欢迎", "loading": "加载中...", + "initializing": "正在初始化", "save": "保存", "cancel": "取消", "delete": "删除", @@ -76,9 +77,10 @@ "passwords_no_match": "密码不匹配", "password_mismatch": "密码不匹配", "passwords_no_match_desc": "您输入的密码不一致", - "creating_account": "正在创建您的账户...", - "creating_account_btn": "创建中...", - "redirecting_login": "正在跳转到登录页面..." + "creating_account": "正在创建您的账户", + "creating_account_btn": "创建中", + "redirecting_login": "正在跳转到登录页面", + "processing_authentication": "正在处理身份验证" }, "searchSpace": { "create_title": "创建搜索空间", @@ -128,12 +130,15 @@ "api_keys": "API 密钥", "profile": "个人资料", "loading_dashboard": "正在加载仪表盘", - "checking_auth": "正在检查身份验证...", + "checking_auth": "正在检查身份验证", "loading_config": "正在加载配置", - "checking_llm_prefs": "正在检查您的 LLM 偏好设置...", + "checking_llm_prefs": "正在检查您的 LLM 偏好设置", + "setting_up_ai": "正在设置 AI", "config_error": "配置错误", "failed_load_llm_config": "无法加载您的 LLM 配置", "error_loading_chats": "加载对话失败", + "loading_chat": "正在加载对话", + "loading_document": "正在加载文档", "no_recent_chats": "暂无最近对话", "error_loading_space": "加载搜索空间失败", "unknown_search_space": "未知搜索空间", @@ -150,7 +155,7 @@ "create_search_space": "创建搜索空间", "add_new_search_space": "添加新的搜索空间", "loading": "加载中", - "fetching_spaces": "正在获取您的搜索空间...", + "fetching_spaces": "正在获取您的搜索空间", "may_take_moment": "这可能需要一些时间", "error": "错误", "something_wrong": "出现错误", From ce9e3b01b7957bbd34bf0907688ad7af1b509826 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:53:56 +0530 Subject: [PATCH 05/12] feat: expand onboarding tour with inbox step and update tooltip positioning - Added a new onboarding tour step for the inbox, guiding users to view mentions and notifications. - Updated tooltip positioning logic to accommodate the new inbox step, ensuring proper alignment during the tour. - Enhanced the check for required elements to include the inbox step, improving the tour initiation process. --- .../layout/ui/sidebar/NavSection.tsx | 4 +++- surfsense_web/components/onboarding-tour.tsx | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx index 742a27bbc..dc730bc4a 100644 --- a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx +++ b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx @@ -20,7 +20,9 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti const joyrideAttr = item.title === "Documents" || item.title.toLowerCase().includes("documents") ? { "data-joyride": "documents-sidebar" } - : {}; + : item.title === "Inbox" || item.title.toLowerCase().includes("inbox") + ? { "data-joyride": "inbox-sidebar" } + : {}; if (isCollapsed) { return ( diff --git a/surfsense_web/components/onboarding-tour.tsx b/surfsense_web/components/onboarding-tour.tsx index 717a27607..12773c932 100644 --- a/surfsense_web/components/onboarding-tour.tsx +++ b/surfsense_web/components/onboarding-tour.tsx @@ -32,6 +32,12 @@ const TOUR_STEPS: TourStep[] = [ content: "Access and manage all your uploaded documents.", placement: "right", }, + { + target: '[data-joyride="inbox-sidebar"]', + title: "Check your inbox", + content: "View mentions and notifications in one place.", + placement: "right", + }, ]; interface TooltipPosition { @@ -188,14 +194,15 @@ function TourTooltip({ const getPointerStyles = (): React.CSSProperties => { const lineLength = 16; const dotSize = 6; - // Check if this is the documents step (stepIndex === 1) + // Check if this is the documents step (stepIndex === 1) or inbox step (stepIndex === 2) const isDocumentsStep = stepIndex === 1; + const isInboxStep = stepIndex === 2; if (position.pointerPosition === "left") { return { position: "absolute", left: -lineLength - dotSize, - top: isDocumentsStep ? "calc(50% - 8px)" : "50%", + top: isDocumentsStep || isInboxStep ? "calc(50% - 8px)" : "50%", transform: "translateY(-50%)", display: "flex", alignItems: "center", @@ -518,12 +525,13 @@ export function OnboardingTour() { // User is new and hasn't seen tour - wait for DOM elements and start tour const checkAndStartTour = () => { - // Check if both required elements exist + // Check if all required elements exist const connectorEl = document.querySelector(TOUR_STEPS[0].target); const documentsEl = document.querySelector(TOUR_STEPS[1].target); + const inboxEl = document.querySelector(TOUR_STEPS[2].target); - if (connectorEl && documentsEl) { - // Both elements found, start tour + if (connectorEl && documentsEl && inboxEl) { + // All elements found, start tour setIsActive(true); setTargetEl(connectorEl); setSpotlightTargetEl(connectorEl); From eaecc091e331207ec2649dd3d913d9da8b302fe1 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:05:42 +0530 Subject: [PATCH 06/12] feat: add check icon to active items in sidebar navigation and user profile - Introduced a Check icon to visually indicate active items in the NavSection and SidebarUserProfile components. - Updated styling to ensure the Check icon appears correctly without altering the background color for active items. - Enhanced user experience by providing a clear visual cue for selected themes and languages in the sidebar. --- .../layout/ui/sidebar/NavSection.tsx | 13 ++-- .../layout/ui/sidebar/SidebarUserProfile.tsx | 60 +++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx index dc730bc4a..d58f52612 100644 --- a/surfsense_web/components/layout/ui/sidebar/NavSection.tsx +++ b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx @@ -1,5 +1,6 @@ "use client"; +import { Check } from "lucide-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import type { NavItem } from "../../types/layout.types"; @@ -34,12 +35,14 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti className={cn( "relative flex h-10 w-10 items-center justify-center rounded-md transition-colors", "hover:bg-accent hover:text-accent-foreground", - "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", - item.isActive && "bg-accent text-accent-foreground" + "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" )} {...joyrideAttr} > + {item.isActive && ( + + )} {item.badge && ( {item.badge} @@ -64,13 +67,15 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti className={cn( "flex items-center gap-2 rounded-md mx-2 px-2 py-1.5 text-sm transition-colors text-left", "hover:bg-accent hover:text-accent-foreground", - "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", - item.isActive && "bg-accent text-accent-foreground" + "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" )} {...joyrideAttr} > {item.title} + {item.isActive && ( + + )} {item.badge && ( {item.badge} diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index 982d88e8b..66fe24565 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronUp, Languages, Laptop, LogOut, Moon, Settings, Sun } from "lucide-react"; +import { Check, ChevronUp, Languages, Laptop, LogOut, Moon, Settings, Sun } from "lucide-react"; import { useTranslations } from "next-intl"; import { DropdownMenu, @@ -197,11 +197,14 @@ export function SidebarUserProfile({ className={cn( "mb-1 last:mb-0 transition-all", "hover:bg-accent/50", - isSelected && "bg-accent/80" + isSelected && "text-primary" )} > {t(themeOption.value)} + {isSelected && ( + + )} ); })} @@ -226,11 +229,14 @@ export function SidebarUserProfile({ className={cn( "mb-1 last:mb-0 transition-all", "hover:bg-accent/50", - isSelected && "bg-accent/80" + isSelected && "text-primary" )} > {language.flag} {language.name} + {isSelected && ( + + )} ); })} @@ -302,26 +308,29 @@ export function SidebarUserProfile({ {t("theme")} - - {THEMES.map((themeOption) => { - const Icon = themeOption.icon; - const isSelected = theme === themeOption.value; - return ( - handleThemeChange(themeOption.value)} - className={cn( - "mb-1 last:mb-0 transition-all", - "hover:bg-accent/50", - isSelected && "bg-accent/80" - )} - > - - {t(themeOption.value)} - - ); - })} - + + {THEMES.map((themeOption) => { + const Icon = themeOption.icon; + const isSelected = theme === themeOption.value; + return ( + handleThemeChange(themeOption.value)} + className={cn( + "mb-1 last:mb-0 transition-all", + "hover:bg-accent/50", + isSelected && "text-primary" + )} + > + + {t(themeOption.value)} + {isSelected && ( + + )} + + ); + })} + )} @@ -342,11 +351,14 @@ export function SidebarUserProfile({ className={cn( "mb-1 last:mb-0 transition-all", "hover:bg-accent/50", - isSelected && "bg-accent/80" + isSelected && "text-primary" )} > {language.flag} {language.name} + {isSelected && ( + + )} ); })} From de08f0644fb52a8c8bd9d5e09feea3c39fb9626d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:51:46 +0530 Subject: [PATCH 07/12] feat: add ordering to search spaces retrieval for consistent display --- surfsense_backend/app/routes/search_spaces_routes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/surfsense_backend/app/routes/search_spaces_routes.py b/surfsense_backend/app/routes/search_spaces_routes.py index bc52a52b1..147f515b3 100644 --- a/surfsense_backend/app/routes/search_spaces_routes.py +++ b/surfsense_backend/app/routes/search_spaces_routes.py @@ -129,6 +129,7 @@ async def read_search_spaces( result = await session.execute( select(SearchSpace) .filter(SearchSpace.user_id == user.id) + .order_by(SearchSpace.id.asc()) .offset(skip) .limit(limit) ) @@ -138,6 +139,7 @@ async def read_search_spaces( select(SearchSpace) .join(SearchSpaceMembership) .filter(SearchSpaceMembership.user_id == user.id) + .order_by(SearchSpace.id.asc()) .offset(skip) .limit(limit) ) From 9215118bab94e21a6d025f5c71d2dad965128593 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:43:04 +0530 Subject: [PATCH 08/12] feat: enhance documentation and fixed bug in `loading.tsx` --- surfsense_web/app/auth/callback/loading.tsx | 2 + .../components/bookstack-connect-form.tsx | 124 +-------------- .../components/luma-connect-form.tsx | 131 ---------------- .../components/obsidian-connect-form.tsx | 145 ------------------ .../content/docs/connectors/airtable.mdx | 12 +- .../content/docs/connectors/bookstack.mdx | 68 +++++++- .../content/docs/connectors/circleback.mdx | 129 +++++++++++++++- .../content/docs/connectors/clickup.mdx | 4 +- .../content/docs/connectors/confluence.mdx | 20 ++- .../content/docs/connectors/discord.mdx | 14 +- .../content/docs/connectors/elasticsearch.mdx | 113 +++++++++++++- .../content/docs/connectors/github.mdx | 40 ++--- .../content/docs/connectors/gmail.mdx | 4 +- .../docs/connectors/google-calendar.mdx | 4 +- .../content/docs/connectors/google-drive.mdx | 4 +- .../content/docs/connectors/jira.mdx | 16 +- .../content/docs/connectors/linear.mdx | 6 +- .../content/docs/connectors/luma.mdx | 66 +++++++- .../content/docs/connectors/meta.json | 3 +- .../docs/connectors/microsoft-teams.mdx | 8 +- .../content/docs/connectors/notion.mdx | 13 +- .../content/docs/connectors/obsidian.mdx | 143 +++++++++++++++++ .../content/docs/connectors/slack.mdx | 8 +- 23 files changed, 614 insertions(+), 463 deletions(-) create mode 100644 surfsense_web/content/docs/connectors/obsidian.mdx diff --git a/surfsense_web/app/auth/callback/loading.tsx b/surfsense_web/app/auth/callback/loading.tsx index 24d1891b4..676ca3632 100644 --- a/surfsense_web/app/auth/callback/loading.tsx +++ b/surfsense_web/app/auth/callback/loading.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useTranslations } from "next-intl"; import { Spinner } from "@/components/ui/spinner"; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx index 2b7123d78..789e23787 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx @@ -6,12 +6,6 @@ import type { FC } from "react"; import { useRef, useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Form, @@ -85,6 +79,7 @@ export const BookStackConnectForm: FC = ({ onSubmit, isSubmitt BOOKSTACK_TOKEN_SECRET: values.token_secret, }, is_indexable: true, + is_active: true, last_indexed_at: null, periodic_indexing_enabled: periodicEnabled, indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, @@ -302,123 +297,6 @@ export const BookStackConnectForm: FC = ({ onSubmit, isSubmitt
)} - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The BookStack connector uses the BookStack REST API to fetch all pages from your - BookStack instance that your account has access to. -

-
    -
  • - For follow up indexing runs, the connector retrieves pages that have been updated - since the last indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates should appear in your - search results within minutes. -
  • -
-
- -
-
-

Authorization

- - - API Token Required - - You need to create an API token from your BookStack instance. The token requires - "Access System API" permission. - - - -
-
-

- Step 1: Create an API Token -

-
    -
  1. Log in to your BookStack instance
  2. -
  3. Click on your profile icon → Edit Profile
  4. -
  5. Navigate to the "API Tokens" tab
  6. -
  7. Click "Create Token" and give it a name
  8. -
  9. Copy both the Token ID and Token Secret
  10. -
  11. Paste them in the form above
  12. -
-
- -
-

- Step 2: Grant necessary access -

-

- Your user account must have "Access System API" permission. The connector will - only index content your account can view. -

- - - Rate Limiting - - BookStack API has a rate limit of 180 requests per minute. The connector - automatically handles rate limiting to ensure reliable indexing. - - -
-
-
-
- -
-
-

Indexing

-
    -
  1. - Navigate to the Connector Dashboard and select the BookStack{" "} - Connector. -
  2. -
  3. - Enter your BookStack Instance URL (e.g., - https://docs.example.com) -
  4. -
  5. - Enter your Token ID and Token Secret from your - BookStack API token. -
  6. -
  7. - Click Connect to establish the connection. -
  8. -
  9. Once connected, your BookStack pages will be indexed automatically.
  10. -
- - - - What Gets Indexed - -

The BookStack connector indexes the following data:

-
    -
  • All pages from your BookStack instance
  • -
  • Page content in Markdown format
  • -
  • Page titles and metadata
  • -
  • Book and chapter hierarchy information
  • -
-
-
-
-
-
-
-
); }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx index daa2e2615..2804dbba8 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/luma-connect-form.tsx @@ -6,12 +6,6 @@ import type { FC } from "react"; import { useRef, useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Form, @@ -253,131 +247,6 @@ export const LumaConnectForm: FC = ({ onSubmit, isSubmitting } )} - - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The Luma connector uses the Luma API to fetch all events that your API key has - access to. -

-
    -
  • - For follow up indexing runs, the connector retrieves events that have been updated - since the last indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates should appear in your - search results within minutes. -
  • -
-
- -
-
-

Authorization

- - - API Key Required - - You need a Luma API key to use this connector. The key will be used to read your - Luma events with read-only permissions. - - - -
-
-

- Step 1: Get Your API Key -

-
    -
  1. Log into your Luma account
  2. -
  3. Navigate to your account settings
  4. -
  5. Go to API settings or Developer settings
  6. -
  7. Generate a new API key
  8. -
  9. Copy the generated API key
  10. -
  11. - You can also visit{" "} - - Luma API Settings - {" "} - for more information. -
  12. -
-
- -
-

- Step 2: Grant necessary access -

-

- The API key will have access to all events that your user account can see. - Make sure your account has appropriate permissions for the events you want to - index. -

- - - Data Privacy - - Only event details, descriptions, and attendee information will be indexed. - Event attachments and linked files are not indexed by this connector. - - -
-
-
-
- -
-
-

Indexing

-
    -
  1. - Navigate to the Connector Dashboard and select the Luma{" "} - Connector. -
  2. -
  3. - Place your API Key in the form field. -
  4. -
  5. - Click Connect to establish the connection. -
  6. -
  7. Once connected, your Luma events will be indexed automatically.
  8. -
- - - - What Gets Indexed - -

The Luma connector indexes the following data:

-
    -
  • Event titles and descriptions
  • -
  • Event details and metadata
  • -
  • Attendee information
  • -
  • Event dates and locations
  • -
-
-
-
-
-
-
-
); }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx index 94839b03b..3c4b64090 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx @@ -6,12 +6,6 @@ import type { FC } from "react"; import { useRef, useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Form, @@ -320,145 +314,6 @@ export const ObsidianConnectForm: FC = ({ onSubmit, isSubmitti )} - - {/* Documentation Section */} - - - - Documentation - - -
-

How it works

-

- The Obsidian connector scans your local Obsidian vault directory and indexes all - Markdown files. It preserves your note structure and extracts metadata from YAML - frontmatter. -

-
    -
  • - The connector parses frontmatter metadata (title, tags, aliases, dates, etc.) -
  • -
  • Wiki-style links ([[note]]) are extracted and preserved
  • -
  • Inline tags (#tag) are recognized and indexed
  • -
  • Content is chunked intelligently for optimal search results
  • -
  • - Subsequent indexing runs use content hashing to skip unchanged files for faster - sync -
  • -
-
- -
-
-

Setup

- - - - File System Access Required - - - The SurfSense backend must have read access to your Obsidian vault directory. - For Docker deployments, mount your vault as a volume. - - - -
-
-

- Step 1: Locate your vault -

-
    -
  1. - macOS/Linux: Right-click any note in Obsidian → "Reveal in - Finder" to see the vault folder -
  2. -
  3. - Windows: Right-click any note → "Show in system explorer" -
  4. -
  5. - Or: Click the vault switcher (bottom-left icon) → "Open - folder" next to your vault name -
  6. -
-
- -
-

- Step 2: Enter the path -

-

- Running locally (no Docker): Use the direct path to your - vault: -

-
-											{`/Users/yourname/Documents/MyObsidianVault`}
-										
-

- Running in Docker: Mount your vault as a volume in - docker-compose.yml: -

-
-											{`volumes:
-  - /path/to/your/vault:/app/obsidian_vaults/my-vault:ro`}
-										
-

- Then use /app/obsidian_vaults/my-vault as your vault path. -

-
- -
-

- Step 3: Configure exclusions -

-

- Common folders to exclude: -

-
    -
  • - .obsidian - Obsidian config (always recommended) -
  • -
  • - .trash - Obsidian's trash folder -
  • -
  • - templates - If you have a templates folder -
  • -
  • - daily-notes - If you want to exclude daily notes -
  • -
-
-
-
-
- -
-
-

What Gets Indexed

- - - Indexed Content - -

The Obsidian connector indexes:

-
    -
  • All Markdown files (.md) in your vault
  • -
  • YAML frontmatter metadata (title, tags, aliases, dates)
  • -
  • Wiki-style links between notes
  • -
  • Inline tags throughout your notes
  • -
  • Full note content with proper chunking
  • -
-
-
-
-
-
-
-
); }; diff --git a/surfsense_web/content/docs/connectors/airtable.mdx b/surfsense_web/content/docs/connectors/airtable.mdx index 366a6e8e5..db7fe3ac0 100644 --- a/surfsense_web/content/docs/connectors/airtable.mdx +++ b/surfsense_web/content/docs/connectors/airtable.mdx @@ -36,20 +36,20 @@ After registration, configure the required scopes (permissions) for your integra | Scope | Description | |-------|-------------| -| ✅ `data.recordComments:read` | See comments in records | -| ✅ `data.records:read` | See the data in records | +| `data.recordComments:read` | See comments in records | +| `data.records:read` | See the data in records | ### Base schema | Scope | Description | |-------|-------------| -| ✅ `schema.bases:read` | See the structure of a base, like table names or field types | +| `schema.bases:read` | See the structure of a base, like table names or field types | ### User metadata | Scope | Description | |-------|-------------| -| ✅ `user.email:read` | See the user's email address | +| `user.email:read` | See the user's email address | ![Scopes Configuration](/docs/connectors/airtable/airtable-scopes.png) @@ -80,7 +80,9 @@ After saving, you'll find your OAuth credentials on the integration page: 1. Copy your **Client ID** 2. Copy your **Client Secret** -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + --- diff --git a/surfsense_web/content/docs/connectors/bookstack.mdx b/surfsense_web/content/docs/connectors/bookstack.mdx index 8ee581948..f1f695e11 100644 --- a/surfsense_web/content/docs/connectors/bookstack.mdx +++ b/surfsense_web/content/docs/connectors/bookstack.mdx @@ -3,4 +3,70 @@ title: Bookstack description: Connect your Bookstack instance to SurfSense --- -# Documentation in progress \ No newline at end of file +# BookStack Integration Setup Guide + +This guide walks you through connecting your BookStack instance to SurfSense. + +## How it works + +The BookStack connector uses the BookStack REST API to fetch all pages from your BookStack instance that your account has access to. + +- For follow up indexing runs, the connector retrieves pages that have been updated since the last indexing attempt. +- Indexing should be configured to run periodically, so updates should appear in your search results within minutes. + +--- + +## Authorization + + +You need to create an API token from your BookStack instance. The token requires "Access System API" permission. + + +### Step 1: Create an API Token + +1. Log in to your BookStack instance +2. Click on your profile icon → Edit Profile +3. Navigate to the "API Tokens" tab +4. Click "Create Token" and give it a name +5. Copy both the **Token ID** and **Token Secret** +6. Paste them in the connector form + +### Step 2: Grant necessary access + +Your user account must have "Access System API" permission. The connector will only index content your account can view. + + +BookStack API has a rate limit of 180 requests per minute. The connector automatically handles rate limiting to ensure reliable indexing. + + + +Enable periodic sync to automatically re-index pages when content changes. Available frequencies: Every 5 minutes, 15 minutes, hourly, every 6 hours, daily, or weekly. + + +--- + +## Connecting to SurfSense + +1. Navigate to the Connector Dashboard and select the **BookStack** Connector. +2. Fill in the required fields: + +| Field | Description | Example | +|-------|-------------|---------| +| **Connector Name** | A friendly name to identify this connector | `My BookStack Connector` | +| **BookStack Base URL** | The full URL of your BookStack instance | `https://docs.example.com` | +| **Token ID** | Your API Token ID from BookStack | | +| **Token Secret** | Your API Token Secret from BookStack | | + +3. Click **Connect** to establish the connection. +4. Once connected, your BookStack pages will be indexed automatically. + +### What Gets Indexed + +The BookStack connector indexes the following data: + +| Data Type | Description | +|-----------|-------------| +| Pages | All pages from your BookStack instance | +| Content | Page content in Markdown format | +| Metadata | Page titles and metadata | +| Structure | Book and chapter hierarchy information | diff --git a/surfsense_web/content/docs/connectors/circleback.mdx b/surfsense_web/content/docs/connectors/circleback.mdx index a5c90a28f..709e35f45 100644 --- a/surfsense_web/content/docs/connectors/circleback.mdx +++ b/surfsense_web/content/docs/connectors/circleback.mdx @@ -1,8 +1,133 @@ --- title: Circleback -description: Connect your circleback to SurfSense +description: Connect your Circleback meetings to SurfSense --- -# Documentation in progress +# Circleback Integration Setup Guide +This guide walks you through connecting your Circleback meetings to SurfSense for meeting search and AI-powered insights. +## How it works + +The Circleback connector uses a **webhook-based integration**. Unlike other connectors that pull data from APIs, Circleback automatically pushes meeting data to SurfSense when meetings are processed. + +- Meetings are sent to SurfSense immediately after Circleback processes them +- No API keys or credentials required +- No periodic indexing needed - data arrives in real-time +- Each meeting is stored with its notes, transcript, action items, and insights + +--- + +## What Gets Indexed + +| Content Type | Description | +|--------------|-------------| +| Meeting Notes | AI-generated meeting notes in Markdown format | +| Transcript | Full meeting transcript with speaker identification and timestamps | +| Action Items | Tasks with assignees and status | +| Attendees | Names and emails of meeting participants | +| Insights | Custom insights extracted from the meeting | +| Metadata | Date, duration, tags, meeting URL, and Circleback link | + + + Meeting recordings are not stored in SurfSense, but a link to the recording (valid for 24 hours from Circleback) is included in the indexed document. + + +--- + +## Setup Guide + +### Step 1: Create the Connector in SurfSense + +1. Navigate to **Connectors** → **Add Connector** → **Circleback** +2. Enter a connector name (e.g., `My Circleback Meetings`) +3. Click **Connect** to create the connector + + +Circleback uses webhooks, so no API key or authentication is required. The webhook URL is unique to your search space. + + +### Step 2: Copy Your Webhook URL + +After creating the connector: + +1. Open the connector settings +2. Find the **Webhook URL** field +3. Click **Copy** to copy the URL to your clipboard + +The webhook URL looks like: +``` +https://your-surfsense-url/api/v1/webhooks/circleback/{search_space_id} +``` + +### Step 3: Configure Circleback Automation + +1. Log into your [Circleback account](https://app.circleback.ai) +2. Go to **Automations** (in the main navigation) +3. Click **Create automation** +4. Add conditions to filter which meetings trigger the webhook (optional): + - Specific calendar(s) + - Meeting tags + - Meeting participants +5. Select **Send webhook request** as the action +6. Enter your SurfSense webhook URL +7. Select the meeting outcomes to include (notes, action items, transcript, insights) +8. Optionally test with your most recent meeting +9. Name and save the automation + +For detailed instructions, see [Circleback's official webhook documentation](https://circleback.ai/docs/webhook-integration). + + + Make sure your SurfSense backend is accessible from the internet for Circleback to send webhooks. If self-hosting, you may need to configure a reverse proxy or use a tunneling service. + + +--- + +## Connector Configuration + +| Field | Description | Required | +|-------|-------------|----------| +| **Connector Name** | A friendly name to identify this connector | Yes | + + + Unlike other connectors, Circleback doesn't support periodic indexing since it's webhook-based. Meetings are automatically pushed to SurfSense when processed by Circleback. + + +--- + +## Verifying the Integration + +Once configured, new meetings will automatically appear in SurfSense after Circleback processes them. To verify: + +1. Attend or process a meeting with Circleback +2. Wait for Circleback to complete processing (usually a few minutes after the meeting ends) +3. Check your SurfSense search space for the new meeting document + +Each meeting document includes: +- A direct link to view the meeting on Circleback +- Full searchable transcript +- Extracted action items with assignees +- Meeting metadata and insights + +--- + +## Troubleshooting + +**Meetings not appearing** +- Verify the webhook URL is correctly configured in Circleback +- Check that your SurfSense backend is accessible from the internet +- Look for errors in your SurfSense backend logs + +**Webhook URL not showing** +- Refresh the connector configuration page +- Ensure the connector was created successfully +- Check that your backend is running + +**Duplicate meetings** +- Each meeting is uniquely identified by its Circleback meeting ID +- If content changes, the existing document is updated rather than duplicated + +**Delayed indexing** +- Meeting data is processed asynchronously +- Documents should appear within a minute of receiving the webhook +- Check the task queue if experiencing longer delays diff --git a/surfsense_web/content/docs/connectors/clickup.mdx b/surfsense_web/content/docs/connectors/clickup.mdx index 1b732c968..960b88370 100644 --- a/surfsense_web/content/docs/connectors/clickup.mdx +++ b/surfsense_web/content/docs/connectors/clickup.mdx @@ -36,7 +36,9 @@ After creating the app, you'll see your credentials: 1. Copy your **Client ID** 2. Copy your **Client Secret** (click "Show" to reveal, or "Regenerate" if needed) -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + --- diff --git a/surfsense_web/content/docs/connectors/confluence.mdx b/surfsense_web/content/docs/connectors/confluence.mdx index fad9f3e3d..57116cf29 100644 --- a/surfsense_web/content/docs/connectors/confluence.mdx +++ b/surfsense_web/content/docs/connectors/confluence.mdx @@ -28,7 +28,9 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration 2. Check the box to agree to Atlassian's developer terms 3. Click **"Create"** -> ℹ️ New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse. + + New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse. + ![Create New Integration Form](/docs/connectors/atlassian/atlassian-name-integration.png) @@ -41,7 +43,9 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration ``` 3. Click **"Save changes"** -> ℹ️ You can enter up to 10 redirect URIs, one per line. + + You can enter up to 10 redirect URIs, one per line. + ![Authorization Callback URLs](/docs/connectors/atlassian/atlassian-authorization.png) @@ -62,7 +66,7 @@ Select the **"Classic scopes"** tab and enable: | Scope Name | Code | Description | |------------|------|-------------| -| ✅ Read user | `read:confluence-user` | View user information in Confluence that you have access to, including usernames, email addresses and profile pictures | +| Read user | `read:confluence-user` | View user information in Confluence that you have access to, including usernames, email addresses and profile pictures | ![Confluence API Classic Scopes](/docs/connectors/atlassian/confluence/atlassian-confluence-classic-scopes.png) @@ -72,9 +76,9 @@ Select the **"Granular scopes"** tab and enable: | Scope Name | Code | Description | |------------|------|-------------| -| ✅ View pages | `read:page:confluence` | View page content | -| ✅ View comments | `read:comment:confluence` | View comments on pages or blogposts | -| ✅ View spaces | `read:space:confluence` | View space details | +| View pages | `read:page:confluence` | View page content | +| View comments | `read:comment:confluence` | View comments on pages or blogposts | +| View spaces | `read:space:confluence` | View space details | 4. Click **"Save"** @@ -85,7 +89,9 @@ Select the **"Granular scopes"** tab and enable: 1. In the left sidebar, click **"Settings"** 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + --- diff --git a/surfsense_web/content/docs/connectors/discord.mdx b/surfsense_web/content/docs/connectors/discord.mdx index 6bb64e7e7..a90bcfe87 100644 --- a/surfsense_web/content/docs/connectors/discord.mdx +++ b/surfsense_web/content/docs/connectors/discord.mdx @@ -38,7 +38,9 @@ You'll also see your **Application ID** and **Public Key** on this page. http://localhost:8000/api/v1/auth/discord/connector/callback ``` -> ⚠️ Keep **Public Client** disabled (off) since SurfSense uses a server to make requests. + + Keep **Public Client** disabled (off) since SurfSense uses a server to make requests. + ![OAuth2 Configuration](/docs/connectors/discord/discord-oauth2.png) @@ -46,13 +48,15 @@ You'll also see your **Application ID** and **Public Key** on this page. 1. In the left sidebar, click **"Bot"** 2. Configure the **Authorization Flow**: - - ✅ **Public Bot** - Enable to allow anyone to add the bot to servers + - **Public Bot** - Enable to allow anyone to add the bot to servers 3. Enable **Privileged Gateway Intents**: - - ✅ **Server Members Intent** - Required to receive GUILD_MEMBERS events - - ✅ **Message Content Intent** - Required to receive message content + - **Server Members Intent** - Required to receive GUILD_MEMBERS events + - **Message Content Intent** - Required to receive message content -> ⚠️ Once your bot reaches 100+ servers, these intents will require verification and approval. + + Once your bot reaches 100+ servers, these intents will require verification and approval. + ![Bot Settings](/docs/connectors/discord/discord-bot-settings.png) diff --git a/surfsense_web/content/docs/connectors/elasticsearch.mdx b/surfsense_web/content/docs/connectors/elasticsearch.mdx index ac43cca4e..f2615a9c6 100644 --- a/surfsense_web/content/docs/connectors/elasticsearch.mdx +++ b/surfsense_web/content/docs/connectors/elasticsearch.mdx @@ -3,4 +3,115 @@ title: Elasticsearch description: Connect your Elasticsearch cluster to SurfSense --- -# Documentation in progress \ No newline at end of file +# Elasticsearch Integration Setup Guide + +This guide walks you through connecting your Elasticsearch cluster to SurfSense. + +## How it works + +The Elasticsearch connector allows you to search and retrieve documents from your Elasticsearch cluster. Configure connection details, select specific indices, and set search parameters to make your existing data searchable within SurfSense. + +- For follow-up indexing runs, the connector retrieves documents that have been updated since the last indexing attempt. +- Indexing should be configured to run periodically, so updates should appear in your search results within minutes. + +--- + +## Authorization + + +Elasticsearch requires authentication. You can use either an API key or username/password authentication. + + +### Step 1: Get Your Elasticsearch Endpoint + +You'll need the endpoint URL for your Elasticsearch cluster. This typically looks like: + +- **Cloud:** `https://your-cluster.es.region.aws.com:443` +- **Self-hosted:** `https://elasticsearch.example.com:9200` + +### Step 2: Configure Authentication + +Elasticsearch requires authentication. You can use either: + +**API Key:** A base64-encoded API key. You can create one in Elasticsearch by running: + +```bash +POST /_security/api_key +``` + +**Username & Password:** Basic authentication using your Elasticsearch username and password. + +### Step 3: Select Indices + +Specify which indices to search. You can: + +- Use wildcards: `logs-*` to match multiple indices +- List specific indices: `logs-2024, documents-2024` +- Leave empty to search all accessible indices (not recommended for performance) + + +Enable periodic sync to automatically re-index documents when content changes. Available frequencies: Every 5 minutes, 15 minutes, hourly, every 6 hours, daily, or weekly. + + +--- + +## Connecting to SurfSense + +1. Navigate to the Connector Dashboard and select the **Elasticsearch** Connector. +2. Fill in the required fields: + +| Field | Description | Example | +|-------|-------------|---------| +| **Connector Name** | A friendly name to identify this connector | `My Elasticsearch Connector` | +| **Elasticsearch Endpoint URL** | The full URL of your Elasticsearch cluster | `https://your-cluster.es.region.aws.com:443` | +| **API Key** | Your base64-encoded API key (if using API key auth) | | +| **Username** | Your Elasticsearch username (if using basic auth) | | +| **Password** | Your Elasticsearch password (if using basic auth) | | +| **Indices** | Comma-separated list of indices to search | `logs-*, documents-2024` | + +3. Click **Connect** to establish the connection. +4. Once connected, your Elasticsearch documents will be indexed automatically. + +--- + +## Advanced Configuration + +### Search Query + +The default query used for searches. Use `*` to match all documents, or specify a more complex Elasticsearch query. + +### Search Fields + +Limit searches to specific fields for better performance. Common fields include: + +- `title` - Document titles +- `content` - Main content +- `description` - Descriptions + +Leave empty to search all fields in your documents. + +### Maximum Documents + +Set a limit on the number of documents retrieved per search (1-10,000). This helps control response times and resource usage. Leave empty to use Elasticsearch's default limit. + +--- + +## Troubleshooting + +### Connection Issues + +- **Invalid URL:** Ensure your endpoint URL includes the protocol (https://) and port number if required. +- **SSL/TLS Errors:** Verify that your cluster uses HTTPS and the certificate is valid. Self-signed certificates may require additional configuration. +- **Connection Timeout:** Check your network connectivity and firewall settings. Ensure the Elasticsearch cluster is accessible from SurfSense servers. + +### Authentication Issues + +- **Invalid Credentials:** Double-check your username/password or API key. API keys must be base64-encoded. +- **Permission Denied:** Ensure your API key or user account has read permissions for the indices you want to search. +- **API Key Format:** Elasticsearch API keys are typically base64-encoded strings. Make sure you're using the full key value. + +### Search Issues + +- **No Results:** Verify that your index selection matches existing indices. Use wildcards carefully. +- **Slow Searches:** Limit the number of indices or use specific index names instead of wildcards. Reduce the maximum documents limit. +- **Field Not Found:** Ensure the search fields you specify actually exist in your Elasticsearch documents. diff --git a/surfsense_web/content/docs/connectors/github.mdx b/surfsense_web/content/docs/connectors/github.mdx index 6a4574ec4..ce60d2a32 100644 --- a/surfsense_web/content/docs/connectors/github.mdx +++ b/surfsense_web/content/docs/connectors/github.mdx @@ -3,9 +3,18 @@ title: GitHub description: Connect your GitHub repositories to SurfSense --- -# GitHub Connector +# GitHub Integration Setup Guide -Connect your GitHub repositories to SurfSense for code search and AI-powered insights. The connector uses [gitingest](https://gitingest.com) to efficiently index entire codebases. +This guide walks you through connecting your GitHub repositories to SurfSense for code search and AI-powered insights. + +## How it works + +The GitHub connector uses [gitingest](https://gitingest.com) to fetch and process repository contents from GitHub. + +- For follow-up indexing runs, the connector retrieves the latest repository state and updates changed files. +- Indexing should be configured to run periodically, so updates should appear in your search results within minutes. + +--- ## What Gets Indexed @@ -15,7 +24,9 @@ Connect your GitHub repositories to SurfSense for code search and AI-powered ins | Documentation | README files, Markdown documents, text files | | Configuration | JSON, YAML, TOML, .env examples, Dockerfiles | -> ⚠️ Binary files and files larger than 5MB are automatically excluded. + + Binary files and files larger than 5MB are automatically excluded. + --- @@ -39,7 +50,13 @@ For private repos, you need a GitHub Personal Access Token (PAT). 2. Set an expiration 3. Click **Generate token** and copy it -> ⚠️ The token starts with `ghp_`. Store it securely. + + The token starts with `ghp_`. Store it securely. + + + +Enable periodic sync to automatically re-index repositories when content changes. Available frequencies: Every 5 minutes, 15 minutes, hourly, every 6 hours, daily, or weekly. + --- @@ -53,21 +70,6 @@ For private repos, you need a GitHub Personal Access Token (PAT). --- -## Periodic Sync - -Enable periodic sync to automatically re-index repositories when content changes: - -| Frequency | Use Case | -|-----------|----------| -| Every 5 minutes | Active development | -| Every 15 minutes | Frequent commits | -| Every hour | Regular workflow | -| Every 6 hours | Less active repos | -| Daily | Reference repositories | -| Weekly | Stable codebases | - ---- - ## Troubleshooting **Repository not found** diff --git a/surfsense_web/content/docs/connectors/gmail.mdx b/surfsense_web/content/docs/connectors/gmail.mdx index 434e6ae4d..2b514f89e 100644 --- a/surfsense_web/content/docs/connectors/gmail.mdx +++ b/surfsense_web/content/docs/connectors/gmail.mdx @@ -60,7 +60,9 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS 1. After creating the OAuth client, you'll see a dialog with your credentials 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + ![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) diff --git a/surfsense_web/content/docs/connectors/google-calendar.mdx b/surfsense_web/content/docs/connectors/google-calendar.mdx index cc1eae545..7919d0361 100644 --- a/surfsense_web/content/docs/connectors/google-calendar.mdx +++ b/surfsense_web/content/docs/connectors/google-calendar.mdx @@ -59,7 +59,9 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS 1. After creating the OAuth client, you'll see a dialog with your credentials 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + ![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) diff --git a/surfsense_web/content/docs/connectors/google-drive.mdx b/surfsense_web/content/docs/connectors/google-drive.mdx index 00ea2f610..402b25566 100644 --- a/surfsense_web/content/docs/connectors/google-drive.mdx +++ b/surfsense_web/content/docs/connectors/google-drive.mdx @@ -60,7 +60,9 @@ This guide walks you through setting up a Google OAuth 2.0 integration for SurfS 1. After creating the OAuth client, you'll see a dialog with your credentials 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + ![Google Developer Console Config](/docs/connectors/google/google_oauth_config.png) diff --git a/surfsense_web/content/docs/connectors/jira.mdx b/surfsense_web/content/docs/connectors/jira.mdx index ebe639d6d..c6b5a26e1 100644 --- a/surfsense_web/content/docs/connectors/jira.mdx +++ b/surfsense_web/content/docs/connectors/jira.mdx @@ -28,7 +28,9 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration 2. Check the box to agree to Atlassian's developer terms 3. Click **"Create"** -> ℹ️ New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse. + + New OAuth 2.0 integrations use rotating refresh tokens, which improve security by limiting token validity and enabling automatic detection of token reuse. + ![Create New Integration Form](/docs/connectors/atlassian/atlassian-name-integration.png) @@ -41,7 +43,9 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration ``` 3. Click **"Save changes"** -> ℹ️ You can enter up to 10 redirect URIs, one per line. + + You can enter up to 10 redirect URIs, one per line. + ![Authorization Callback URLs](/docs/connectors/atlassian/atlassian-authorization.png) @@ -60,8 +64,8 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration | Scope Name | Code | Description | |------------|------|-------------| -| ✅ View Jira issue data | `read:jira-work` | Read Jira project and issue data, search for issues, and objects associated with issues like attachments and worklogs | -| ✅ View user profiles | `read:jira-user` | View user information in Jira that the user has access to, including usernames, email addresses, and avatars | +| View Jira issue data | `read:jira-work` | Read Jira project and issue data, search for issues, and objects associated with issues like attachments and worklogs | +| View user profiles | `read:jira-user` | View user information in Jira that the user has access to, including usernames, email addresses, and avatars | 4. Click **"Save"** @@ -72,7 +76,9 @@ This guide walks you through setting up an Atlassian OAuth 2.0 (3LO) integration 1. In the left sidebar, click **"Settings"** 2. Copy your **Client ID** and **Client Secret** -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + --- diff --git a/surfsense_web/content/docs/connectors/linear.mdx b/surfsense_web/content/docs/connectors/linear.mdx index f9dc9a62b..5fb7bc8c5 100644 --- a/surfsense_web/content/docs/connectors/linear.mdx +++ b/surfsense_web/content/docs/connectors/linear.mdx @@ -30,7 +30,7 @@ Fill in the application details: ### Settings -- ✅ **Public** - Enable this to allow the application to be installed by other workspaces +- **Public** - Enable this to allow the application to be installed by other workspaces Click **Create** to create the application. @@ -43,7 +43,9 @@ After creating the application, you'll see your OAuth credentials: 1. Copy your **Client ID** 2. Copy your **Client Secret** -> ⚠️ Never share your client secret publicly. + + Never share your client secret publicly. + ![OAuth Credentials](/docs/connectors/linear/linear-oauth-credentials.png) diff --git a/surfsense_web/content/docs/connectors/luma.mdx b/surfsense_web/content/docs/connectors/luma.mdx index e16e5a949..a218be46b 100644 --- a/surfsense_web/content/docs/connectors/luma.mdx +++ b/surfsense_web/content/docs/connectors/luma.mdx @@ -3,4 +3,68 @@ title: Luma description: Connect your Luma events to SurfSense --- -# Documentation in progress \ No newline at end of file +# Luma Integration Setup Guide + +This guide walks you through connecting your Luma events to SurfSense for event search and AI-powered insights. + +## How it works + +The Luma connector uses the Luma API to fetch all events that your API key has access to. + +- For follow-up indexing runs, the connector retrieves events that have been updated since the last indexing attempt. +- Indexing should be configured to run periodically, so updates should appear in your search results within minutes. + +--- + +## Authorization + + +You need a Luma API key to use this connector. The key will be used to read your Luma events with read-only permissions. + + +### Step 1: Get Your API Key + +1. Log into your Luma account +2. Navigate to your account settings +3. Go to API settings or Developer settings +4. Generate a new API key +5. Copy the generated API key + +You can also visit [Luma API Docs](https://docs.luma.com/reference/getting-started-with-your-api) for more information. + +### Step 2: Grant Necessary Access + +The API key will have access to all events that your user account can see. Make sure your account has appropriate permissions for the events you want to index. + + +Enable periodic sync to automatically re-index events when content changes. Available frequencies: Every 5 minutes, 15 minutes, hourly, every 6 hours, daily, or weekly. + + +--- + +## Connecting to SurfSense + +1. Navigate to the Connector Dashboard and select the **Luma** Connector. +2. Fill in the required fields: + +| Field | Description | Example | +|-------|-------------|---------| +| **Connector Name** | A friendly name to identify this connector | `My Luma Connector` | +| **Luma API Key** | Your Luma API key (will be encrypted and stored securely) | | + +3. Click **Connect** to establish the connection. +4. Once connected, your Luma events will be indexed automatically. + +### What Gets Indexed + +The Luma connector indexes the following data: + +| Data Type | Description | +|-----------|-------------| +| Event Details | Titles, descriptions, metadata | +| Attendee Info | Attendee information and lists | +| Event Metadata | Dates, locations, settings | + + + Event attachments and linked files are not indexed by this connector. + diff --git a/surfsense_web/content/docs/connectors/meta.json b/surfsense_web/content/docs/connectors/meta.json index b41e92ab9..c6f7ab383 100644 --- a/surfsense_web/content/docs/connectors/meta.json +++ b/surfsense_web/content/docs/connectors/meta.json @@ -17,7 +17,8 @@ "luma", "circleback", "elasticsearch", - "bookstack" + "bookstack", + "obsidian" ], "defaultOpen": false } diff --git a/surfsense_web/content/docs/connectors/microsoft-teams.mdx b/surfsense_web/content/docs/connectors/microsoft-teams.mdx index daa6eb375..53f36c249 100644 --- a/surfsense_web/content/docs/connectors/microsoft-teams.mdx +++ b/surfsense_web/content/docs/connectors/microsoft-teams.mdx @@ -58,7 +58,9 @@ After registration, you'll be taken to the app's **Overview** page. Here you'll ![Certificates & Secrets - Created](/docs/connectors/microsoft-teams/azure-certificates-created.png) -> ⚠️ Never share your client secret publicly or include it in code repositories. + + Never share your client secret publicly or include it in code repositories. + ## Step 6: Configure API Permissions @@ -78,7 +80,9 @@ After registration, you'll be taken to the app's **Overview** page. Here you'll 6. Click **"Add permissions"** -> ⚠️ The `ChannelMessage.Read.All` permission requires admin consent. An admin will need to click **"Grant admin consent for [Directory]"** for full functionality. + + The `ChannelMessage.Read.All` permission requires admin consent. An admin will need to click **"Grant admin consent for [Directory]"** for full functionality. + ![API Permissions](/docs/connectors/microsoft-teams/azure-api-permissions.png) diff --git a/surfsense_web/content/docs/connectors/notion.mdx b/surfsense_web/content/docs/connectors/notion.mdx index 936972f7e..0612c4f4f 100644 --- a/surfsense_web/content/docs/connectors/notion.mdx +++ b/surfsense_web/content/docs/connectors/notion.mdx @@ -52,14 +52,13 @@ After creating the integration, you'll see the configuration page with your cred ### Set Required Capabilities -Under **Content Capabilities**, enable: -- ✅ Read content +Enable the following capabilities: -Under **Comment Capabilities**, enable: -- ✅ Read comments - -Under **User Capabilities**, select: -- 🔘 Read user information including email addresses +| Capability Type | Required Setting | +|----------------|------------------| +| **Content Capabilities** | Read content | +| **Comment Capabilities** | Read comments | +| **User Capabilities** | Read user information including email addresses | Click **Save** to apply the capabilities. diff --git a/surfsense_web/content/docs/connectors/obsidian.mdx b/surfsense_web/content/docs/connectors/obsidian.mdx new file mode 100644 index 000000000..c8475c97f --- /dev/null +++ b/surfsense_web/content/docs/connectors/obsidian.mdx @@ -0,0 +1,143 @@ +--- +title: Obsidian +description: Connect your Obsidian vault to SurfSense +--- + +# Obsidian Integration Setup Guide + +This guide walks you through connecting your Obsidian vault to SurfSense for note search and AI-powered insights. + + + This connector requires direct file system access and only works with self-hosted SurfSense installations. + + +## How it works + +The Obsidian connector scans your local Obsidian vault directory and indexes all Markdown files. It preserves your note structure and extracts metadata from YAML frontmatter. + +- For follow-up indexing runs, the connector uses content hashing to skip unchanged files for faster sync. +- Indexing should be configured to run periodically, so updates should appear in your search results within minutes. + +--- + +## What Gets Indexed + +| Content Type | Description | +|--------------|-------------| +| Markdown Files | All `.md` files in your vault | +| Frontmatter | YAML metadata (title, tags, aliases, dates) | +| Wiki Links | Links between notes (`[[note]]`) | +| Inline Tags | Tags throughout your notes (`#tag`) | +| Note Content | Full content with intelligent chunking | + + + Binary files and attachments are not indexed by default. Enable "Include Attachments" to index embedded files. + + +--- + +## Quick Start (Local Installation) + +1. Navigate to **Connectors** → **Add Connector** → **Obsidian** +2. Enter your vault path: `/Users/yourname/Documents/MyVault` +3. Enter a vault name (e.g., `Personal Notes`) +4. Click **Connect Obsidian** + + + Find your vault path: In Obsidian, right-click any note → "Reveal in Finder" (macOS) or "Show in Explorer" (Windows). + + + +Enable periodic sync to automatically re-index notes when content changes. Available frequencies: Every 5 minutes, 15 minutes, hourly, every 6 hours, daily, or weekly. + + +--- + +## Docker Setup + +For Docker deployments, you need to mount your Obsidian vault as a volume. + +### Step 1: Update docker-compose.yml + +Add your vault as a volume mount to the SurfSense backend service: + +```yaml +services: + surfsense: + # ... other config + volumes: + - /path/to/your/obsidian/vault:/app/obsidian_vaults/my-vault:ro +``` + + + The `:ro` flag mounts the vault as read-only, which is recommended for security. + + +### Step 2: Configure the Connector + +Use the **container path** (not your local path) when setting up the connector: + +| Your Local Path | Container Path (use this) | +|-----------------|---------------------------| +| `/Users/john/Documents/MyVault` | `/app/obsidian_vaults/my-vault` | +| `C:\Users\john\Documents\MyVault` | `/app/obsidian_vaults/my-vault` | + +### Example: Multiple Vaults + +```yaml +volumes: + - /Users/john/Documents/PersonalNotes:/app/obsidian_vaults/personal:ro + - /Users/john/Documents/WorkNotes:/app/obsidian_vaults/work:ro +``` + +Then create separate connectors for each vault using `/app/obsidian_vaults/personal` and `/app/obsidian_vaults/work`. + +--- + +## Connector Configuration + +| Field | Description | Required | +|-------|-------------|----------| +| **Connector Name** | A friendly name to identify this connector | Yes | +| **Vault Path** | Absolute path to your vault (container path for Docker) | Yes | +| **Vault Name** | Display name for your vault in search results | Yes | +| **Exclude Folders** | Comma-separated folder names to skip | No | +| **Include Attachments** | Index embedded files (images, PDFs) | No | + +--- + +## Recommended Exclusions + +Common folders to exclude from indexing: + +| Folder | Reason | +|--------|--------| +| `.obsidian` | Obsidian config files (always exclude) | +| `.trash` | Obsidian's trash folder | +| `templates` | Template files you don't want searchable | +| `daily-notes` | If you want to exclude daily notes | +| `attachments` | If not using "Include Attachments" | + +Default exclusions: `.obsidian,.trash` + +--- + +## Troubleshooting + +**Vault not found / Permission denied** +- Verify the path exists and is accessible +- For Docker: ensure the volume is mounted correctly in `docker-compose.yml` +- Check file permissions: SurfSense needs read access to the vault directory + +**No notes indexed** +- Ensure your vault contains `.md` files +- Check that notes aren't in excluded folders +- Verify the path points to the vault root (contains `.obsidian` folder) + +**Changes not appearing** +- Wait for the next sync cycle, or manually trigger re-indexing +- For Docker: restart the container if you modified volume mounts + +**Docker: "path not found" error** +- Use the container path (`/app/obsidian_vaults/...`), not your local path +- Verify the volume mount in `docker-compose.yml` matches diff --git a/surfsense_web/content/docs/connectors/slack.mdx b/surfsense_web/content/docs/connectors/slack.mdx index ccabe6f9e..072b83343 100644 --- a/surfsense_web/content/docs/connectors/slack.mdx +++ b/surfsense_web/content/docs/connectors/slack.mdx @@ -21,7 +21,9 @@ This guide walks you through setting up a Slack OAuth integration for SurfSense. 2. Select the workspace to develop your app in 3. Click **"Create App"** -> ⚠️ You won't be able to change the workspace later. The workspace will control the app even if you leave it. + + You won't be able to change the workspace later. The workspace will control the app even if you leave it. + ![Name App & Choose Workspace](/docs/connectors/slack/slack-name-workspace.png) @@ -32,7 +34,9 @@ After creating the app, you'll be taken to the **Basic Information** page. Here 1. Copy your **Client ID** 2. Copy your **Client Secret** (click Show to reveal) -> ⚠️ Never share your app credentials publicly. + + Never share your app credentials publicly. + ![Basic Information - App Credentials](/docs/connectors/slack/slack-app-credentials.png) From 2d17d1a1b6684e83d591601ab677fa3254ae976a Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:23:45 +0530 Subject: [PATCH 09/12] feat: replace Loader2 with Spinner component for consistent loading indicators --- .../(manage)/components/DocumentsTableShell.tsx | 5 +++-- .../editor/[documentId]/page.tsx | 11 +++++------ .../dashboard/[search_space_id]/onboard/page.tsx | 4 ++-- .../dashboard/[search_space_id]/team/page.tsx | 14 +++++++------- .../user/settings/components/ProfileContent.tsx | 7 ++++--- surfsense_web/app/invite/[invite_code]/page.tsx | 7 +++---- .../components/assistant-ui/attachment.tsx | 7 ++++--- .../assistant-ui/chat-session-status.tsx | 4 ++-- .../components/assistant-ui/connector-popup.tsx | 5 +++-- .../components/connector-card.tsx | 7 ++++--- .../views/connector-connect-view.tsx | 5 +++-- .../views/connector-edit-view.tsx | 7 ++++--- .../views/indexing-configuration-view.tsx | 5 +++-- .../tabs/active-connectors-tab.tsx | 7 ++++--- .../views/connector-accounts-list-view.tsx | 7 ++++--- .../views/youtube-crawler-view.tsx | 5 +++-- .../comment-item/comment-actions.tsx | 6 ++++-- .../member-mention-picker.tsx | 4 ++-- .../connectors/composio-drive-folder-tree.tsx | 6 +++--- .../connectors/google-drive-folder-tree.tsx | 6 +++--- .../ui/dialogs/CreateSearchSpaceDialog.tsx | 5 +++-- .../layout/ui/sidebar/AllPrivateChatsSidebar.tsx | 6 +++--- .../layout/ui/sidebar/AllSharedChatsSidebar.tsx | 6 +++--- .../components/layout/ui/sidebar/NavSection.tsx | 7 ------- .../components/new-chat/model-selector.tsx | 4 ++-- .../components/settings/llm-role-manager.tsx | 4 ++-- .../components/settings/model-config-manager.tsx | 6 +++--- .../components/shared/llm-config-form.tsx | 4 ++-- .../components/sources/DocumentUploadTab.tsx | 5 +++-- .../components/tool-ui/generate-podcast.tsx | 9 +++++---- surfsense_web/components/tool-ui/image/index.tsx | 5 +++-- .../components/tool-ui/media-card/index.tsx | 16 ++++++++-------- surfsense_web/components/tool-ui/write-todos.tsx | 4 ++-- surfsense_web/messages/en.json | 8 ++++---- 34 files changed, 113 insertions(+), 105 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx index 38d61a6ce..6d28f9166 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronDown, ChevronUp, FileX, Loader2, Plus } from "lucide-react"; +import { ChevronDown, ChevronUp, FileX, Plus } from "lucide-react"; import { motion } from "motion/react"; import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -8,6 +8,7 @@ import React from "react"; import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { DocumentViewer } from "@/components/document-viewer"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import { Checkbox } from "@/components/ui/checkbox"; import { Table, @@ -114,7 +115,7 @@ export function DocumentsTableShell({ {loading ? (
- +

{t("loading")}

diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index 2320b3b9a..74104f450 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -1,8 +1,7 @@ "use client"; -import { useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; -import { AlertCircle, ArrowLeft, FileText, Loader2, Save } from "lucide-react"; +import { AlertCircle, ArrowLeft, FileText, Save } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; @@ -21,6 +20,7 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Spinner } from "@/components/ui/spinner"; import { notesApiService } from "@/lib/apis/notes-api.service"; import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; @@ -78,7 +78,6 @@ function extractTitleFromBlockNote(blocknoteDocument: BlockNoteDocument): string export default function EditorPage() { const params = useParams(); const router = useRouter(); - const queryClient = useQueryClient(); const documentId = params.documentId as string; const searchSpaceId = Number(params.search_space_id); const isNewNote = documentId === "new"; @@ -349,8 +348,8 @@ export default function EditorPage() {
- -

Loading editor...

+ +

Loading editor

@@ -437,7 +436,7 @@ export default function EditorPage() { > {saving ? ( <> - + {isNewNote ? "Creating" : "Saving"} ) : ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx index 25f189203..1b7fa297f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useAtomValue } from "jotai"; -import { Loader2 } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; @@ -17,6 +16,7 @@ import { import { Logo } from "@/components/Logo"; import { LLMConfigForm, type LLMConfigFormData } from "@/components/shared/llm-config-form"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Spinner } from "@/components/ui/spinner"; import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; export default function OnboardPage() { @@ -156,7 +156,7 @@ export default function OnboardPage() {
- +
diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index b661e9222..c535da9f1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -14,7 +14,6 @@ import { Hash, Link2, LinkIcon, - Loader2, Logs, type LucideIcon, MessageCircle, @@ -106,6 +105,7 @@ import { } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import { Spinner } from "@/components/ui/spinner"; import type { CreateInviteRequest, DeleteInviteRequest, @@ -321,7 +321,7 @@ export default function TeamManagementPage() { animate={{ opacity: 1, scale: 1 }} className="flex flex-col items-center gap-4" > - +

Loading team data...

@@ -571,7 +571,7 @@ function MembersTab({ if (loading) { return (
- +
); } @@ -911,7 +911,7 @@ function RolesTab({ if (loading) { return (
- +
); } @@ -1068,7 +1068,7 @@ function InvitesTab({ if (loading) { return (
- +
); } @@ -1446,7 +1446,7 @@ function CreateInviteDialog({
diff --git a/surfsense_web/app/invite/[invite_code]/page.tsx b/surfsense_web/app/invite/[invite_code]/page.tsx index 1f2a786a5..a778962b4 100644 --- a/surfsense_web/app/invite/[invite_code]/page.tsx +++ b/surfsense_web/app/invite/[invite_code]/page.tsx @@ -6,8 +6,6 @@ import { AlertCircle, ArrowRight, CheckCircle2, - Clock, - Loader2, LogIn, Shield, Sparkles, @@ -30,6 +28,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { Spinner } from "@/components/ui/spinner"; import type { AcceptInviteResponse } from "@/contracts/types/invites.types"; import { invitesApiService } from "@/lib/apis/invites-api.service"; import { getBearerToken } from "@/lib/auth-utils"; @@ -164,7 +163,7 @@ export default function InviteAcceptPage() { animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }} > - +

Loading invite details...

@@ -353,7 +352,7 @@ export default function InviteAcceptPage() { @@ -33,8 +34,9 @@ export function CommentActions({ canEdit, canDelete, onEdit, onDelete }: Comment Edit )} + {canEdit && canDelete && } {canDelete && ( - + Delete diff --git a/surfsense_web/components/chat-comments/member-mention-picker/member-mention-picker.tsx b/surfsense_web/components/chat-comments/member-mention-picker/member-mention-picker.tsx index 77ba1b697..78dff877a 100644 --- a/surfsense_web/components/chat-comments/member-mention-picker/member-mention-picker.tsx +++ b/surfsense_web/components/chat-comments/member-mention-picker/member-mention-picker.tsx @@ -1,7 +1,7 @@ "use client"; -import { Loader2 } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Spinner } from "@/components/ui/spinner"; import { MemberMentionItem } from "./member-mention-item"; import type { MemberMentionPickerProps } from "./types"; @@ -24,7 +24,7 @@ export function MemberMentionPicker({ if (isLoading) { return (
- +
); } diff --git a/surfsense_web/components/connectors/composio-drive-folder-tree.tsx b/surfsense_web/components/connectors/composio-drive-folder-tree.tsx index 76ae218cb..bc6330faa 100644 --- a/surfsense_web/components/connectors/composio-drive-folder-tree.tsx +++ b/surfsense_web/components/connectors/composio-drive-folder-tree.tsx @@ -10,12 +10,12 @@ import { FolderOpen, HardDrive, Image, - Loader2, Presentation, } from "lucide-react"; import { useState } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Spinner } from "@/components/ui/spinner"; import { useComposioDriveFolders } from "@/hooks/use-composio-drive-folders"; import { connectorsApiService } from "@/lib/apis/connectors-api.service"; import { cn } from "@/lib/utils"; @@ -253,7 +253,7 @@ export function ComposioDriveFolderTree({ aria-label={isExpanded ? `Collapse ${item.name}` : `Expand ${item.name}`} > {isLoading ? ( - + ) : isExpanded ? ( ) : ( @@ -344,7 +344,7 @@ export function ComposioDriveFolderTree({ {isLoadingRoot && (
- +
)} diff --git a/surfsense_web/components/connectors/google-drive-folder-tree.tsx b/surfsense_web/components/connectors/google-drive-folder-tree.tsx index 30df2d788..bb6fd822e 100644 --- a/surfsense_web/components/connectors/google-drive-folder-tree.tsx +++ b/surfsense_web/components/connectors/google-drive-folder-tree.tsx @@ -10,12 +10,12 @@ import { FolderOpen, HardDrive, Image, - Loader2, Presentation, } from "lucide-react"; import { useState } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Spinner } from "@/components/ui/spinner"; import { useGoogleDriveFolders } from "@/hooks/use-google-drive-folders"; import { connectorsApiService } from "@/lib/apis/connectors-api.service"; import { cn } from "@/lib/utils"; @@ -253,7 +253,7 @@ export function GoogleDriveFolderTree({ aria-label={isExpanded ? `Collapse ${item.name}` : `Expand ${item.name}`} > {isLoading ? ( - + ) : isExpanded ? ( ) : ( @@ -344,7 +344,7 @@ export function GoogleDriveFolderTree({ {isLoadingRoot && (
- +
)} diff --git a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx index 427cbd31b..c0f92b440 100644 --- a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx +++ b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useAtomValue } from "jotai"; -import { Loader2, Plus, Search } from "lucide-react"; +import { Plus, Search } from "lucide-react"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -26,6 +26,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Spinner } from "@/components/ui/spinner"; import { trackSearchSpaceCreated } from "@/lib/posthog/events"; const formSchema = z.object({ @@ -145,7 +146,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac @@ -337,7 +337,7 @@ export function MediaCardLoading({ title = "Loading preview..." }: { title?: str return (
- +
diff --git a/surfsense_web/components/tool-ui/write-todos.tsx b/surfsense_web/components/tool-ui/write-todos.tsx index a5da31e9e..9b959bd33 100644 --- a/surfsense_web/components/tool-ui/write-todos.tsx +++ b/surfsense_web/components/tool-ui/write-todos.tsx @@ -2,7 +2,6 @@ import { makeAssistantToolUI, useAssistantState } from "@assistant-ui/react"; import { useAtomValue, useSetAtom } from "jotai"; -import { Loader2 } from "lucide-react"; import { useEffect, useMemo } from "react"; import { z } from "zod"; import { @@ -11,6 +10,7 @@ import { registerPlanOwner, updatePlanStateAtom, } from "@/atoms/chat/plan-state.atom"; +import { Spinner } from "@/components/ui/spinner"; import { Plan, PlanErrorBoundary, parseSerializablePlan, TodoStatusSchema } from "./plan"; // ============================================================================ @@ -46,7 +46,7 @@ function WriteTodosLoading() { return (
- + Creating plan...
diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 59b9cf56a..f23359bf2 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -321,10 +321,10 @@ "columns": "Columns", "confirm_delete": "Confirm Delete", "confirm_delete_desc": "Are you sure you want to delete {count} document(s)? This action cannot be undone.", - "uploading": "Uploading...", + "uploading": "Uploading", "upload_success": "Document uploaded successfully", "upload_failed": "Failed to upload document", - "loading": "Loading documents...", + "loading": "Loading documents", "error_loading": "Error loading documents", "retry": "Retry", "no_documents": "No documents found", @@ -391,8 +391,8 @@ "selected_files": "Selected Files ({count})", "total_size": "Total size", "clear_all": "Clear all", - "uploading_files": "Uploading files...", - "uploading": "Uploading...", + "uploading_files": "Uploading files", + "uploading": "Uploading", "upload_button": "Upload {count} {count, plural, one {file} other {files}}", "upload_initiated": "Upload Task Initiated", "upload_initiated_desc": "Files Uploading Initiated", From 66a3c877efd90e3a0bee6276022283a8e22b2ef0 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:15:25 +0530 Subject: [PATCH 10/12] feat: replace unified loading approach with global loading provider and refactor loading handling across components - Introduced GlobalLoadingProvider to manage a consistent loading overlay across the application. - Replaced existing loading implementations with useGlobalLoadingEffect hook for better control and to prevent animation resets. - Updated components such as LoginPage, AuthCallbackPage, and DashboardLayout to utilize the new global loading mechanism. - Removed UnifiedLoadingScreen component to streamline loading management and enhance user experience. --- surfsense_web/app/(home)/layout.tsx | 6 +- surfsense_web/app/(home)/login/page.tsx | 12 +- surfsense_web/app/auth/callback/loading.tsx | 21 ++-- surfsense_web/app/auth/callback/page.tsx | 8 +- .../[search_space_id]/client-layout.tsx | 26 +++-- surfsense_web/app/dashboard/layout.tsx | 9 +- surfsense_web/app/dashboard/loading.tsx | 21 ++-- surfsense_web/app/dashboard/page.tsx | 13 ++- surfsense_web/app/layout.tsx | 5 +- surfsense_web/atoms/ui/loading.atoms.ts | 31 ++++++ surfsense_web/components/TokenHandler.tsx | 10 +- .../components/providers/ElectricProvider.tsx | 16 +-- .../providers/GlobalLoadingProvider.tsx | 80 ++++++++++++++ .../components/ui/unified-loading-screen.tsx | 72 ------------ surfsense_web/hooks/use-global-loading.ts | 104 ++++++++++++++++++ 15 files changed, 294 insertions(+), 140 deletions(-) create mode 100644 surfsense_web/atoms/ui/loading.atoms.ts create mode 100644 surfsense_web/components/providers/GlobalLoadingProvider.tsx delete mode 100644 surfsense_web/components/ui/unified-loading-screen.tsx create mode 100644 surfsense_web/hooks/use-global-loading.ts diff --git a/surfsense_web/app/(home)/layout.tsx b/surfsense_web/app/(home)/layout.tsx index 9488ee875..f1ceffac0 100644 --- a/surfsense_web/app/(home)/layout.tsx +++ b/surfsense_web/app/(home)/layout.tsx @@ -1,14 +1,18 @@ "use client"; +import { usePathname } from "next/navigation"; import { FooterNew } from "@/components/homepage/footer-new"; import { Navbar } from "@/components/homepage/navbar"; export default function HomePageLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const isAuthPage = pathname === "/login" || pathname === "/register"; + return (
{children} - + {!isAuthPage && }
); } diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx index 0026fdcd0..0dc9c445f 100644 --- a/surfsense_web/app/(home)/login/page.tsx +++ b/surfsense_web/app/(home)/login/page.tsx @@ -6,7 +6,7 @@ import { useTranslations } from "next-intl"; import { Suspense, useEffect, useState } from "react"; import { toast } from "sonner"; import { Logo } from "@/components/Logo"; -import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { AUTH_TYPE } from "@/lib/env-config"; import { AmbientBackground } from "./AmbientBackground"; @@ -92,9 +92,12 @@ function LoginContent() { setIsLoading(false); }, [searchParams, t, tCommon]); - // Show loading state while determining auth type + // Use global loading screen for auth type determination - spinner animation won't reset + useGlobalLoadingEffect(isLoading, tCommon("loading"), "login"); + + // Show nothing while loading - the GlobalLoadingProvider handles the loading UI if (isLoading) { - return ; + return null; } if (authType === "GOOGLE") { @@ -176,8 +179,9 @@ function LoginContent() { } export default function LoginPage() { + // Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI return ( - }> + ); diff --git a/surfsense_web/app/auth/callback/loading.tsx b/surfsense_web/app/auth/callback/loading.tsx index 676ca3632..0c94e1ee0 100644 --- a/surfsense_web/app/auth/callback/loading.tsx +++ b/surfsense_web/app/auth/callback/loading.tsx @@ -1,21 +1,14 @@ "use client"; import { useTranslations } from "next-intl"; -import { Spinner } from "@/components/ui/spinner"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; export default function AuthCallbackLoading() { const t = useTranslations("auth"); - return ( -
-
-
- -
- - {t("processing_authentication")} - -
-
- ); -} + // Use global loading - spinner animation won't reset when page transitions + useGlobalLoadingEffect(true, t("processing_authentication"), "default"); + + // Return null - the GlobalLoadingProvider handles the loading UI + return null; +} diff --git a/surfsense_web/app/auth/callback/page.tsx b/surfsense_web/app/auth/callback/page.tsx index 920581654..4050eefb6 100644 --- a/surfsense_web/app/auth/callback/page.tsx +++ b/surfsense_web/app/auth/callback/page.tsx @@ -1,15 +1,13 @@ "use client"; import { Suspense } from "react"; -import { useTranslations } from "next-intl"; -import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import TokenHandler from "@/components/TokenHandler"; export default function AuthCallbackPage() { - const t = useTranslations("auth"); - + // Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI + // TokenHandler uses useGlobalLoadingEffect to show the loading screen return ( - }> + - ); + isAutoConfiguring); + + // Use global loading screen - spinner animation won't reset + useGlobalLoadingEffect( + shouldShowLoading, + isAutoConfiguring ? t("setting_up_ai") : t("checking_llm_prefs"), + "default" + ); + + if (shouldShowLoading) { + return null; } if (error && !hasCheckedOnboarding && !isOnboardingPage) { diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index 8ffef229c..6a548ea51 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; -import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; interface DashboardLayoutProps { @@ -13,6 +13,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { const t = useTranslations("dashboard"); const [isCheckingAuth, setIsCheckingAuth] = useState(true); + // Use the global loading screen - spinner animation won't reset + useGlobalLoadingEffect(isCheckingAuth, t("checking_auth"), "default"); + useEffect(() => { // Check if user is authenticated const token = getBearerToken(); @@ -24,9 +27,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { setIsCheckingAuth(false); }, []); - // Show loading screen while checking authentication + // Return null while loading - the global provider handles the loading UI if (isCheckingAuth) { - return ; + return null; } return ( diff --git a/surfsense_web/app/dashboard/loading.tsx b/surfsense_web/app/dashboard/loading.tsx index b18c5dd75..2eee93651 100644 --- a/surfsense_web/app/dashboard/loading.tsx +++ b/surfsense_web/app/dashboard/loading.tsx @@ -1,21 +1,14 @@ "use client"; import { useTranslations } from "next-intl"; -import { Spinner } from "@/components/ui/spinner"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; export default function DashboardLoading() { const t = useTranslations("common"); - return ( -
-
-
- -
- - {t("loading")} - -
-
- ); -} + // Use global loading - spinner animation won't reset when page transitions + useGlobalLoadingEffect(true, t("loading"), "default"); + + // Return null - the GlobalLoadingProvider handles the loading UI + return null; +} diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index 440f37123..504d172c3 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -18,7 +18,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; function ErrorScreen({ message }: { message: string }) { const t = useTranslations("dashboard"); @@ -102,11 +102,16 @@ export default function DashboardPage() { } }, [isLoading, searchSpaces, router]); - if (isLoading) return ; + // Show loading while fetching or while we have spaces and are about to redirect + const shouldShowLoading = isLoading || searchSpaces.length > 0; + + // Use global loading screen - spinner animation won't reset + useGlobalLoadingEffect(shouldShowLoading, t("fetching_spaces"), "default"); + if (error) return ; - if (searchSpaces.length > 0) { - return ; + if (shouldShowLoading) { + return null; } return ( diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 2f9921629..3722dc26e 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -3,6 +3,7 @@ import "./globals.css"; import { RootProvider } from "fumadocs-ui/provider/next"; import { Roboto } from "next/font/google"; import { ElectricProvider } from "@/components/providers/ElectricProvider"; +import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider"; import { I18nProvider } from "@/components/providers/I18nProvider"; import { PostHogProvider } from "@/components/providers/PostHogProvider"; import { ThemeProvider } from "@/components/theme/theme-provider"; @@ -104,7 +105,9 @@ export default function RootLayout({ > - {children} + + {children} + diff --git a/surfsense_web/atoms/ui/loading.atoms.ts b/surfsense_web/atoms/ui/loading.atoms.ts new file mode 100644 index 000000000..6c6cac1a2 --- /dev/null +++ b/surfsense_web/atoms/ui/loading.atoms.ts @@ -0,0 +1,31 @@ +import { atom } from "jotai"; + +interface GlobalLoadingState { + isLoading: boolean; + message?: string; + variant: "login" | "default"; +} + +export const globalLoadingAtom = atom({ + isLoading: false, + message: undefined, + variant: "default", +}); + +// Helper atom for showing global loading +export const showGlobalLoadingAtom = atom( + null, + ( + get, + set, + { message, variant = "default" }: { message?: string; variant?: "login" | "default" } + ) => { + set(globalLoadingAtom, { isLoading: true, message, variant }); + } +); + +// Helper atom for hiding global loading +export const hideGlobalLoadingAtom = atom(null, (get, set) => { + set(globalLoadingAtom, { isLoading: false, message: undefined, variant: "default" }); +}); + diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx index 3f98451ef..35408c1b2 100644 --- a/surfsense_web/components/TokenHandler.tsx +++ b/surfsense_web/components/TokenHandler.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { useEffect } from "react"; -import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils"; import { trackLoginSuccess } from "@/lib/posthog/events"; @@ -30,6 +30,9 @@ const TokenHandler = ({ const t = useTranslations("auth"); const searchParams = useSearchParams(); + // Always show loading for this component - spinner animation won't reset + useGlobalLoadingEffect(true, t("processing_authentication"), "default"); + useEffect(() => { // Only run on client-side if (typeof window === "undefined") return; @@ -69,9 +72,8 @@ const TokenHandler = ({ } }, [searchParams, tokenParamName, storageKey, redirectPath]); - return ( - - ); + // Return null - the global provider handles the loading UI + return null; }; export default TokenHandler; diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx index 68263b036..07d736c64 100644 --- a/surfsense_web/components/providers/ElectricProvider.tsx +++ b/surfsense_web/components/providers/ElectricProvider.tsx @@ -4,7 +4,7 @@ import { useAtomValue } from "jotai"; import { useTranslations } from "next-intl"; import { useEffect, useRef, useState } from "react"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { getBearerToken } from "@/lib/auth-utils"; import { cleanupElectric, @@ -113,19 +113,21 @@ export function ElectricProvider({ children }: ElectricProviderProps) { // This prevents showing loading screen for unauthenticated users on homepage const hasToken = typeof window !== "undefined" && !!getBearerToken(); + // Determine if we should show loading + const shouldShowLoading = hasToken && isUserLoaded && !!user?.id && !electricClient && !error; + + // Use global loading hook with ownership tracking - prevents flash during transitions + useGlobalLoadingEffect(shouldShowLoading, t("initializing"), "default"); + // For non-authenticated pages (like landing page), render immediately with null context // Also render immediately if user query failed (e.g., token expired) if (!hasToken || !isUserLoaded || !user?.id || isUserError) { return {children}; } - // Show loading state while initializing for authenticated users + // Return children with null context while initializing - the global provider handles the loading UI if (!electricClient && !error) { - return ( - - - - ); + return {children}; } // If there's an error, still render but warn diff --git a/surfsense_web/components/providers/GlobalLoadingProvider.tsx b/surfsense_web/components/providers/GlobalLoadingProvider.tsx new file mode 100644 index 000000000..fe816b316 --- /dev/null +++ b/surfsense_web/components/providers/GlobalLoadingProvider.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useAtomValue } from "jotai"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { globalLoadingAtom } from "@/atoms/ui/loading.atoms"; +import { Logo } from "@/components/Logo"; +import { Spinner } from "@/components/ui/spinner"; +import { AmbientBackground } from "@/app/(home)/login/AmbientBackground"; +import { cn } from "@/lib/utils"; + +/** + * GlobalLoadingProvider renders a persistent loading overlay. + * The spinner is ALWAYS in the DOM to prevent animation reset when + * loading states change between different pages/components. + * + * Visibility is controlled via CSS opacity/pointer-events, NOT mounting/unmounting. + */ +export function GlobalLoadingProvider({ children }: { children: React.ReactNode }) { + const [mounted, setMounted] = useState(false); + const { isLoading, message, variant } = useAtomValue(globalLoadingAtom); + + useEffect(() => { + setMounted(true); + }, []); + + // The overlay is ALWAYS rendered, but visibility is controlled by CSS + // This prevents the spinner animation from resetting + const loadingOverlay = ( +
+ {variant === "login" ? ( +
+ +
+ +
+
+ {/* Spinner is always mounted, animation never resets */} + +
+ + {message} + +
+
+
+ ) : ( +
+
+
+ {/* Spinner is always mounted, animation never resets */} + +
+ + {message} + +
+
+ )} +
+ ); + + // Render inline during SSR/before hydration, use portal after mounting + // This prevents the white flash during initial render + return ( + <> + {children} + {mounted ? createPortal(loadingOverlay, document.body) : loadingOverlay} + + ); +} + diff --git a/surfsense_web/components/ui/unified-loading-screen.tsx b/surfsense_web/components/ui/unified-loading-screen.tsx deleted file mode 100644 index 7ed6b83d9..000000000 --- a/surfsense_web/components/ui/unified-loading-screen.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; -import { Logo } from "@/components/Logo"; -import { Spinner } from "@/components/ui/spinner"; -import { AmbientBackground } from "@/app/(home)/login/AmbientBackground"; - -interface UnifiedLoadingScreenProps { - /** Optional message to display below the spinner */ - message?: string; - /** Visual style variant */ - variant?: "login" | "default"; -} - -export function UnifiedLoadingScreen({ - message, - variant = "default", -}: UnifiedLoadingScreenProps) { - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - // Fixed-size container to prevent layout shifts - const spinnerContainer = ( -
- -
- ); - - const content = variant === "login" ? ( -
- -
- -
- {spinnerContainer} - {message && ( - - {message} - - )} -
-
-
- ) : ( -
-
- {spinnerContainer} - {message && ( - - {message} - - )} -
-
- ); - - // Render inline during SSR, use portal after mounting - // This prevents the black flash during initial render - if (!mounted) { - return content; - } - - return createPortal(content, document.body); -} - diff --git a/surfsense_web/hooks/use-global-loading.ts b/surfsense_web/hooks/use-global-loading.ts new file mode 100644 index 000000000..baaa1f089 --- /dev/null +++ b/surfsense_web/hooks/use-global-loading.ts @@ -0,0 +1,104 @@ +"use client"; + +import { useAtom } from "jotai"; +import { useCallback, useEffect, useRef } from "react"; +import { globalLoadingAtom } from "@/atoms/ui/loading.atoms"; + +// Global counter to generate unique IDs for each loading request +let loadingIdCounter = 0; + +// Track the current active loading ID globally +let currentLoadingId: number | null = null; + +// Pending hide timeout - allows new loading requests to take over before hiding +let pendingHideTimeout: ReturnType | null = null; + +/** + * Hook to control the global loading screen. + * The spinner is always mounted in the DOM to prevent animation reset. + */ +export function useGlobalLoading() { + const [loading, setLoading] = useAtom(globalLoadingAtom); + + const show = useCallback( + (message?: string, variant: "login" | "default" = "default") => { + // Cancel any pending hide - new loading request takes over + if (pendingHideTimeout) { + clearTimeout(pendingHideTimeout); + pendingHideTimeout = null; + } + + const id = ++loadingIdCounter; + currentLoadingId = id; + setLoading({ isLoading: true, message, variant }); + return id; + }, + [setLoading] + ); + + const hide = useCallback( + (id?: number) => { + // Only hide if this is the current loading, or if no ID provided (force hide) + if (id === undefined || id === currentLoadingId) { + // Use a small delay to allow React to flush pending mounts + // This prevents flash when transitioning between loading states + if (pendingHideTimeout) { + clearTimeout(pendingHideTimeout); + } + + pendingHideTimeout = setTimeout(() => { + // Double-check we're still the current loading after the delay + if (id === undefined || id === currentLoadingId) { + currentLoadingId = null; + setLoading({ isLoading: false, message: undefined, variant: "default" }); + } + pendingHideTimeout = null; + }, 50); // Small delay to allow next component to mount and show loading + } + }, + [setLoading] + ); + + return { show, hide, isLoading: loading.isLoading }; +} + +/** + * Hook that automatically shows/hides the global loading screen based on a condition. + * Useful for components that show loading on mount and hide on unmount. + * + * Uses ownership tracking to prevent flashes when multiple components + * transition loading states (e.g., layout → page). + * + * @param shouldShow - Whether the loading screen should be visible + * @param message - Optional message to display + * @param variant - Visual style variant ("login" or "default") + */ +export function useGlobalLoadingEffect( + shouldShow: boolean, + message?: string, + variant: "login" | "default" = "default" +) { + const { show, hide } = useGlobalLoading(); + const loadingIdRef = useRef(null); + + useEffect(() => { + if (shouldShow) { + // Show loading and store the ID + loadingIdRef.current = show(message, variant); + } else if (loadingIdRef.current !== null) { + // Only hide if we were the ones showing loading + hide(loadingIdRef.current); + loadingIdRef.current = null; + } + }, [shouldShow, message, variant, show, hide]); + + // Cleanup on unmount - only hide if we're still the active loading + useEffect(() => { + return () => { + if (loadingIdRef.current !== null) { + hide(loadingIdRef.current); + loadingIdRef.current = null; + } + }; + }, [hide]); +} From 03e38ab17898b6001ce902c9ad771ff684496e46 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:16:09 +0530 Subject: [PATCH 11/12] chore: ran frontend linting --- .../[search_space_id]/client-layout.tsx | 4 +- surfsense_web/atoms/ui/loading.atoms.ts | 1 - .../components/bookstack-connect-form.tsx | 1 - surfsense_web/components/homepage/navbar.tsx | 8 ++- .../ui/dialogs/CreateSearchSpaceDialog.tsx | 19 ++++++- .../layout/ui/sidebar/ChatListItem.tsx | 9 ++- .../layout/ui/sidebar/SidebarUserProfile.tsx | 56 ++++++++----------- .../new-chat/source-detail-panel.tsx | 16 ++---- .../providers/GlobalLoadingProvider.tsx | 5 +- .../settings/general-settings-manager.tsx | 9 +-- .../components/settings/llm-role-manager.tsx | 10 +--- 11 files changed, 68 insertions(+), 70 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index b2af3a28a..e6730d8d1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -148,10 +148,10 @@ export function DashboardClientLayout({ // Determine if we should show loading const shouldShowLoading = - ((!hasCheckedOnboarding && + (!hasCheckedOnboarding && (loading || accessLoading || globalConfigsLoading) && !isOnboardingPage) || - isAutoConfiguring); + isAutoConfiguring; // Use global loading screen - spinner animation won't reset useGlobalLoadingEffect( diff --git a/surfsense_web/atoms/ui/loading.atoms.ts b/surfsense_web/atoms/ui/loading.atoms.ts index 6c6cac1a2..f10d9247b 100644 --- a/surfsense_web/atoms/ui/loading.atoms.ts +++ b/surfsense_web/atoms/ui/loading.atoms.ts @@ -28,4 +28,3 @@ export const showGlobalLoadingAtom = atom( export const hideGlobalLoadingAtom = atom(null, (get, set) => { set(globalLoadingAtom, { isLoading: false, message: undefined, variant: "default" }); }); - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx index 789e23787..65edb095c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx @@ -296,7 +296,6 @@ export const BookStackConnectForm: FC = ({ onSubmit, isSubmitt
)} - ); }; diff --git a/surfsense_web/components/homepage/navbar.tsx b/surfsense_web/components/homepage/navbar.tsx index 2a8820bd6..c83d3556a 100644 --- a/surfsense_web/components/homepage/navbar.tsx +++ b/surfsense_web/components/homepage/navbar.tsx @@ -1,5 +1,11 @@ "use client"; -import { IconBrandDiscord, IconBrandGithub, IconBrandReddit, IconMenu2, IconX } from "@tabler/icons-react"; +import { + IconBrandDiscord, + IconBrandGithub, + IconBrandReddit, + IconMenu2, + IconX, +} from "@tabler/icons-react"; import { AnimatePresence, motion } from "motion/react"; import Link from "next/link"; import { useEffect, useState } from "react"; diff --git a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx index c0f92b440..63a430004 100644 --- a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx +++ b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx @@ -107,7 +107,12 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac {t("name_label")} - + @@ -126,7 +131,11 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac - + @@ -143,7 +152,11 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac > {tCommon("cancel")} - diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx index 243ad4c60..f8170ff63 100644 --- a/surfsense_web/app/(home)/register/page.tsx +++ b/surfsense_web/app/(home)/register/page.tsx @@ -18,6 +18,7 @@ import { trackRegistrationSuccess, } from "@/lib/posthog/events"; import { AmbientBackground } from "../login/AmbientBackground"; +import { Spinner } from "@/components/ui/spinner"; export default function RegisterPage() { const t = useTranslations("auth"); @@ -60,9 +61,6 @@ export default function RegisterPage() { // Track registration attempt trackRegistrationAttempt(); - // Show loading toast - const loadingToast = toast.loading(t("creating_account")); - try { await register({ email, @@ -77,7 +75,6 @@ export default function RegisterPage() { // Success toast toast.success(t("register_success"), { - id: loadingToast, description: t("redirecting_login"), duration: 2000, }); @@ -95,7 +92,6 @@ export default function RegisterPage() { trackRegistrationFailure("Registration disabled"); setError({ title: "Registration is disabled", message: friendlyMessage }); toast.error("Registration is disabled", { - id: loadingToast, description: friendlyMessage, duration: 6000, }); @@ -109,7 +105,6 @@ export default function RegisterPage() { trackRegistrationFailure(err.message); setError({ title: err.name, message: err.message }); toast.error(err.name, { - id: loadingToast, description: err.message, duration: 6000, }); @@ -137,7 +132,6 @@ export default function RegisterPage() { // Show error toast with conditional retry action const toastOptions: any = { - id: loadingToast, description: errorDetails.description, duration: 6000, }; @@ -295,9 +289,16 @@ export default function RegisterPage() { diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx index 0ff61ac28..7ccdd850c 100644 --- a/surfsense_web/components/homepage/hero-section.tsx +++ b/surfsense_web/components/homepage/hero-section.tsx @@ -184,7 +184,7 @@ function GetStartedButton() { return ( Get Started diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index f23359bf2..f14c73ddc 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -46,6 +46,7 @@ "hide_password": "Hide password", "remember_me": "Remember Me", "sign_in": "Sign In", + "signing_in": "Signing in", "sign_up": "Sign Up", "sign_in_with": "Sign in with {provider}", "dont_have_account": "Don't have an account?", diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index 3c4b6cf34..6838b0f52 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -46,6 +46,7 @@ "hide_password": "隐藏密码", "remember_me": "记住我", "sign_in": "登录", + "signing_in": "正在登录", "sign_up": "注册", "sign_in_with": "使用 {provider} 登录", "dont_have_account": "还没有账户?",