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) ) 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/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx index 62d2a2a66..bd9c2c1b4 100644 --- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx +++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx @@ -12,6 +12,7 @@ import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-err import { AUTH_TYPE } from "@/lib/env-config"; import { ValidationError } from "@/lib/error"; import { trackLoginAttempt, trackLoginFailure, trackLoginSuccess } from "@/lib/posthog/events"; +import { Spinner } from "@/components/ui/spinner"; export function LocalLoginForm() { const t = useTranslations("auth"); @@ -42,9 +43,6 @@ export function LocalLoginForm() { // Track login attempt trackLoginAttempt("local"); - // Show loading toast - const loadingToast = toast.loading(tCommon("loading")); - try { const data = await login({ username, @@ -62,8 +60,7 @@ export function LocalLoginForm() { // Success toast toast.success(t("login_success"), { - id: loadingToast, - description: "Redirecting to dashboard...", + description: "Redirecting to dashboard", duration: 2000, }); @@ -76,7 +73,6 @@ export function LocalLoginForm() { trackLoginFailure("local", err.message); setError({ title: err.name, message: err.message }); toast.error(err.name, { - id: loadingToast, description: err.message, duration: 6000, }); @@ -106,7 +102,6 @@ export function LocalLoginForm() { // Show error toast with conditional retry action const toastOptions: any = { - id: loadingToast, description: errorDetails.description, duration: 6000, }; @@ -244,9 +239,16 @@ export function LocalLoginForm() { diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx index 7aade8427..0dc9c445f 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 { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; 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, }; @@ -88,20 +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 ( -
- -
- -
- - {tCommon("loading")} -
-
-
- ); + return null; } if (authType === "GOOGLE") { @@ -182,23 +178,10 @@ function LoginContent() { ); } -// Loading fallback for Suspense -const LoadingFallback = () => ( -
- -
- -
- - Loading... -
-
-
-); - export default function LoginPage() { + // Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI return ( - }> + ); 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/app/auth/callback/loading.tsx b/surfsense_web/app/auth/callback/loading.tsx new file mode 100644 index 000000000..0c94e1ee0 --- /dev/null +++ b/surfsense_web/app/auth/callback/loading.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; + +export default function AuthCallbackLoading() { + const t = useTranslations("auth"); + + // 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 da868c316..4050eefb6 100644 --- a/surfsense_web/app/auth/callback/page.tsx +++ b/surfsense_web/app/auth/callback/page.tsx @@ -1,23 +1,18 @@ +"use client"; + import { Suspense } from "react"; import TokenHandler from "@/components/TokenHandler"; export default function AuthCallbackPage() { + // Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI + // TokenHandler uses useGlobalLoadingEffect to show the loading screen 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..e6730d8d1 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 { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; export function DashboardClientLayout({ children, @@ -146,31 +146,22 @@ export function DashboardClientLayout({ setActiveSearchSpaceIdState(activeSeacrhSpaceId); }, [search_space_id, setActiveSearchSpaceIdState]); - if ( + // Determine if we should show loading + const shouldShowLoading = (!hasCheckedOnboarding && (loading || accessLoading || globalConfigsLoading) && !isOnboardingPage) || - isAutoConfiguring - ) { - return ( -
- - - - {isAutoConfiguring ? "Setting up AI..." : t("loading_config")} - - - {isAutoConfiguring - ? "Auto-configuring with available settings" - : t("checking_llm_prefs")} - - - - - - -
- ); + 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/[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]/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/[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]/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/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() {
); diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 293d4a243..20d76faa2 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -1,13 +1,14 @@ "use client"; import { useAtomValue } from "jotai"; -import { Cable, Loader2 } from "lucide-react"; +import { Cable } from "lucide-react"; import { useSearchParams } from "next/navigation"; import type { FC } from "react"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { useConnectorsElectric } from "@/hooks/use-connectors-electric"; @@ -174,7 +175,7 @@ export const ConnectorIndicator: FC = () => { onClick={() => handleOpenChange(true)} > {isLoading ? ( - + ) : ( <> diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx index b93e74213..4119b74cd 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/connector-card.tsx @@ -1,9 +1,10 @@ "use client"; import { IconBrandYoutube } from "@tabler/icons-react"; -import { FileText, Loader2 } from "lucide-react"; +import { FileText } from "lucide-react"; import type { FC } from "react"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { cn } from "@/lib/utils"; @@ -111,7 +112,7 @@ export const ConnectorCard: FC = ({ {isIndexing ? (

- + Syncing

) : isConnected ? ( @@ -151,7 +152,7 @@ export const ConnectorCard: FC = ({ disabled={isConnecting || !isEnabled} > {isConnecting ? ( - + ) : !isEnabled ? ( "Unavailable" ) : isConnected ? ( 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..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 @@ -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, @@ -301,124 +296,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/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index 49d1ebacc..ec754da2e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -1,8 +1,9 @@ "use client"; -import { ArrowLeft, Loader2 } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; import { type FC, useMemo } from "react"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import type { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorTypeDisplay } from "@/lib/connectors/utils"; @@ -139,7 +140,7 @@ export const ConnectorConnectView: FC = ({ > {isSubmitting ? ( <> - + Connecting ) : connectorType === "MCP_CONNECTOR" ? ( diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 5668d398e..0c4f65cc9 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -1,8 +1,9 @@ "use client"; -import { ArrowLeft, Info, Loader2, RefreshCw, Trash2 } from "lucide-react"; +import { ArrowLeft, Info, RefreshCw, Trash2 } from "lucide-react"; import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { cn } from "@/lib/utils"; @@ -311,7 +312,7 @@ export const ConnectorEditView: FC = ({ > {isDisconnecting ? ( <> - + Disconnecting ) : ( @@ -347,7 +348,7 @@ export const ConnectorEditView: FC = ({ > {isSaving ? ( <> - + Saving ) : ( diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx index 684f03252..35815b0b7 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx @@ -1,9 +1,10 @@ "use client"; -import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; +import { ArrowLeft, Check, Info } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { getConnectorTypeDisplay } from "@/lib/connectors/utils"; import { cn } from "@/lib/utils"; @@ -216,7 +217,7 @@ export const IndexingConfigurationView: FC = ({ > {isStartingIndexing ? ( <> - + Starting... ) : ( diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index 2067ca9ad..639dab2aa 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -1,11 +1,12 @@ "use client"; -import { ArrowRight, Cable, Loader2 } from "lucide-react"; +import { ArrowRight, Cable } from "lucide-react"; import { useRouter } from "next/navigation"; import type { FC } from "react"; import { useState } from "react"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { TabsContent } from "@/components/ui/tabs"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; @@ -209,7 +210,7 @@ export const ActiveConnectorsTab: FC = ({

{title}

{isAnyIndexing ? (

- + Syncing

) : ( @@ -270,7 +271,7 @@ export const ActiveConnectorsTab: FC = ({ {isIndexing ? (

- + Syncing

) : !isMCPConnector ? ( diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx index a48ca02e6..bf151ab43 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -1,9 +1,10 @@ "use client"; import { differenceInDays, differenceInMinutes, format, isToday, isYesterday } from "date-fns"; -import { ArrowLeft, Loader2, Plus, Server } from "lucide-react"; +import { ArrowLeft, Plus, Server } from "lucide-react"; import type { FC } from "react"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; @@ -143,7 +144,7 @@ export const ConnectorAccountsListView: FC = ({ >
{isConnecting ? ( - + ) : ( )} @@ -207,7 +208,7 @@ export const ConnectorAccountsListView: FC = ({

{isIndexing ? (

- + Syncing

) : ( diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx index d8353f284..82ee49bd7 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx @@ -2,13 +2,14 @@ import { TagInput, type Tag as TagType } from "emblor"; import { useAtom } from "jotai"; -import { ArrowLeft, Loader2 } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { type FC, useState } from "react"; import { toast } from "sonner"; import { createDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import { Label } from "@/components/ui/label"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; @@ -222,7 +223,7 @@ export const YouTubeCrawlerView: FC = ({ searchSpaceId, > {isSubmitting ? ( <> - + {t("processing")} ) : ( diff --git a/surfsense_web/components/chat-comments/comment-item/comment-actions.tsx b/surfsense_web/components/chat-comments/comment-item/comment-actions.tsx index d31c44182..3e30fb185 100644 --- a/surfsense_web/components/chat-comments/comment-item/comment-actions.tsx +++ b/surfsense_web/components/chat-comments/comment-item/comment-actions.tsx @@ -6,6 +6,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import type { CommentActionsProps } from "./types"; @@ -23,7 +24,7 @@ export function CommentActions({ canEdit, canDelete, onEdit, onDelete }: Comment size="icon" className="size-7 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity" > - + @@ -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/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/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/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 732b3099c..702014050 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, useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; import { currentThreadAtom, resetCurrentThreadAtom } from "@/atoms/chat/current-thread.atom"; import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; @@ -22,7 +23,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"; @@ -57,6 +58,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(); @@ -171,6 +173,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: @@ -333,6 +336,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]); @@ -420,6 +445,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/dialogs/CreateSearchSpaceDialog.tsx b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx index 7e962536f..63a430004 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({ @@ -82,29 +83,36 @@ 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 +124,47 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac name="description" render={({ field }) => ( - + {t("description_label")}{" "} ({tCommon("optional")}) - + )} /> - + - - { - 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/NavSection.tsx b/surfsense_web/components/layout/ui/sidebar/NavSection.tsx index 742a27bbc..8bf83ac11 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 ( @@ -32,8 +34,7 @@ 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} > @@ -62,8 +63,7 @@ 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} > 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")} diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index 982d88e8b..7c96b1dcb 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,12 @@ 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 +227,12 @@ 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 && } ); })} @@ -313,11 +315,12 @@ 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 && } ); })} @@ -342,11 +345,12 @@ 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 && } ); })} diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index bc7591c51..515fe9f32 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -8,7 +8,6 @@ import { Cloud, Edit3, Globe, - Loader2, Plus, Settings2, Sparkles, @@ -36,6 +35,7 @@ import { CommandSeparator, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Spinner } from "@/components/ui/spinner"; import type { GlobalNewLLMConfig, NewLLMConfigPublic, @@ -179,7 +179,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp > {isLoading ? ( <> - + Loading ) : currentConfig ? ( diff --git a/surfsense_web/components/new-chat/source-detail-panel.tsx b/surfsense_web/components/new-chat/source-detail-panel.tsx index 08cff8380..087d9b775 100644 --- a/surfsense_web/components/new-chat/source-detail-panel.tsx +++ b/surfsense_web/components/new-chat/source-detail-panel.tsx @@ -1,17 +1,9 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { - BookOpen, - ChevronDown, - ExternalLink, - FileText, - Hash, - Loader2, - Sparkles, - X, -} from "lucide-react"; +import { BookOpen, ChevronDown, ExternalLink, FileText, Hash, 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 +12,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 +56,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 +376,10 @@ 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/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); diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx index e31885973..07d736c64 100644 --- a/surfsense_web/components/providers/ElectricProvider.tsx +++ b/surfsense_web/components/providers/ElectricProvider.tsx @@ -1,8 +1,11 @@ "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 { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; +import { getBearerToken } from "@/lib/auth-utils"; import { cleanupElectric, type ElectricClient, @@ -27,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 { @@ -105,21 +109,25 @@ 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(); + + // 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 (!isUserLoaded || !user?.id || isUserError) { + 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 ( - -
-
Initializing...
-
-
- ); + 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..e72f2b876 --- /dev/null +++ b/surfsense_web/components/providers/GlobalLoadingProvider.tsx @@ -0,0 +1,79 @@ +"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/settings/general-settings-manager.tsx b/surfsense_web/components/settings/general-settings-manager.tsx new file mode 100644 index 000000000..b2f93402a --- /dev/null +++ b/surfsense_web/components/settings/general-settings-manager.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useAtomValue } from "jotai"; +import { useQuery } from "@tanstack/react-query"; +import { Info, RotateCcw, Save } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { updateSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; + +interface GeneralSettingsManagerProps { + searchSpaceId: number; +} + +export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManagerProps) { + const t = useTranslations("searchSpaceSettings"); + const tCommon = useTranslations("common"); + const { + data: searchSpace, + isLoading: loading, + refetch: fetchSearchSpace, + } = useQuery({ + queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()), + queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }), + enabled: !!searchSpaceId, + }); + + const { mutateAsync: updateSearchSpace } = useAtomValue(updateSearchSpaceMutationAtom); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [saving, setSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + + // Initialize state from fetched search space + useEffect(() => { + if (searchSpace) { + setName(searchSpace.name || ""); + setDescription(searchSpace.description || ""); + setHasChanges(false); + } + }, [searchSpace]); + + // Track changes + useEffect(() => { + if (searchSpace) { + const currentName = searchSpace.name || ""; + const currentDescription = searchSpace.description || ""; + const changed = currentName !== name || currentDescription !== description; + setHasChanges(changed); + } + }, [searchSpace, name, description]); + + const handleSave = async () => { + try { + setSaving(true); + + await updateSearchSpace({ + id: searchSpaceId, + data: { + name: name.trim(), + description: description.trim() || undefined, + }, + }); + + setHasChanges(false); + await fetchSearchSpace(); + } catch (error: any) { + console.error("Error saving search space details:", error); + toast.error(error.message || "Failed to save search space details"); + } finally { + setSaving(false); + } + }; + + const handleReset = () => { + if (searchSpace) { + setName(searchSpace.name || ""); + setDescription(searchSpace.description || ""); + setHasChanges(false); + } + }; + + if (loading) { + return ( +
+ + + + + + + + + + +
+ ); + } + + return ( +
+ + + + Update your search space name and description. These details help identify and organize + your workspace. + + + + {/* Search Space Details Card */} + + + Search Space Details + + Manage the basic information for this search space. + + + +
+ + setName(e.target.value)} + className="text-sm md:text-base h-9 md:h-10" + /> +

+ {t("general_name_description")} +

+
+ +
+ + setDescription(e.target.value)} + className="text-sm md:text-base h-9 md:h-10" + /> +

+ {t("general_description_description")} +

+
+
+
+ + {/* Action Buttons */} +
+ + +
+ + {hasChanges && ( + + + + {t("general_unsaved_changes")} + + + )} +
+ ); +} diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx index c41a2d3bf..649507d77 100644 --- a/surfsense_web/components/settings/llm-role-manager.tsx +++ b/surfsense_web/components/settings/llm-role-manager.tsx @@ -1,16 +1,7 @@ "use client"; import { useAtomValue } from "jotai"; -import { - AlertCircle, - Bot, - CheckCircle, - FileText, - Loader2, - RefreshCw, - RotateCcw, - Save, -} from "lucide-react"; +import { AlertCircle, Bot, CheckCircle, FileText, RefreshCw, RotateCcw, Save } from "lucide-react"; import { motion } from "motion/react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -32,6 +23,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Spinner } from "@/components/ui/spinner"; const ROLE_DESCRIPTIONS = { agent: { @@ -206,7 +198,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
- + {configsLoading && preferencesLoading ? "Loading configurations and preferences..." diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index 95230930c..c2769d403 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -7,7 +7,6 @@ import { Clock, Edit3, FileText, - Loader2, MessageSquareQuote, Plus, RefreshCw, @@ -49,6 +48,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Spinner } from "@/components/ui/spinner"; import type { NewLLMConfig } from "@/contracts/types/new-llm-config.types"; import { cn } from "@/lib/utils"; @@ -211,7 +211,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
- + Loading configurations... @@ -484,7 +484,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { > {isDeleting ? ( <> - + Deleting ) : ( diff --git a/surfsense_web/components/shared/llm-config-form.tsx b/surfsense_web/components/shared/llm-config-form.tsx index 5ffff1ab7..1a23eced6 100644 --- a/surfsense_web/components/shared/llm-config-form.tsx +++ b/surfsense_web/components/shared/llm-config-form.tsx @@ -8,7 +8,6 @@ import { ChevronDown, ChevronsUpDown, Key, - Loader2, MessageSquareQuote, Rocket, Sparkles, @@ -48,6 +47,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; +import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { getModelsByProvider } from "@/contracts/enums/llm-models"; @@ -592,7 +592,7 @@ export function LLMConfigForm({ > {isSubmitting ? ( <> - + {mode === "edit" ? "Updating..." : "Creating"} ) : ( diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index bb7b0e40d..a0ccb7c5f 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -1,13 +1,14 @@ "use client"; import { useAtom } from "jotai"; -import { CheckCircle2, FileType, Info, Loader2, Tag, Upload, X } from "lucide-react"; +import { CheckCircle2, FileType, Info, Tag, Upload, X } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useTranslations } from "next-intl"; import { useCallback, useMemo, useRef, useState } from "react"; import { useDropzone } from "react-dropzone"; import { toast } from "sonner"; import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; +import { Spinner } from "@/components/ui/spinner"; import { Accordion, AccordionContent, @@ -424,7 +425,7 @@ export function DocumentUploadTab({ > {isUploading ? ( - + {t("uploading")} ) : ( diff --git a/surfsense_web/components/tool-ui/generate-podcast.tsx b/surfsense_web/components/tool-ui/generate-podcast.tsx index f9855a007..c76d7ce5a 100644 --- a/surfsense_web/components/tool-ui/generate-podcast.tsx +++ b/surfsense_web/components/tool-ui/generate-podcast.tsx @@ -1,10 +1,11 @@ "use client"; import { makeAssistantToolUI } from "@assistant-ui/react"; -import { AlertCircleIcon, Loader2Icon, MicIcon } from "lucide-react"; +import { AlertCircleIcon, MicIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { z } from "zod"; import { Audio } from "@/components/tool-ui/audio"; +import { Spinner } from "@/components/ui/spinner"; import { baseApiService } from "@/lib/apis/base-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state"; @@ -97,8 +98,8 @@ function PodcastGeneratingState({ title }: { title: string }) {

{title}

- - Generating podcast. This may take a few minutes + + Generating podcast. This may take a few minutes.
@@ -144,7 +145,7 @@ function AudioLoadingState({ title }: { title: string }) {

{title}

- + Loading audio...
diff --git a/surfsense_web/components/tool-ui/image/index.tsx b/surfsense_web/components/tool-ui/image/index.tsx index f872e293f..42725d258 100644 --- a/surfsense_web/components/tool-ui/image/index.tsx +++ b/surfsense_web/components/tool-ui/image/index.tsx @@ -1,11 +1,12 @@ "use client"; -import { ExternalLinkIcon, ImageIcon, Loader2 } from "lucide-react"; +import { ExternalLinkIcon, ImageIcon } from "lucide-react"; import NextImage from "next/image"; import { Component, type ReactNode, useState } from "react"; import { z } from "zod"; import { Badge } from "@/components/ui/badge"; import { Card } from "@/components/ui/card"; +import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; /** @@ -184,7 +185,7 @@ export function ImageLoading({ title = "Loading image..." }: { title?: string })
- +

{title}

diff --git a/surfsense_web/components/tool-ui/media-card/index.tsx b/surfsense_web/components/tool-ui/media-card/index.tsx index d4fe0c7c0..c7c8cfdf2 100644 --- a/surfsense_web/components/tool-ui/media-card/index.tsx +++ b/surfsense_web/components/tool-ui/media-card/index.tsx @@ -1,12 +1,13 @@ "use client"; -import { ExternalLinkIcon, Globe, ImageIcon, LinkIcon, Loader2 } from "lucide-react"; +import { ExternalLinkIcon, Globe, ImageIcon, LinkIcon } from "lucide-react"; import Image from "next/image"; import { Component, type ReactNode } from "react"; import { z } from "zod"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; @@ -299,18 +300,17 @@ export function MediaCard({ {/* Response Actions */} {responseActions && responseActions.length > 0 && ( -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > +
{responseActions.map((action) => ( @@ -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/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} 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) 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]); +} diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 8ca382669..f14c73ddc 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", @@ -45,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?", @@ -76,9 +78,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 +146,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 +171,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", @@ -316,10 +322,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", @@ -386,8 +392,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", @@ -723,12 +729,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..6838b0f52 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": "删除", @@ -45,6 +46,7 @@ "hide_password": "隐藏密码", "remember_me": "记住我", "sign_in": "登录", + "signing_in": "正在登录", "sign_up": "注册", "sign_in_with": "使用 {provider} 登录", "dont_have_account": "还没有账户?", @@ -76,9 +78,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 +131,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 +156,7 @@ "create_search_space": "创建搜索空间", "add_new_search_space": "添加新的搜索空间", "loading": "加载中", - "fetching_spaces": "正在获取您的搜索空间...", + "fetching_spaces": "正在获取您的搜索空间", "may_take_moment": "这可能需要一些时间", "error": "错误", "something_wrong": "出现错误", @@ -708,12 +714,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 工作空间",