diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx index 7aade8427..0026fdcd0 100644 --- a/surfsense_web/app/(home)/login/page.tsx +++ b/surfsense_web/app/(home)/login/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { Loader2 } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { Suspense, useEffect, useState } from "react"; import { toast } from "sonner"; import { Logo } from "@/components/Logo"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { AUTH_TYPE } from "@/lib/env-config"; import { AmbientBackground } from "./AmbientBackground"; @@ -59,7 +59,11 @@ function LoginContent() { }); // Show toast with conditional retry action - const toastOptions: any = { + const toastOptions: { + description: string; + duration: number; + action?: { label: string; onClick: () => void }; + } = { description: errorDescription, duration: 6000, }; @@ -90,18 +94,7 @@ function LoginContent() { // Show loading state while determining auth type if (isLoading) { - return ( -
- -
- -
- - {tCommon("loading")} -
-
-
- ); + return ; } if (authType === "GOOGLE") { @@ -182,23 +175,9 @@ function LoginContent() { ); } -// Loading fallback for Suspense -const LoadingFallback = () => ( -
- -
- -
- - Loading... -
-
-
-); - export default function LoginPage() { return ( - }> + }> ); diff --git a/surfsense_web/app/auth/callback/loading.tsx b/surfsense_web/app/auth/callback/loading.tsx new file mode 100644 index 000000000..24d1891b4 --- /dev/null +++ b/surfsense_web/app/auth/callback/loading.tsx @@ -0,0 +1,19 @@ +import { useTranslations } from "next-intl"; +import { Spinner } from "@/components/ui/spinner"; + +export default function AuthCallbackLoading() { + const t = useTranslations("auth"); + return ( +
+
+
+ +
+ + {t("processing_authentication")} + +
+
+ ); +} + diff --git a/surfsense_web/app/auth/callback/page.tsx b/surfsense_web/app/auth/callback/page.tsx index da868c316..920581654 100644 --- a/surfsense_web/app/auth/callback/page.tsx +++ b/surfsense_web/app/auth/callback/page.tsx @@ -1,23 +1,20 @@ +"use client"; + import { Suspense } from "react"; +import { useTranslations } from "next-intl"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import TokenHandler from "@/components/TokenHandler"; export default function AuthCallbackPage() { + const t = useTranslations("auth"); + return ( -
-

Authentication Callback

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

Processing authentication...

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

Loading document

+ +

{t("loading_document")}

)} diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx index f187d10c1..68263b036 100644 --- a/surfsense_web/components/providers/ElectricProvider.tsx +++ b/surfsense_web/components/providers/ElectricProvider.tsx @@ -1,8 +1,10 @@ "use client"; import { useAtomValue } from "jotai"; +import { useTranslations } from "next-intl"; import { useEffect, useRef, useState } from "react"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen"; import { getBearerToken } from "@/lib/auth-utils"; import { cleanupElectric, @@ -28,6 +30,7 @@ interface ElectricProviderProps { * 5. Provides client via context - hooks should use useElectricClient() */ export function ElectricProvider({ children }: ElectricProviderProps) { + const t = useTranslations("common"); const [electricClient, setElectricClient] = useState(null); const [error, setError] = useState(null); const { @@ -120,9 +123,7 @@ export function ElectricProvider({ children }: ElectricProviderProps) { if (!electricClient && !error) { return ( -
-
Initializing...
-
+
); } diff --git a/surfsense_web/components/ui/unified-loading-screen.tsx b/surfsense_web/components/ui/unified-loading-screen.tsx new file mode 100644 index 000000000..7ed6b83d9 --- /dev/null +++ b/surfsense_web/components/ui/unified-loading-screen.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { Logo } from "@/components/Logo"; +import { Spinner } from "@/components/ui/spinner"; +import { AmbientBackground } from "@/app/(home)/login/AmbientBackground"; + +interface UnifiedLoadingScreenProps { + /** Optional message to display below the spinner */ + message?: string; + /** Visual style variant */ + variant?: "login" | "default"; +} + +export function UnifiedLoadingScreen({ + message, + variant = "default", +}: UnifiedLoadingScreenProps) { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + // Fixed-size container to prevent layout shifts + const spinnerContainer = ( +
+ +
+ ); + + const content = variant === "login" ? ( +
+ +
+ +
+ {spinnerContainer} + {message && ( + + {message} + + )} +
+
+
+ ) : ( +
+
+ {spinnerContainer} + {message && ( + + {message} + + )} +
+
+ ); + + // Render inline during SSR, use portal after mounting + // This prevents the black flash during initial render + if (!mounted) { + return content; + } + + return createPortal(content, document.body); +} + diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 108d93262..59b9cf56a 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -2,7 +2,8 @@ "common": { "app_name": "SurfSense", "welcome": "Welcome", - "loading": "Loading...", + "loading": "Loading", + "initializing": "Initializing", "save": "Save", "cancel": "Cancel", "delete": "Delete", @@ -76,9 +77,10 @@ "passwords_no_match": "Passwords do not match", "password_mismatch": "Password Mismatch", "passwords_no_match_desc": "The passwords you entered do not match", - "creating_account": "Creating your account...", - "creating_account_btn": "Creating account...", - "redirecting_login": "Redirecting to login page..." + "creating_account": "Creating your account", + "creating_account_btn": "Creating account", + "redirecting_login": "Redirecting to login page", + "processing_authentication": "Processing authentication" }, "searchSpace": { "create_title": "Create Search Space", @@ -143,12 +145,15 @@ "api_keys": "API Keys", "profile": "Profile", "loading_dashboard": "Loading Dashboard", - "checking_auth": "Checking authentication...", + "checking_auth": "Checking authentication", "loading_config": "Loading Configuration", - "checking_llm_prefs": "Checking your LLM preferences...", + "checking_llm_prefs": "Checking your LLM preferences", + "setting_up_ai": "Setting up AI", "config_error": "Configuration Error", "failed_load_llm_config": "Failed to load your LLM configuration", "error_loading_chats": "Error loading chats", + "loading_chat": "Loading chat", + "loading_document": "Loading document", "no_recent_chats": "No recent chats", "error_loading_space": "Error loading search space", "unknown_search_space": "Unknown Search Space", @@ -165,7 +170,7 @@ "create_search_space": "Create Search Space", "add_new_search_space": "Add New Search Space", "loading": "Loading", - "fetching_spaces": "Fetching your search spaces...", + "fetching_spaces": "Fetching your search spaces", "may_take_moment": "This may take a moment", "error": "Error", "something_wrong": "Something went wrong", diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index 51e378bb2..3c4b6cf34 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -3,6 +3,7 @@ "app_name": "SurfSense", "welcome": "欢迎", "loading": "加载中...", + "initializing": "正在初始化", "save": "保存", "cancel": "取消", "delete": "删除", @@ -76,9 +77,10 @@ "passwords_no_match": "密码不匹配", "password_mismatch": "密码不匹配", "passwords_no_match_desc": "您输入的密码不一致", - "creating_account": "正在创建您的账户...", - "creating_account_btn": "创建中...", - "redirecting_login": "正在跳转到登录页面..." + "creating_account": "正在创建您的账户", + "creating_account_btn": "创建中", + "redirecting_login": "正在跳转到登录页面", + "processing_authentication": "正在处理身份验证" }, "searchSpace": { "create_title": "创建搜索空间", @@ -128,12 +130,15 @@ "api_keys": "API 密钥", "profile": "个人资料", "loading_dashboard": "正在加载仪表盘", - "checking_auth": "正在检查身份验证...", + "checking_auth": "正在检查身份验证", "loading_config": "正在加载配置", - "checking_llm_prefs": "正在检查您的 LLM 偏好设置...", + "checking_llm_prefs": "正在检查您的 LLM 偏好设置", + "setting_up_ai": "正在设置 AI", "config_error": "配置错误", "failed_load_llm_config": "无法加载您的 LLM 配置", "error_loading_chats": "加载对话失败", + "loading_chat": "正在加载对话", + "loading_document": "正在加载文档", "no_recent_chats": "暂无最近对话", "error_loading_space": "加载搜索空间失败", "unknown_search_space": "未知搜索空间", @@ -150,7 +155,7 @@ "create_search_space": "创建搜索空间", "add_new_search_space": "添加新的搜索空间", "loading": "加载中", - "fetching_spaces": "正在获取您的搜索空间...", + "fetching_spaces": "正在获取您的搜索空间", "may_take_moment": "这可能需要一些时间", "error": "错误", "something_wrong": "出现错误",