Merge pull request #744 from AnishSarkar22/feat/loading-and-chats

fix: No text in loading screen & UI of chats shown in sidebar
This commit is contained in:
Rohan Verma 2026-01-27 12:52:21 -08:00 committed by GitHub
commit 09f570ce85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 171 additions and 218 deletions

View file

@ -93,7 +93,7 @@ function LoginContent() {
}, [searchParams, t, tCommon]); }, [searchParams, t, tCommon]);
// Use global loading screen for auth type determination - spinner animation won't reset // Use global loading screen for auth type determination - spinner animation won't reset
useGlobalLoadingEffect(isLoading, tCommon("loading"), "login"); useGlobalLoadingEffect(isLoading);
// Show nothing while loading - the GlobalLoadingProvider handles the loading UI // Show nothing while loading - the GlobalLoadingProvider handles the loading UI
if (isLoading) { if (isLoading) {

View file

@ -1,13 +1,10 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
export default function AuthCallbackLoading() { export default function AuthCallbackLoading() {
const t = useTranslations("auth");
// Use global loading - spinner animation won't reset when page transitions // Use global loading - spinner animation won't reset when page transitions
useGlobalLoadingEffect(true, t("processing_authentication"), "default"); useGlobalLoadingEffect(true);
// Return null - the GlobalLoadingProvider handles the loading UI // Return null - the GlobalLoadingProvider handles the loading UI
return null; return null;

View file

@ -154,11 +154,7 @@ export function DashboardClientLayout({
isAutoConfiguring; isAutoConfiguring;
// Use global loading screen - spinner animation won't reset // Use global loading screen - spinner animation won't reset
useGlobalLoadingEffect( useGlobalLoadingEffect(shouldShowLoading);
shouldShowLoading,
isAutoConfiguring ? t("setting_up_ai") : t("checking_llm_prefs"),
"default"
);
if (shouldShowLoading) { if (shouldShowLoading) {
return null; return null;

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
@ -10,11 +9,10 @@ interface DashboardLayoutProps {
} }
export default function DashboardLayout({ children }: DashboardLayoutProps) { export default function DashboardLayout({ children }: DashboardLayoutProps) {
const t = useTranslations("dashboard");
const [isCheckingAuth, setIsCheckingAuth] = useState(true); const [isCheckingAuth, setIsCheckingAuth] = useState(true);
// Use the global loading screen - spinner animation won't reset // Use the global loading screen - spinner animation won't reset
useGlobalLoadingEffect(isCheckingAuth, t("checking_auth"), "default"); useGlobalLoadingEffect(isCheckingAuth);
useEffect(() => { useEffect(() => {
// Check if user is authenticated // Check if user is authenticated

View file

@ -1,13 +1,10 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
export default function DashboardLoading() { export default function DashboardLoading() {
const t = useTranslations("common");
// Use global loading - spinner animation won't reset when page transitions // Use global loading - spinner animation won't reset when page transitions
useGlobalLoadingEffect(true, t("loading"), "default"); useGlobalLoadingEffect(true);
// Return null - the GlobalLoadingProvider handles the loading UI // Return null - the GlobalLoadingProvider handles the loading UI
return null; return null;

View file

@ -106,7 +106,7 @@ export default function DashboardPage() {
const shouldShowLoading = isLoading || searchSpaces.length > 0; const shouldShowLoading = isLoading || searchSpaces.length > 0;
// Use global loading screen - spinner animation won't reset // Use global loading screen - spinner animation won't reset
useGlobalLoadingEffect(shouldShowLoading, t("fetching_spaces"), "default"); useGlobalLoadingEffect(shouldShowLoading);
if (error) return <ErrorScreen message={error?.message || "Failed to load search spaces"} />; if (error) return <ErrorScreen message={error?.message || "Failed to load search spaces"} />;

View file

@ -2,29 +2,18 @@ import { atom } from "jotai";
interface GlobalLoadingState { interface GlobalLoadingState {
isLoading: boolean; isLoading: boolean;
message?: string;
variant: "login" | "default";
} }
export const globalLoadingAtom = atom<GlobalLoadingState>({ export const globalLoadingAtom = atom<GlobalLoadingState>({
isLoading: false, isLoading: false,
message: undefined,
variant: "default",
}); });
// Helper atom for showing global loading // Helper atom for showing global loading
export const showGlobalLoadingAtom = atom( export const showGlobalLoadingAtom = atom(null, (get, set) => {
null, set(globalLoadingAtom, { isLoading: true });
( });
get,
set,
{ message, variant = "default" }: { message?: string; variant?: "login" | "default" }
) => {
set(globalLoadingAtom, { isLoading: true, message, variant });
}
);
// Helper atom for hiding global loading // Helper atom for hiding global loading
export const hideGlobalLoadingAtom = atom(null, (get, set) => { export const hideGlobalLoadingAtom = atom(null, (get, set) => {
set(globalLoadingAtom, { isLoading: false, message: undefined, variant: "default" }); set(globalLoadingAtom, { isLoading: false });
}); });

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect } from "react"; import { useEffect } from "react";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils"; import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils";
@ -27,11 +26,10 @@ const TokenHandler = ({
tokenParamName = "token", tokenParamName = "token",
storageKey = "surfsense_bearer_token", storageKey = "surfsense_bearer_token",
}: TokenHandlerProps) => { }: TokenHandlerProps) => {
const t = useTranslations("auth");
const searchParams = useSearchParams(); const searchParams = useSearchParams();
// Always show loading for this component - spinner animation won't reset // Always show loading for this component - spinner animation won't reset
useGlobalLoadingEffect(true, t("processing_authentication"), "default"); useGlobalLoadingEffect(true);
useEffect(() => { useEffect(() => {
// Only run on client-side // Only run on client-side

View file

@ -87,10 +87,10 @@ export function LayoutDataProvider({
enabled: !!searchSpaceId, enabled: !!searchSpaceId,
}); });
// Fetch threads // Fetch threads (40 total to allow up to 20 per section - shared/private)
const { data: threadsData } = useQuery({ const { data: threadsData } = useQuery({
queryKey: ["threads", searchSpaceId, { limit: 4 }], queryKey: ["threads", searchSpaceId, { limit: 40 }],
queryFn: () => fetchThreads(Number(searchSpaceId), 4), queryFn: () => fetchThreads(Number(searchSpaceId), 40),
enabled: !!searchSpaceId, enabled: !!searchSpaceId,
}); });

View file

@ -3,7 +3,6 @@
import { FolderOpen, MessageSquare, PenSquare } from "lucide-react"; import { FolderOpen, MessageSquare, PenSquare } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types"; import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
@ -121,101 +120,113 @@ export function Sidebar({
)} )}
</div> </div>
{/* Scrollable content */} {/* Chat sections - fills available space */}
<ScrollArea className="flex-1"> {isCollapsed ? (
{isCollapsed ? ( <div className="flex-1 flex flex-col items-center gap-2 py-2 w-[60px]">
<div className="flex flex-col items-center gap-2 py-2 w-[60px]"> {(chats.length > 0 || sharedChats.length > 0) && (
{(chats.length > 0 || sharedChats.length > 0) && ( <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Button
<Button variant="ghost"
variant="ghost" size="icon"
size="icon" className="h-10 w-10"
className="h-10 w-10" onClick={() => onToggleCollapse?.()}
onClick={() => onToggleCollapse?.()} >
> <MessageSquare className="h-4 w-4" />
<MessageSquare className="h-4 w-4" /> <span className="sr-only">{t("chats")}</span>
<span className="sr-only">{t("chats")}</span> </Button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent side="right">
<TooltipContent side="right"> {t("chats")} ({chats.length + sharedChats.length})
{t("chats")} ({chats.length + sharedChats.length}) </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> )}
</div>
) : (
<div className="flex-1 flex flex-col gap-1 py-2 w-[240px] min-h-0 overflow-hidden">
{/* Shared Chats Section - takes half the space */}
<SidebarSection
title={t("shared_chats")}
defaultOpen={true}
fillHeight={true}
action={
onViewAllSharedChats ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
onClick={onViewAllSharedChats}
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
{t("view_all_shared_chats") || "View all shared chats"}
</TooltipContent>
</Tooltip>
) : undefined
}
>
{sharedChats.length > 0 ? (
<div className="relative flex-1 min-h-0">
<div
className={`flex flex-col gap-0.5 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? "pb-8" : ""}`}
>
{sharedChats.slice(0, 20).map((chat) => (
<ChatListItem
key={chat.id}
name={chat.name}
isActive={chat.id === activeChatId}
archived={chat.archived}
onClick={() => onChatSelect(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)}
/>
))}
</div>
{/* Gradient fade indicator when more than 4 items */}
{sharedChats.length > 4 && (
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-sidebar via-sidebar/90 to-transparent" />
)}
</div>
) : (
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_shared_chats")}</p>
)} )}
</div> </SidebarSection>
) : (
<div className="flex flex-col gap-1 py-2 w-[240px]">
{/* Shared Chats Section */}
<SidebarSection
title={t("shared_chats")}
defaultOpen={true}
action={
onViewAllSharedChats ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
onClick={onViewAllSharedChats}
>
<FolderOpen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">
{t("view_all_shared_chats") || "View all shared chats"}
</TooltipContent>
</Tooltip>
) : undefined
}
>
{sharedChats.length > 0 ? (
<div className="flex flex-col gap-0.5">
{sharedChats.map((chat) => (
<ChatListItem
key={chat.id}
name={chat.name}
isActive={chat.id === activeChatId}
archived={chat.archived}
onClick={() => onChatSelect(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)}
/>
))}
</div>
) : (
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_shared_chats")}</p>
)}
</SidebarSection>
{/* Private Chats Section */} {/* Private Chats Section - takes half the space */}
<SidebarSection <SidebarSection
title={t("chats")} title={t("chats")}
defaultOpen={true} defaultOpen={true}
action={ fillHeight={true}
onViewAllPrivateChats ? ( action={
<Tooltip> onViewAllPrivateChats ? (
<TooltipTrigger asChild> <Tooltip>
<Button <TooltipTrigger asChild>
variant="ghost" <Button
size="icon" variant="ghost"
className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0" size="icon"
onClick={onViewAllPrivateChats} className="h-8 w-8 shrink-0 hover:bg-transparent hover:text-current focus-visible:ring-0"
> onClick={onViewAllPrivateChats}
<FolderOpen className="h-4 w-4" /> >
</Button> <FolderOpen className="h-4 w-4" />
</TooltipTrigger> </Button>
<TooltipContent side="top"> </TooltipTrigger>
{t("view_all_private_chats") || "View all private chats"} <TooltipContent side="top">
</TooltipContent> {t("view_all_private_chats") || "View all private chats"}
</Tooltip> </TooltipContent>
) : undefined </Tooltip>
} ) : undefined
> }
{chats.length > 0 ? ( >
<div className="flex flex-col gap-0.5"> {chats.length > 0 ? (
{chats.map((chat) => ( <div className="relative flex-1 min-h-0">
<div
className={`flex flex-col gap-0.5 h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${chats.length > 4 ? "pb-8" : ""}`}
>
{chats.slice(0, 20).map((chat) => (
<ChatListItem <ChatListItem
key={chat.id} key={chat.id}
name={chat.name} name={chat.name}
@ -227,13 +238,17 @@ export function Sidebar({
/> />
))} ))}
</div> </div>
) : ( {/* Gradient fade indicator when more than 4 items */}
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_chats")}</p> {chats.length > 4 && (
)} <div className="pointer-events-none absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-sidebar via-sidebar/90 to-transparent" />
</SidebarSection> )}
</div> </div>
)} ) : (
</ScrollArea> <p className="px-2 py-1 text-xs text-muted-foreground">{t("no_chats")}</p>
)}
</SidebarSection>
</div>
)}
{/* Footer */} {/* Footer */}
<div className="mt-auto border-t"> <div className="mt-auto border-t">

View file

@ -11,6 +11,8 @@ interface SidebarSectionProps {
children: React.ReactNode; children: React.ReactNode;
action?: React.ReactNode; action?: React.ReactNode;
persistentAction?: React.ReactNode; persistentAction?: React.ReactNode;
className?: string;
fillHeight?: boolean;
} }
export function SidebarSection({ export function SidebarSection({
@ -19,12 +21,18 @@ export function SidebarSection({
children, children,
action, action,
persistentAction, persistentAction,
className,
fillHeight = false,
}: SidebarSectionProps) { }: SidebarSectionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen); const [isOpen, setIsOpen] = useState(defaultOpen);
return ( return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="overflow-hidden"> <Collapsible
<div className="flex items-center group/section"> open={isOpen}
onOpenChange={setIsOpen}
className={cn("overflow-hidden", fillHeight && "flex flex-col flex-1 min-h-0", className)}
>
<div className="flex items-center group/section shrink-0">
<CollapsibleTrigger className="flex flex-1 items-center gap-1.5 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors min-w-0"> <CollapsibleTrigger className="flex flex-1 items-center gap-1.5 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors min-w-0">
<ChevronRight <ChevronRight
className={cn( className={cn(
@ -48,8 +56,14 @@ export function SidebarSection({
)} )}
</div> </div>
<CollapsibleContent className="overflow-hidden"> <CollapsibleContent
<div className="px-2 pb-2">{children}</div> className={cn("overflow-hidden", fillHeight && "flex-1 flex flex-col min-h-0")}
>
<div
className={cn("px-2 pb-2", fillHeight && "flex-1 flex flex-col min-h-0 overflow-hidden")}
>
{children}
</div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
); );

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
@ -30,7 +29,6 @@ interface ElectricProviderProps {
* 5. Provides client via context - hooks should use useElectricClient() * 5. Provides client via context - hooks should use useElectricClient()
*/ */
export function ElectricProvider({ children }: ElectricProviderProps) { export function ElectricProvider({ children }: ElectricProviderProps) {
const t = useTranslations("common");
const [electricClient, setElectricClient] = useState<ElectricClient | null>(null); const [electricClient, setElectricClient] = useState<ElectricClient | null>(null);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
const { const {
@ -117,7 +115,7 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
const shouldShowLoading = hasToken && isUserLoaded && !!user?.id && !electricClient && !error; const shouldShowLoading = hasToken && isUserLoaded && !!user?.id && !electricClient && !error;
// Use global loading hook with ownership tracking - prevents flash during transitions // Use global loading hook with ownership tracking - prevents flash during transitions
useGlobalLoadingEffect(shouldShowLoading, t("initializing"), "default"); useGlobalLoadingEffect(shouldShowLoading);
// For non-authenticated pages (like landing page), render immediately with null context // For non-authenticated pages (like landing page), render immediately with null context
// Also render immediately if user query failed (e.g., token expired) // Also render immediately if user query failed (e.g., token expired)

View file

@ -3,9 +3,7 @@
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { AmbientBackground } from "@/app/(home)/login/AmbientBackground";
import { globalLoadingAtom } from "@/atoms/ui/loading.atoms"; import { globalLoadingAtom } from "@/atoms/ui/loading.atoms";
import { Logo } from "@/components/Logo";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -18,7 +16,7 @@ import { cn } from "@/lib/utils";
*/ */
export function GlobalLoadingProvider({ children }: { children: React.ReactNode }) { export function GlobalLoadingProvider({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const { isLoading, message, variant } = useAtomValue(globalLoadingAtom); const { isLoading } = useAtomValue(globalLoadingAtom);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
@ -36,35 +34,11 @@ export function GlobalLoadingProvider({ children }: { children: React.ReactNode
)} )}
aria-hidden={!isLoading} aria-hidden={!isLoading}
> >
{variant === "login" ? ( <div className="flex min-h-screen flex-col items-center justify-center bg-background">
<div className="relative w-full h-full overflow-hidden bg-background"> <div className="h-12 w-12 flex items-center justify-center">
<AmbientBackground /> <Spinner size="lg" className="text-muted-foreground" />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
<Logo className="rounded-md" />
<div className="mt-8 flex flex-col items-center space-y-4">
<div className="h-12 w-12 flex items-center justify-center">
{/* Spinner is always mounted, animation never resets */}
<Spinner size="lg" className="text-muted-foreground" />
</div>
<span className="text-muted-foreground text-sm min-h-[1.25rem] text-center max-w-xs">
{message}
</span>
</div>
</div>
</div> </div>
) : ( </div>
<div className="flex min-h-screen flex-col items-center justify-center bg-background">
<div className="flex flex-col items-center space-y-4">
<div className="h-12 w-12 flex items-center justify-center">
{/* Spinner is always mounted, animation never resets */}
<Spinner size="xl" className="text-primary" />
</div>
<span className="text-muted-foreground text-sm min-h-[1.25rem] text-center max-w-md px-4">
{message}
</span>
</div>
</div>
)}
</div> </div>
); );

View file

@ -20,21 +20,18 @@ let pendingHideTimeout: ReturnType<typeof setTimeout> | null = null;
export function useGlobalLoading() { export function useGlobalLoading() {
const [loading, setLoading] = useAtom(globalLoadingAtom); const [loading, setLoading] = useAtom(globalLoadingAtom);
const show = useCallback( const show = useCallback(() => {
(message?: string, variant: "login" | "default" = "default") => { // Cancel any pending hide - new loading request takes over
// Cancel any pending hide - new loading request takes over if (pendingHideTimeout) {
if (pendingHideTimeout) { clearTimeout(pendingHideTimeout);
clearTimeout(pendingHideTimeout); pendingHideTimeout = null;
pendingHideTimeout = null; }
}
const id = ++loadingIdCounter; const id = ++loadingIdCounter;
currentLoadingId = id; currentLoadingId = id;
setLoading({ isLoading: true, message, variant }); setLoading({ isLoading: true });
return id; return id;
}, }, [setLoading]);
[setLoading]
);
const hide = useCallback( const hide = useCallback(
(id?: number) => { (id?: number) => {
@ -50,7 +47,7 @@ export function useGlobalLoading() {
// Double-check we're still the current loading after the delay // Double-check we're still the current loading after the delay
if (id === undefined || id === currentLoadingId) { if (id === undefined || id === currentLoadingId) {
currentLoadingId = null; currentLoadingId = null;
setLoading({ isLoading: false, message: undefined, variant: "default" }); setLoading({ isLoading: false });
} }
pendingHideTimeout = null; pendingHideTimeout = null;
}, 50); // Small delay to allow next component to mount and show loading }, 50); // Small delay to allow next component to mount and show loading
@ -70,27 +67,21 @@ export function useGlobalLoading() {
* transition loading states (e.g., layout page). * transition loading states (e.g., layout page).
* *
* @param shouldShow - Whether the loading screen should be visible * @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( export function useGlobalLoadingEffect(shouldShow: boolean) {
shouldShow: boolean,
message?: string,
variant: "login" | "default" = "default"
) {
const { show, hide } = useGlobalLoading(); const { show, hide } = useGlobalLoading();
const loadingIdRef = useRef<number | null>(null); const loadingIdRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
if (shouldShow) { if (shouldShow) {
// Show loading and store the ID // Show loading and store the ID
loadingIdRef.current = show(message, variant); loadingIdRef.current = show();
} else if (loadingIdRef.current !== null) { } else if (loadingIdRef.current !== null) {
// Only hide if we were the ones showing loading // Only hide if we were the ones showing loading
hide(loadingIdRef.current); hide(loadingIdRef.current);
loadingIdRef.current = null; loadingIdRef.current = null;
} }
}, [shouldShow, message, variant, show, hide]); }, [shouldShow, show, hide]);
// Cleanup on unmount - only hide if we're still the active loading // Cleanup on unmount - only hide if we're still the active loading
useEffect(() => { useEffect(() => {

View file

@ -2,8 +2,6 @@
"common": { "common": {
"app_name": "SurfSense", "app_name": "SurfSense",
"welcome": "Welcome", "welcome": "Welcome",
"loading": "Loading",
"initializing": "Initializing",
"save": "Save", "save": "Save",
"cancel": "Cancel", "cancel": "Cancel",
"delete": "Delete", "delete": "Delete",
@ -80,8 +78,7 @@
"passwords_no_match_desc": "The passwords you entered do not match", "passwords_no_match_desc": "The passwords you entered do not match",
"creating_account": "Creating your account", "creating_account": "Creating your account",
"creating_account_btn": "Creating account", "creating_account_btn": "Creating account",
"redirecting_login": "Redirecting to login page", "redirecting_login": "Redirecting to login page"
"processing_authentication": "Processing authentication"
}, },
"searchSpace": { "searchSpace": {
"create_title": "Create Search Space", "create_title": "Create Search Space",
@ -146,10 +143,7 @@
"api_keys": "API Keys", "api_keys": "API Keys",
"profile": "Profile", "profile": "Profile",
"loading_dashboard": "Loading Dashboard", "loading_dashboard": "Loading Dashboard",
"checking_auth": "Checking authentication",
"loading_config": "Loading Configuration", "loading_config": "Loading Configuration",
"checking_llm_prefs": "Checking your LLM preferences",
"setting_up_ai": "Setting up AI",
"config_error": "Configuration Error", "config_error": "Configuration Error",
"failed_load_llm_config": "Failed to load your LLM configuration", "failed_load_llm_config": "Failed to load your LLM configuration",
"error_loading_chats": "Error loading chats", "error_loading_chats": "Error loading chats",
@ -171,7 +165,6 @@
"create_search_space": "Create Search Space", "create_search_space": "Create Search Space",
"add_new_search_space": "Add New Search Space", "add_new_search_space": "Add New Search Space",
"loading": "Loading", "loading": "Loading",
"fetching_spaces": "Fetching your search spaces",
"may_take_moment": "This may take a moment", "may_take_moment": "This may take a moment",
"error": "Error", "error": "Error",
"something_wrong": "Something went wrong", "something_wrong": "Something went wrong",

View file

@ -2,8 +2,6 @@
"common": { "common": {
"app_name": "SurfSense", "app_name": "SurfSense",
"welcome": "欢迎", "welcome": "欢迎",
"loading": "加载中...",
"initializing": "正在初始化",
"save": "保存", "save": "保存",
"cancel": "取消", "cancel": "取消",
"delete": "删除", "delete": "删除",
@ -80,8 +78,7 @@
"passwords_no_match_desc": "您输入的密码不一致", "passwords_no_match_desc": "您输入的密码不一致",
"creating_account": "正在创建您的账户", "creating_account": "正在创建您的账户",
"creating_account_btn": "创建中", "creating_account_btn": "创建中",
"redirecting_login": "正在跳转到登录页面", "redirecting_login": "正在跳转到登录页面"
"processing_authentication": "正在处理身份验证"
}, },
"searchSpace": { "searchSpace": {
"create_title": "创建搜索空间", "create_title": "创建搜索空间",
@ -131,10 +128,7 @@
"api_keys": "API 密钥", "api_keys": "API 密钥",
"profile": "个人资料", "profile": "个人资料",
"loading_dashboard": "正在加载仪表盘", "loading_dashboard": "正在加载仪表盘",
"checking_auth": "正在检查身份验证",
"loading_config": "正在加载配置", "loading_config": "正在加载配置",
"checking_llm_prefs": "正在检查您的 LLM 偏好设置",
"setting_up_ai": "正在设置 AI",
"config_error": "配置错误", "config_error": "配置错误",
"failed_load_llm_config": "无法加载您的 LLM 配置", "failed_load_llm_config": "无法加载您的 LLM 配置",
"error_loading_chats": "加载对话失败", "error_loading_chats": "加载对话失败",
@ -156,7 +150,6 @@
"create_search_space": "创建搜索空间", "create_search_space": "创建搜索空间",
"add_new_search_space": "添加新的搜索空间", "add_new_search_space": "添加新的搜索空间",
"loading": "加载中", "loading": "加载中",
"fetching_spaces": "正在获取您的搜索空间",
"may_take_moment": "这可能需要一些时间", "may_take_moment": "这可能需要一些时间",
"error": "错误", "error": "错误",
"something_wrong": "出现错误", "something_wrong": "出现错误",