mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-05 05:42:39 +02:00
Merge remote-tracking branch 'upstream/dev' into fix/drive-connector
This commit is contained in:
commit
8a5f6ecce1
16 changed files with 171 additions and 218 deletions
|
|
@ -93,7 +93,7 @@ function LoginContent() {
|
|||
}, [searchParams, t, tCommon]);
|
||||
|
||||
// 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
|
||||
if (isLoading) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
"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");
|
||||
useGlobalLoadingEffect(true);
|
||||
|
||||
// Return null - the GlobalLoadingProvider handles the loading UI
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -154,11 +154,7 @@ export function DashboardClientLayout({
|
|||
isAutoConfiguring;
|
||||
|
||||
// Use global loading screen - spinner animation won't reset
|
||||
useGlobalLoadingEffect(
|
||||
shouldShowLoading,
|
||||
isAutoConfiguring ? t("setting_up_ai") : t("checking_llm_prefs"),
|
||||
"default"
|
||||
);
|
||||
useGlobalLoadingEffect(shouldShowLoading);
|
||||
|
||||
if (shouldShowLoading) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
|
|
@ -10,11 +9,10 @@ interface DashboardLayoutProps {
|
|||
}
|
||||
|
||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const t = useTranslations("dashboard");
|
||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||
|
||||
// Use the global loading screen - spinner animation won't reset
|
||||
useGlobalLoadingEffect(isCheckingAuth, t("checking_auth"), "default");
|
||||
useGlobalLoadingEffect(isCheckingAuth);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is authenticated
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
|
||||
export default function DashboardLoading() {
|
||||
const t = useTranslations("common");
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export default function DashboardPage() {
|
|||
const shouldShowLoading = isLoading || searchSpaces.length > 0;
|
||||
|
||||
// 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"} />;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,29 +2,18 @@ import { atom } from "jotai";
|
|||
|
||||
interface GlobalLoadingState {
|
||||
isLoading: boolean;
|
||||
message?: string;
|
||||
variant: "login" | "default";
|
||||
}
|
||||
|
||||
export const globalLoadingAtom = atom<GlobalLoadingState>({
|
||||
isLoading: false,
|
||||
message: undefined,
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
// Helper atom for showing global loading
|
||||
export const showGlobalLoadingAtom = atom(
|
||||
null,
|
||||
(
|
||||
get,
|
||||
set,
|
||||
{ message, variant = "default" }: { message?: string; variant?: "login" | "default" }
|
||||
) => {
|
||||
set(globalLoadingAtom, { isLoading: true, message, variant });
|
||||
}
|
||||
);
|
||||
export const showGlobalLoadingAtom = atom(null, (get, set) => {
|
||||
set(globalLoadingAtom, { isLoading: true });
|
||||
});
|
||||
|
||||
// Helper atom for hiding global loading
|
||||
export const hideGlobalLoadingAtom = atom(null, (get, set) => {
|
||||
set(globalLoadingAtom, { isLoading: false, message: undefined, variant: "default" });
|
||||
set(globalLoadingAtom, { isLoading: false });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect } from "react";
|
||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||
import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils";
|
||||
|
|
@ -27,11 +26,10 @@ const TokenHandler = ({
|
|||
tokenParamName = "token",
|
||||
storageKey = "surfsense_bearer_token",
|
||||
}: TokenHandlerProps) => {
|
||||
const t = useTranslations("auth");
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Always show loading for this component - spinner animation won't reset
|
||||
useGlobalLoadingEffect(true, t("processing_authentication"), "default");
|
||||
useGlobalLoadingEffect(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Only run on client-side
|
||||
|
|
|
|||
|
|
@ -86,10 +86,10 @@ export function LayoutDataProvider({
|
|||
enabled: !!searchSpaceId,
|
||||
});
|
||||
|
||||
// Fetch threads
|
||||
// Fetch threads (40 total to allow up to 20 per section - shared/private)
|
||||
const { data: threadsData } = useQuery({
|
||||
queryKey: ["threads", searchSpaceId, { limit: 4 }],
|
||||
queryFn: () => fetchThreads(Number(searchSpaceId), 4),
|
||||
queryKey: ["threads", searchSpaceId, { limit: 40 }],
|
||||
queryFn: () => fetchThreads(Number(searchSpaceId), 40),
|
||||
enabled: !!searchSpaceId,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { FolderOpen, MessageSquare, PenSquare } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
|
||||
|
|
@ -121,101 +120,113 @@ export function Sidebar({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<ScrollArea className="flex-1">
|
||||
{isCollapsed ? (
|
||||
<div className="flex flex-col items-center gap-2 py-2 w-[60px]">
|
||||
{(chats.length > 0 || sharedChats.length > 0) && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
onClick={() => onToggleCollapse?.()}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
<span className="sr-only">{t("chats")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{t("chats")} ({chats.length + sharedChats.length})
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{/* Chat sections - fills available space */}
|
||||
{isCollapsed ? (
|
||||
<div className="flex-1 flex flex-col items-center gap-2 py-2 w-[60px]">
|
||||
{(chats.length > 0 || sharedChats.length > 0) && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
onClick={() => onToggleCollapse?.()}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
<span className="sr-only">{t("chats")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{t("chats")} ({chats.length + sharedChats.length})
|
||||
</TooltipContent>
|
||||
</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>
|
||||
) : (
|
||||
<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>
|
||||
</SidebarSection>
|
||||
|
||||
{/* Private Chats Section */}
|
||||
<SidebarSection
|
||||
title={t("chats")}
|
||||
defaultOpen={true}
|
||||
action={
|
||||
onViewAllPrivateChats ? (
|
||||
<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={onViewAllPrivateChats}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
{t("view_all_private_chats") || "View all private chats"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{chats.length > 0 ? (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{chats.map((chat) => (
|
||||
{/* Private Chats Section - takes half the space */}
|
||||
<SidebarSection
|
||||
title={t("chats")}
|
||||
defaultOpen={true}
|
||||
fillHeight={true}
|
||||
action={
|
||||
onViewAllPrivateChats ? (
|
||||
<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={onViewAllPrivateChats}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
{t("view_all_private_chats") || "View all private chats"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{chats.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 ${chats.length > 4 ? "pb-8" : ""}`}
|
||||
>
|
||||
{chats.slice(0, 20).map((chat) => (
|
||||
<ChatListItem
|
||||
key={chat.id}
|
||||
name={chat.name}
|
||||
|
|
@ -227,13 +238,17 @@ export function Sidebar({
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_chats")}</p>
|
||||
)}
|
||||
</SidebarSection>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
{/* Gradient fade indicator when more than 4 items */}
|
||||
{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" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="px-2 py-1 text-xs text-muted-foreground">{t("no_chats")}</p>
|
||||
)}
|
||||
</SidebarSection>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-auto border-t">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ interface SidebarSectionProps {
|
|||
children: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
persistentAction?: React.ReactNode;
|
||||
className?: string;
|
||||
fillHeight?: boolean;
|
||||
}
|
||||
|
||||
export function SidebarSection({
|
||||
|
|
@ -19,12 +21,18 @@ export function SidebarSection({
|
|||
children,
|
||||
action,
|
||||
persistentAction,
|
||||
className,
|
||||
fillHeight = false,
|
||||
}: SidebarSectionProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="overflow-hidden">
|
||||
<div className="flex items-center group/section">
|
||||
<Collapsible
|
||||
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">
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
|
|
@ -48,8 +56,14 @@ export function SidebarSection({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<CollapsibleContent className="overflow-hidden">
|
||||
<div className="px-2 pb-2">{children}</div>
|
||||
<CollapsibleContent
|
||||
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>
|
||||
</Collapsible>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"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";
|
||||
|
|
@ -30,7 +29,6 @@ interface ElectricProviderProps {
|
|||
* 5. Provides client via context - hooks should use useElectricClient()
|
||||
*/
|
||||
export function ElectricProvider({ children }: ElectricProviderProps) {
|
||||
const t = useTranslations("common");
|
||||
const [electricClient, setElectricClient] = useState<ElectricClient | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const {
|
||||
|
|
@ -117,7 +115,7 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
|
|||
const shouldShowLoading = hasToken && isUserLoaded && !!user?.id && !electricClient && !error;
|
||||
|
||||
// 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
|
||||
// Also render immediately if user query failed (e.g., token expired)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
import { useAtomValue } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { AmbientBackground } from "@/app/(home)/login/AmbientBackground";
|
||||
import { globalLoadingAtom } from "@/atoms/ui/loading.atoms";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -18,7 +16,7 @@ import { cn } from "@/lib/utils";
|
|||
*/
|
||||
export function GlobalLoadingProvider({ children }: { children: React.ReactNode }) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { isLoading, message, variant } = useAtomValue(globalLoadingAtom);
|
||||
const { isLoading } = useAtomValue(globalLoadingAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
|
@ -36,35 +34,11 @@ export function GlobalLoadingProvider({ children }: { children: React.ReactNode
|
|||
)}
|
||||
aria-hidden={!isLoading}
|
||||
>
|
||||
{variant === "login" ? (
|
||||
<div className="relative w-full h-full overflow-hidden bg-background">
|
||||
<AmbientBackground />
|
||||
<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 className="flex min-h-screen flex-col items-center justify-center bg-background">
|
||||
<div className="h-12 w-12 flex items-center justify-center">
|
||||
<Spinner size="lg" className="text-muted-foreground" />
|
||||
</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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,21 +20,18 @@ let pendingHideTimeout: ReturnType<typeof setTimeout> | null = null;
|
|||
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 show = useCallback(() => {
|
||||
// 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 id = ++loadingIdCounter;
|
||||
currentLoadingId = id;
|
||||
setLoading({ isLoading: true });
|
||||
return id;
|
||||
}, [setLoading]);
|
||||
|
||||
const hide = useCallback(
|
||||
(id?: number) => {
|
||||
|
|
@ -50,7 +47,7 @@ export function useGlobalLoading() {
|
|||
// 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" });
|
||||
setLoading({ isLoading: false });
|
||||
}
|
||||
pendingHideTimeout = null;
|
||||
}, 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).
|
||||
*
|
||||
* @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"
|
||||
) {
|
||||
export function useGlobalLoadingEffect(shouldShow: boolean) {
|
||||
const { show, hide } = useGlobalLoading();
|
||||
const loadingIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldShow) {
|
||||
// Show loading and store the ID
|
||||
loadingIdRef.current = show(message, variant);
|
||||
loadingIdRef.current = show();
|
||||
} 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]);
|
||||
}, [shouldShow, show, hide]);
|
||||
|
||||
// Cleanup on unmount - only hide if we're still the active loading
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
"common": {
|
||||
"app_name": "SurfSense",
|
||||
"welcome": "Welcome",
|
||||
"loading": "Loading",
|
||||
"initializing": "Initializing",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
|
|
@ -80,8 +78,7 @@
|
|||
"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",
|
||||
"processing_authentication": "Processing authentication"
|
||||
"redirecting_login": "Redirecting to login page"
|
||||
},
|
||||
"searchSpace": {
|
||||
"create_title": "Create Search Space",
|
||||
|
|
@ -146,10 +143,7 @@
|
|||
"api_keys": "API Keys",
|
||||
"profile": "Profile",
|
||||
"loading_dashboard": "Loading Dashboard",
|
||||
"checking_auth": "Checking authentication",
|
||||
"loading_config": "Loading Configuration",
|
||||
"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",
|
||||
|
|
@ -171,7 +165,6 @@
|
|||
"create_search_space": "Create Search Space",
|
||||
"add_new_search_space": "Add New Search Space",
|
||||
"loading": "Loading",
|
||||
"fetching_spaces": "Fetching your search spaces",
|
||||
"may_take_moment": "This may take a moment",
|
||||
"error": "Error",
|
||||
"something_wrong": "Something went wrong",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
"common": {
|
||||
"app_name": "SurfSense",
|
||||
"welcome": "欢迎",
|
||||
"loading": "加载中...",
|
||||
"initializing": "正在初始化",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
|
|
@ -80,8 +78,7 @@
|
|||
"passwords_no_match_desc": "您输入的密码不一致",
|
||||
"creating_account": "正在创建您的账户",
|
||||
"creating_account_btn": "创建中",
|
||||
"redirecting_login": "正在跳转到登录页面",
|
||||
"processing_authentication": "正在处理身份验证"
|
||||
"redirecting_login": "正在跳转到登录页面"
|
||||
},
|
||||
"searchSpace": {
|
||||
"create_title": "创建搜索空间",
|
||||
|
|
@ -131,10 +128,7 @@
|
|||
"api_keys": "API 密钥",
|
||||
"profile": "个人资料",
|
||||
"loading_dashboard": "正在加载仪表盘",
|
||||
"checking_auth": "正在检查身份验证",
|
||||
"loading_config": "正在加载配置",
|
||||
"checking_llm_prefs": "正在检查您的 LLM 偏好设置",
|
||||
"setting_up_ai": "正在设置 AI",
|
||||
"config_error": "配置错误",
|
||||
"failed_load_llm_config": "无法加载您的 LLM 配置",
|
||||
"error_loading_chats": "加载对话失败",
|
||||
|
|
@ -156,7 +150,6 @@
|
|||
"create_search_space": "创建搜索空间",
|
||||
"add_new_search_space": "添加新的搜索空间",
|
||||
"loading": "加载中",
|
||||
"fetching_spaces": "正在获取您的搜索空间",
|
||||
"may_take_moment": "这可能需要一些时间",
|
||||
"error": "错误",
|
||||
"something_wrong": "出现错误",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue