diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx
index 7aade8427..0026fdcd0 100644
--- a/surfsense_web/app/(home)/login/page.tsx
+++ b/surfsense_web/app/(home)/login/page.tsx
@@ -1,12 +1,12 @@
"use client";
-import { Loader2 } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner";
import { Logo } from "@/components/Logo";
+import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors";
import { AUTH_TYPE } from "@/lib/env-config";
import { AmbientBackground } from "./AmbientBackground";
@@ -59,7 +59,11 @@ function LoginContent() {
});
// Show toast with conditional retry action
- const toastOptions: any = {
+ const toastOptions: {
+ description: string;
+ duration: number;
+ action?: { label: string; onClick: () => void };
+ } = {
description: errorDescription,
duration: 6000,
};
@@ -90,18 +94,7 @@ function LoginContent() {
// Show loading state while determining auth type
if (isLoading) {
- return (
-
-
Loading chat...
+
);
}
diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx
index 71cd6275f..8ffef229c 100644
--- a/surfsense_web/app/dashboard/layout.tsx
+++ b/surfsense_web/app/dashboard/layout.tsx
@@ -1,8 +1,8 @@
"use client";
-import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { useTranslations } from "next-intl";
+import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
interface DashboardLayoutProps {
@@ -10,6 +10,7 @@ interface DashboardLayoutProps {
}
export default function DashboardLayout({ children }: DashboardLayoutProps) {
+ const t = useTranslations("dashboard");
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
useEffect(() => {
@@ -25,19 +26,7 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
// Show loading screen while checking authentication
if (isCheckingAuth) {
- return (
-
-
-
- Loading Dashboard
- Checking authentication...
-
-
-
-
-
-
- );
+ return
;
}
return (
diff --git a/surfsense_web/app/dashboard/loading.tsx b/surfsense_web/app/dashboard/loading.tsx
new file mode 100644
index 000000000..b18c5dd75
--- /dev/null
+++ b/surfsense_web/app/dashboard/loading.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { Spinner } from "@/components/ui/spinner";
+
+export default function DashboardLoading() {
+ const t = useTranslations("common");
+ return (
+
+
+
+
+
+
+ {t("loading")}
+
+
+
+ );
+}
+
diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx
index 767ce5201..440f37123 100644
--- a/surfsense_web/app/dashboard/page.tsx
+++ b/surfsense_web/app/dashboard/page.tsx
@@ -1,7 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
-import { AlertCircle, Loader2, Plus, Search } from "lucide-react";
+import { AlertCircle, Plus, Search } from "lucide-react";
import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
@@ -18,37 +18,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
-
-function LoadingScreen() {
- const t = useTranslations("dashboard");
- return (
-
-
-
-
- {t("loading")}
- {t("fetching_spaces")}
-
-
-
-
-
-
-
- {t("may_take_moment")}
-
-
-
-
- );
-}
+import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
function ErrorScreen({ message }: { message: string }) {
const t = useTranslations("dashboard");
@@ -121,6 +91,7 @@ export default function DashboardPage() {
const router = useRouter();
const [showCreateDialog, setShowCreateDialog] = useState(false);
+ const t = useTranslations("dashboard");
const { data: searchSpaces = [], isLoading, error } = useAtomValue(searchSpacesAtom);
useEffect(() => {
@@ -131,11 +102,11 @@ export default function DashboardPage() {
}
}, [isLoading, searchSpaces, router]);
- if (isLoading) return
;
+ if (isLoading) return
;
if (error) return
;
if (searchSpaces.length > 0) {
- return
;
+ return
;
}
return (
diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx
index b4ca36298..3f98451ef 100644
--- a/surfsense_web/components/TokenHandler.tsx
+++ b/surfsense_web/components/TokenHandler.tsx
@@ -1,7 +1,9 @@
"use client";
import { useSearchParams } from "next/navigation";
+import { useTranslations } from "next-intl";
import { useEffect } from "react";
+import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils";
import { trackLoginSuccess } from "@/lib/posthog/events";
@@ -25,6 +27,7 @@ const TokenHandler = ({
tokenParamName = "token",
storageKey = "surfsense_bearer_token",
}: TokenHandlerProps) => {
+ const t = useTranslations("auth");
const searchParams = useSearchParams();
useEffect(() => {
@@ -67,9 +70,7 @@ const TokenHandler = ({
}, [searchParams, tokenParamName, storageKey, redirectPath]);
return (
-
-
Processing authentication...
-
+
);
};
diff --git a/surfsense_web/components/new-chat/source-detail-panel.tsx b/surfsense_web/components/new-chat/source-detail-panel.tsx
index 08cff8380..1cac73a01 100644
--- a/surfsense_web/components/new-chat/source-detail-panel.tsx
+++ b/surfsense_web/components/new-chat/source-detail-panel.tsx
@@ -7,11 +7,11 @@ import {
ExternalLink,
FileText,
Hash,
- Loader2,
Sparkles,
X,
} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
+import { useTranslations } from "next-intl";
import type React from "react";
import { forwardRef, type ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
@@ -20,6 +20,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
+import { Spinner } from "@/components/ui/spinner";
import type {
GetDocumentByChunkResponse,
GetSurfsenseDocsByChunkResponse,
@@ -63,7 +64,7 @@ interface ChunkCardProps {
}
const ChunkCard = forwardRef
(
- ({ chunk, index, totalChunks, isCited, isActive, disableLayoutAnimation }, ref) => {
+ ({ chunk, index, totalChunks, isCited }, ref) => {
return (
(null);
const hasScrolledRef = useRef(false); // Use ref to avoid stale closures
const [summaryOpen, setSummaryOpen] = useState(false);
const [activeChunkIndex, setActiveChunkIndex] = useState
(null);
const [mounted, setMounted] = useState(false);
- const [hasScrolledToCited, setHasScrolledToCited] = useState(false);
+ const [_hasScrolledToCited, setHasScrolledToCited] = useState(false);
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
@@ -382,11 +384,8 @@ export function SourceDetailPanel({
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center gap-4"
>
-
- Loading document
+
+ {t("loading_document")}
)}
diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx
index f187d10c1..68263b036 100644
--- a/surfsense_web/components/providers/ElectricProvider.tsx
+++ b/surfsense_web/components/providers/ElectricProvider.tsx
@@ -1,8 +1,10 @@
"use client";
import { useAtomValue } from "jotai";
+import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
+import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
import { getBearerToken } from "@/lib/auth-utils";
import {
cleanupElectric,
@@ -28,6 +30,7 @@ interface ElectricProviderProps {
* 5. Provides client via context - hooks should use useElectricClient()
*/
export function ElectricProvider({ children }: ElectricProviderProps) {
+ const t = useTranslations("common");
const [electricClient, setElectricClient] = useState(null);
const [error, setError] = useState(null);
const {
@@ -120,9 +123,7 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
if (!electricClient && !error) {
return (
-
+
);
}
diff --git a/surfsense_web/components/ui/unified-loading-screen.tsx b/surfsense_web/components/ui/unified-loading-screen.tsx
new file mode 100644
index 000000000..7ed6b83d9
--- /dev/null
+++ b/surfsense_web/components/ui/unified-loading-screen.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { createPortal } from "react-dom";
+import { Logo } from "@/components/Logo";
+import { Spinner } from "@/components/ui/spinner";
+import { AmbientBackground } from "@/app/(home)/login/AmbientBackground";
+
+interface UnifiedLoadingScreenProps {
+ /** Optional message to display below the spinner */
+ message?: string;
+ /** Visual style variant */
+ variant?: "login" | "default";
+}
+
+export function UnifiedLoadingScreen({
+ message,
+ variant = "default",
+}: UnifiedLoadingScreenProps) {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ // Fixed-size container to prevent layout shifts
+ const spinnerContainer = (
+
+
+
+ );
+
+ const content = variant === "login" ? (
+
+
+
+
+
+ {spinnerContainer}
+ {message && (
+
+ {message}
+
+ )}
+
+
+
+ ) : (
+
+
+ {spinnerContainer}
+ {message && (
+
+ {message}
+
+ )}
+
+
+ );
+
+ // Render inline during SSR, use portal after mounting
+ // This prevents the black flash during initial render
+ if (!mounted) {
+ return content;
+ }
+
+ return createPortal(content, document.body);
+}
+
diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json
index 108d93262..59b9cf56a 100644
--- a/surfsense_web/messages/en.json
+++ b/surfsense_web/messages/en.json
@@ -2,7 +2,8 @@
"common": {
"app_name": "SurfSense",
"welcome": "Welcome",
- "loading": "Loading...",
+ "loading": "Loading",
+ "initializing": "Initializing",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
@@ -76,9 +77,10 @@
"passwords_no_match": "Passwords do not match",
"password_mismatch": "Password Mismatch",
"passwords_no_match_desc": "The passwords you entered do not match",
- "creating_account": "Creating your account...",
- "creating_account_btn": "Creating account...",
- "redirecting_login": "Redirecting to login page..."
+ "creating_account": "Creating your account",
+ "creating_account_btn": "Creating account",
+ "redirecting_login": "Redirecting to login page",
+ "processing_authentication": "Processing authentication"
},
"searchSpace": {
"create_title": "Create Search Space",
@@ -143,12 +145,15 @@
"api_keys": "API Keys",
"profile": "Profile",
"loading_dashboard": "Loading Dashboard",
- "checking_auth": "Checking authentication...",
+ "checking_auth": "Checking authentication",
"loading_config": "Loading Configuration",
- "checking_llm_prefs": "Checking your LLM preferences...",
+ "checking_llm_prefs": "Checking your LLM preferences",
+ "setting_up_ai": "Setting up AI",
"config_error": "Configuration Error",
"failed_load_llm_config": "Failed to load your LLM configuration",
"error_loading_chats": "Error loading chats",
+ "loading_chat": "Loading chat",
+ "loading_document": "Loading document",
"no_recent_chats": "No recent chats",
"error_loading_space": "Error loading search space",
"unknown_search_space": "Unknown Search Space",
@@ -165,7 +170,7 @@
"create_search_space": "Create Search Space",
"add_new_search_space": "Add New Search Space",
"loading": "Loading",
- "fetching_spaces": "Fetching your search spaces...",
+ "fetching_spaces": "Fetching your search spaces",
"may_take_moment": "This may take a moment",
"error": "Error",
"something_wrong": "Something went wrong",
diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json
index 51e378bb2..3c4b6cf34 100644
--- a/surfsense_web/messages/zh.json
+++ b/surfsense_web/messages/zh.json
@@ -3,6 +3,7 @@
"app_name": "SurfSense",
"welcome": "欢迎",
"loading": "加载中...",
+ "initializing": "正在初始化",
"save": "保存",
"cancel": "取消",
"delete": "删除",
@@ -76,9 +77,10 @@
"passwords_no_match": "密码不匹配",
"password_mismatch": "密码不匹配",
"passwords_no_match_desc": "您输入的密码不一致",
- "creating_account": "正在创建您的账户...",
- "creating_account_btn": "创建中...",
- "redirecting_login": "正在跳转到登录页面..."
+ "creating_account": "正在创建您的账户",
+ "creating_account_btn": "创建中",
+ "redirecting_login": "正在跳转到登录页面",
+ "processing_authentication": "正在处理身份验证"
},
"searchSpace": {
"create_title": "创建搜索空间",
@@ -128,12 +130,15 @@
"api_keys": "API 密钥",
"profile": "个人资料",
"loading_dashboard": "正在加载仪表盘",
- "checking_auth": "正在检查身份验证...",
+ "checking_auth": "正在检查身份验证",
"loading_config": "正在加载配置",
- "checking_llm_prefs": "正在检查您的 LLM 偏好设置...",
+ "checking_llm_prefs": "正在检查您的 LLM 偏好设置",
+ "setting_up_ai": "正在设置 AI",
"config_error": "配置错误",
"failed_load_llm_config": "无法加载您的 LLM 配置",
"error_loading_chats": "加载对话失败",
+ "loading_chat": "正在加载对话",
+ "loading_document": "正在加载文档",
"no_recent_chats": "暂无最近对话",
"error_loading_space": "加载搜索空间失败",
"unknown_search_space": "未知搜索空间",
@@ -150,7 +155,7 @@
"create_search_space": "创建搜索空间",
"add_new_search_space": "添加新的搜索空间",
"loading": "加载中",
- "fetching_spaces": "正在获取您的搜索空间...",
+ "fetching_spaces": "正在获取您的搜索空间",
"may_take_moment": "这可能需要一些时间",
"error": "错误",
"something_wrong": "出现错误",