diff --git a/surfsense_web/atoms/chat/current-thread.atom.ts b/surfsense_web/atoms/chat/current-thread.atom.ts index 1231887f8..69868ac67 100644 --- a/surfsense_web/atoms/chat/current-thread.atom.ts +++ b/surfsense_web/atoms/chat/current-thread.atom.ts @@ -47,6 +47,14 @@ export const addingCommentToMessageIdAtom = atom( } ); +// Setter atom for updating thread visibility +export const setThreadVisibilityAtom = atom( + null, + (get, set, newVisibility: ChatVisibility) => { + set(currentThreadAtom, { ...get(currentThreadAtom), visibility: newVisibility }); + } +); + export const resetCurrentThreadAtom = atom(null, (_, set) => { set(currentThreadAtom, initialState); }); diff --git a/surfsense_web/components/layout/ui/header/Header.tsx b/surfsense_web/components/layout/ui/header/Header.tsx index 4f17aa303..1981bba68 100644 --- a/surfsense_web/components/layout/ui/header/Header.tsx +++ b/surfsense_web/components/layout/ui/header/Header.tsx @@ -1,11 +1,11 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; -import { useParams, usePathname } from "next/navigation"; -import { NotificationButton } from "@/components/notifications/NotificationButton"; +import { useAtomValue } from "jotai"; +import { usePathname } from "next/navigation"; +import { currentThreadAtom } from "@/atoms/chat/current-thread.atom"; import { ChatShareButton } from "@/components/new-chat/chat-share-button"; -import { getThreadFull } from "@/lib/chat/thread-persistence"; -import type { ChatVisibility } from "@/lib/chat/thread-persistence"; +import { NotificationButton } from "@/components/notifications/NotificationButton"; +import type { ChatVisibility, ThreadRecord } from "@/lib/chat/thread-persistence"; interface HeaderProps { breadcrumb?: React.ReactNode; @@ -16,26 +16,34 @@ export function Header({ breadcrumb, mobileMenuTrigger, }: HeaderProps) { - const params = useParams(); const pathname = usePathname(); // Check if we're on a chat page const isChatPage = pathname?.includes("/new-chat") ?? false; - // Get chat_id from URL params - const chatId = params?.chat_id - ? Number(Array.isArray(params.chat_id) ? params.chat_id[0] : params.chat_id) + // Use Jotai atom for thread state (synced from chat page) + const currentThreadState = useAtomValue(currentThreadAtom); + + // Show button only when we have a thread id (thread exists and is synced to Jotai) + const hasThread = isChatPage && currentThreadState.id !== null; + + // Create minimal thread object for ChatShareButton (used for API calls) + const threadForButton: ThreadRecord | null = hasThread + ? { + id: currentThreadState.id!, + visibility: currentThreadState.visibility ?? "PRIVATE", + // These fields are not used by ChatShareButton for display, only for checks + created_by_id: null, + search_space_id: 0, + title: "", + archived: false, + created_at: "", + updated_at: "", + } : null; - // Fetch current thread if on chat page and chat_id exists - const { data: currentThread } = useQuery({ - queryKey: ["thread", chatId], - queryFn: () => getThreadFull(chatId!), - enabled: isChatPage && chatId !== null && chatId > 0, - }); - const handleVisibilityChange = (visibility: ChatVisibility) => { - // Visibility change is handled by ChatShareButton internally + // Visibility change is handled by ChatShareButton internally via Jotai // This callback can be used for additional side effects if needed }; @@ -52,9 +60,9 @@ export function Header({ {/* Notifications */} {/* Share button - only show on chat pages when thread exists */} - {isChatPage && currentThread && ( + {hasThread && ( )} diff --git a/surfsense_web/components/new-chat/chat-share-button.tsx b/surfsense_web/components/new-chat/chat-share-button.tsx index d9e269794..a40813e29 100644 --- a/surfsense_web/components/new-chat/chat-share-button.tsx +++ b/surfsense_web/components/new-chat/chat-share-button.tsx @@ -1,9 +1,11 @@ "use client"; import { useQueryClient } from "@tanstack/react-query"; +import { useAtomValue, useSetAtom } from "jotai"; import { Loader2, Lock, Users } from "lucide-react"; import { useCallback, useState } from "react"; import { toast } from "sonner"; +import { currentThreadAtom, setThreadVisibilityAtom } from "@/atoms/chat/current-thread.atom"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { @@ -44,7 +46,12 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS const [open, setOpen] = useState(false); const [isUpdating, setIsUpdating] = useState(false); - const currentVisibility = thread?.visibility ?? "PRIVATE"; + // Use Jotai atom for visibility (single source of truth) + const currentThreadState = useAtomValue(currentThreadAtom); + const setThreadVisibility = useSetAtom(setThreadVisibilityAtom); + + // Use Jotai visibility if available (synced from chat page), otherwise fall back to thread prop + const currentVisibility = currentThreadState.visibility ?? thread?.visibility ?? "PRIVATE"; const isOwnThread = thread?.created_by_id !== null; // If we have the thread, we can modify it const handleVisibilityChange = useCallback( @@ -55,10 +62,13 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS } setIsUpdating(true); + // Update Jotai atom immediately for instant UI feedback + setThreadVisibility(newVisibility); + try { await updateThreadVisibility(thread.id, newVisibility); - // Refetch all thread queries to update sidebar immediately + // Refetch threads list to update sidebar await queryClient.refetchQueries({ predicate: (query) => Array.isArray(query.queryKey) && query.queryKey[0] === "threads", }); @@ -70,12 +80,14 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS setOpen(false); } catch (error) { console.error("Failed to update visibility:", error); + // Revert Jotai state on error + setThreadVisibility(thread.visibility ?? "PRIVATE"); toast.error("Failed to update sharing settings"); } finally { setIsUpdating(false); } }, - [thread, currentVisibility, onVisibilityChange, queryClient] + [thread, currentVisibility, onVisibilityChange, queryClient, setThreadVisibility] ); // Don't show if no thread (new chat that hasn't been created yet) diff --git a/surfsense_web/components/new-chat/model-config-sidebar.tsx b/surfsense_web/components/new-chat/model-config-sidebar.tsx index 9d755f221..2e22612ad 100644 --- a/surfsense_web/components/new-chat/model-config-sidebar.tsx +++ b/surfsense_web/components/new-chat/model-config-sidebar.tsx @@ -4,6 +4,7 @@ import { useAtomValue } from "jotai"; import { AlertCircle, Bot, ChevronRight, Globe, User, X } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useCallback, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; import { toast } from "sonner"; import { createNewLLMConfigMutationAtom, @@ -38,6 +39,12 @@ export function ModelConfigSidebar({ mode, }: ModelConfigSidebarProps) { const [isSubmitting, setIsSubmitting] = useState(false); + const [mounted, setMounted] = useState(false); + + // Handle SSR - only render portal on client + useEffect(() => { + setMounted(true); + }, []); // Mutations - use mutateAsync from the atom value const { mutateAsync: createConfig } = useAtomValue(createNewLLMConfigMutationAtom); @@ -147,7 +154,9 @@ export function ModelConfigSidebar({ } }, [config, isGlobal, searchSpaceId, updatePreferences, onOpenChange]); - return ( + if (!mounted) return null; + + const sidebarContent = ( {open && ( <> @@ -157,7 +166,7 @@ export function ModelConfigSidebar({ animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} - className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm" + className="fixed inset-0 z-[24] bg-black/20 backdrop-blur-sm" onClick={() => onOpenChange(false)} /> @@ -172,7 +181,7 @@ export function ModelConfigSidebar({ stiffness: 300, }} className={cn( - "fixed right-0 top-0 z-50 h-full w-full sm:w-[480px] lg:w-[540px]", + "fixed right-0 top-0 z-[25] h-full w-full sm:w-[480px] lg:w-[540px]", "bg-background border-l border-border/50 shadow-2xl", "flex flex-col" )} @@ -245,16 +254,16 @@ export function ModelConfigSidebar({ - + Configuration Name - + {config.name} {config.description && ( - + Description - + {config.description} )} @@ -264,15 +273,15 @@ export function ModelConfigSidebar({ - + Provider - + {config.provider} - + Model - + {config.model_name} @@ -281,9 +290,9 @@ export function ModelConfigSidebar({ - + Citations - + - + System Instructions - + {config.system_instructions} @@ -367,4 +376,6 @@ export function ModelConfigSidebar({ )} ); + + return typeof document !== "undefined" ? createPortal(sidebarContent, document.body) : null; }
{config.name}
{config.description}
{config.provider}
{config.model_name}
{config.system_instructions} @@ -367,4 +376,6 @@ export function ModelConfigSidebar({ )} ); + + return typeof document !== "undefined" ? createPortal(sidebarContent, document.body) : null; }