From 24049345a2e8d8bedd6a13ce1cb0510cd9fd13c2 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:47:23 +0530 Subject: [PATCH 01/72] feat: add language selection feature to user profile sidebar, fixed some hover color logic for both model selector and language selection --- .../[search_space_id]/client-layout.tsx | 2 - .../layout/providers/LayoutDataProvider.tsx | 3 - .../components/layout/ui/header/Header.tsx | 4 - .../layout/ui/shell/LayoutShell.tsx | 4 - .../layout/ui/sidebar/SidebarUserProfile.tsx | 84 +++++++++++++++++-- .../components/new-chat/model-selector.tsx | 8 +- surfsense_web/components/ui/dropdown-menu.tsx | 4 +- surfsense_web/messages/en.json | 1 + surfsense_web/messages/zh.json | 1 + 9 files changed, 87 insertions(+), 24 deletions(-) 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 7b1bb61b0..7f36d021a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -16,7 +16,6 @@ import { import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup"; import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; -import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { LayoutDataProvider } from "@/components/layout"; import { OnboardingTour } from "@/components/onboarding-tour"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -200,7 +199,6 @@ export function DashboardClientLayout({ } - languageSwitcher={} > {children} diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 7f55e295f..1238cf28e 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -34,14 +34,12 @@ interface LayoutDataProviderProps { searchSpaceId: string; children: React.ReactNode; breadcrumb?: React.ReactNode; - languageSwitcher?: React.ReactNode; } export function LayoutDataProvider({ searchSpaceId, children, breadcrumb, - languageSwitcher, }: LayoutDataProviderProps) { const t = useTranslations("dashboard"); const tCommon = useTranslations("common"); @@ -375,7 +373,6 @@ export function LayoutDataProvider({ onLogout={handleLogout} pageUsage={pageUsage} breadcrumb={breadcrumb} - languageSwitcher={languageSwitcher} theme={theme} onToggleTheme={handleToggleTheme} isChatPage={isChatPage} diff --git a/surfsense_web/components/layout/ui/header/Header.tsx b/surfsense_web/components/layout/ui/header/Header.tsx index 182934d27..4244bba0d 100644 --- a/surfsense_web/components/layout/ui/header/Header.tsx +++ b/surfsense_web/components/layout/ui/header/Header.tsx @@ -7,7 +7,6 @@ import { NotificationButton } from "@/components/notifications/NotificationButto interface HeaderProps { breadcrumb?: React.ReactNode; - languageSwitcher?: React.ReactNode; theme?: string; onToggleTheme?: () => void; mobileMenuTrigger?: React.ReactNode; @@ -15,7 +14,6 @@ interface HeaderProps { export function Header({ breadcrumb, - languageSwitcher, theme, onToggleTheme, mobileMenuTrigger, @@ -45,8 +43,6 @@ export function Header({ {theme === "dark" ? "Light mode" : "Dark mode"} )} - - {languageSwitcher} ); diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx index ed3a09099..3047f84b5 100644 --- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx +++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx @@ -35,7 +35,6 @@ interface LayoutShellProps { onLogout?: () => void; pageUsage?: PageUsage; breadcrumb?: React.ReactNode; - languageSwitcher?: React.ReactNode; theme?: string; onToggleTheme?: () => void; defaultCollapsed?: boolean; @@ -69,7 +68,6 @@ export function LayoutShell({ onLogout, pageUsage, breadcrumb, - languageSwitcher, theme, onToggleTheme, defaultCollapsed = false, @@ -88,7 +86,6 @@ export function LayoutShell({
setMobileMenuOpen(true)} />} @@ -172,7 +169,6 @@ export function LayoutShell({
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index f67dbf7c6..376f27d06 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -1,19 +1,30 @@ "use client"; -import { ChevronUp, LogOut, Settings } from "lucide-react"; +import { ChevronUp, Languages, LogOut, Settings } from "lucide-react"; import { useTranslations } from "next-intl"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { useLocaleContext } from "@/contexts/LocaleContext"; import { cn } from "@/lib/utils"; import type { User } from "../../types/layout.types"; +// Supported languages configuration +const LANGUAGES = [ + { code: "en" as const, name: "English", flag: "🇺🇸" }, + { code: "zh" as const, name: "简体中文", flag: "🇨🇳" }, +]; + interface SidebarUserProfileProps { user: User; onUserSettings?: () => void; @@ -101,10 +112,15 @@ export function SidebarUserProfile({ isCollapsed = false, }: SidebarUserProfileProps) { const t = useTranslations("sidebar"); + const { locale, setLocale } = useLocaleContext(); const bgColor = stringToColor(user.email); const initials = getInitials(user.email); const displayName = user.name || user.email.split("@")[0]; + const handleLanguageChange = (newLocale: "en" | "zh") => { + setLocale(newLocale); + }; + // Collapsed view - just show avatar with dropdown if (isCollapsed) { return ( @@ -118,7 +134,8 @@ export function SidebarUserProfile({ className={cn( "flex h-10 w-full items-center justify-center rounded-md", "hover:bg-accent transition-colors", - "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + "focus:outline-none focus-visible:outline-none", + "data-[state=open]:bg-transparent" )} > @@ -129,7 +146,7 @@ export function SidebarUserProfile({ {displayName} - +
@@ -147,6 +164,34 @@ export function SidebarUserProfile({ {t("user_settings")} + + + + {t("language")} + + + + {LANGUAGES.map((language) => { + const isSelected = locale === language.code; + return ( + handleLanguageChange(language.code)} + className={cn( + "mb-1 last:mb-0", + !isSelected && "focus:bg-transparent hover:bg-transparent", + isSelected && "bg-accent focus:!bg-accent hover:!bg-accent" + )} + > + {language.flag} + {language.name} + + ); + })} + + + + @@ -169,7 +214,8 @@ export function SidebarUserProfile({ className={cn( "flex w-full items-center gap-2 px-2 py-3 text-left", "hover:bg-accent transition-colors", - "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + "focus:outline-none focus-visible:outline-none", + "data-[state=open]:bg-transparent" )} > @@ -185,7 +231,7 @@ export function SidebarUserProfile({ - +
@@ -203,6 +249,34 @@ export function SidebarUserProfile({ {t("user_settings")} + + + + {t("language")} + + + + {LANGUAGES.map((language) => { + const isSelected = locale === language.code; + return ( + handleLanguageChange(language.code)} + className={cn( + "mb-1 last:mb-0", + !isSelected && "focus:bg-transparent hover:bg-transparent", + isSelected && "bg-accent focus:!bg-accent hover:!bg-accent" + )} + > + {language.flag} + {language.name} + + ); + })} + + + + diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index ead378e86..f9ebc0077 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -265,8 +265,8 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp onSelect={() => handleSelectConfig(config)} className={cn( "mx-2 rounded-lg mb-1 cursor-pointer", - "aria-selected:bg-accent/50", - isSelected && "bg-accent/80" + !isSelected && "data-[selected=true]:bg-transparent hover:bg-transparent", + isSelected && "bg-accent/80 data-[selected=true]:!bg-accent/80 hover:!bg-accent/80" )} >
@@ -327,8 +327,8 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp onSelect={() => handleSelectConfig(config)} className={cn( "mx-2 rounded-lg mb-1 cursor-pointer", - "aria-selected:bg-accent/50", - isSelected && "bg-accent/80" + !isSelected && "data-[selected=true]:bg-transparent hover:bg-transparent", + isSelected && "bg-accent/80 data-[selected=true]:!bg-accent/80 hover:!bg-accent/80" )} >
diff --git a/surfsense_web/components/ui/dropdown-menu.tsx b/surfsense_web/components/ui/dropdown-menu.tsx index 810827ea4..a9df1a5b2 100644 --- a/surfsense_web/components/ui/dropdown-menu.tsx +++ b/surfsense_web/components/ui/dropdown-menu.tsx @@ -182,13 +182,13 @@ function DropdownMenuSubTrigger({ data-slot="dropdown-menu-sub-trigger" data-inset={inset} className={cn( - "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", + "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className )} {...props} > {children} - + ); } diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 0c6fe63a8..4d58e51f4 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -687,6 +687,7 @@ "expand_sidebar": "Expand sidebar", "collapse_sidebar": "Collapse sidebar", "user_settings": "User settings", + "language": "Language", "logout": "Logout" }, "errors": { diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index b48e3e9c7..9a9061a52 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -672,6 +672,7 @@ "expand_sidebar": "展开侧边栏", "collapse_sidebar": "收起侧边栏", "user_settings": "用户设置", + "language": "语言", "logout": "退出登录" }, "errors": { From 459a354e0f786379809d42d6a9f44fb9a0462e7b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:04:56 +0530 Subject: [PATCH 02/72] feat: shifted the theme selection to user profile sidebar --- .../layout/providers/LayoutDataProvider.tsx | 5 +- .../components/layout/ui/header/Header.tsx | 20 ----- .../layout/ui/shell/LayoutShell.tsx | 12 +-- .../layout/ui/sidebar/MobileSidebar.tsx | 6 ++ .../components/layout/ui/sidebar/Sidebar.tsx | 6 ++ .../layout/ui/sidebar/SidebarUserProfile.tsx | 81 ++++++++++++++++++- .../components/new-chat/model-selector.tsx | 2 +- surfsense_web/messages/en.json | 4 + surfsense_web/messages/zh.json | 4 + 9 files changed, 108 insertions(+), 32 deletions(-) diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 1238cf28e..c7ffa8aba 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -300,9 +300,6 @@ export function LayoutDataProvider({ } }, [router]); - const handleToggleTheme = useCallback(() => { - setTheme(theme === "dark" ? "light" : "dark"); - }, [theme, setTheme]); const handleViewAllSharedChats = useCallback(() => { setIsAllSharedChatsSidebarOpen(true); @@ -374,7 +371,7 @@ export function LayoutDataProvider({ pageUsage={pageUsage} breadcrumb={breadcrumb} theme={theme} - onToggleTheme={handleToggleTheme} + setTheme={setTheme} isChatPage={isChatPage} > {children} diff --git a/surfsense_web/components/layout/ui/header/Header.tsx b/surfsense_web/components/layout/ui/header/Header.tsx index 4244bba0d..0a1bd225e 100644 --- a/surfsense_web/components/layout/ui/header/Header.tsx +++ b/surfsense_web/components/layout/ui/header/Header.tsx @@ -1,21 +1,14 @@ "use client"; -import { Moon, Sun } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { NotificationButton } from "@/components/notifications/NotificationButton"; interface HeaderProps { breadcrumb?: React.ReactNode; - theme?: string; - onToggleTheme?: () => void; mobileMenuTrigger?: React.ReactNode; } export function Header({ breadcrumb, - theme, - onToggleTheme, mobileMenuTrigger, }: HeaderProps) { return ( @@ -30,19 +23,6 @@ export function Header({
{/* Notifications */} - - {/* Theme toggle */} - {onToggleTheme && ( - - - - - {theme === "dark" ? "Light mode" : "Dark mode"} - - )}
); diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx index 3047f84b5..0225b94f9 100644 --- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx +++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx @@ -36,7 +36,7 @@ interface LayoutShellProps { pageUsage?: PageUsage; breadcrumb?: React.ReactNode; theme?: string; - onToggleTheme?: () => void; + setTheme?: (theme: "light" | "dark" | "system") => void; defaultCollapsed?: boolean; isChatPage?: boolean; children: React.ReactNode; @@ -69,7 +69,7 @@ export function LayoutShell({ pageUsage, breadcrumb, theme, - onToggleTheme, + setTheme, defaultCollapsed = false, isChatPage = false, children, @@ -86,8 +86,6 @@ export function LayoutShell({
setMobileMenuOpen(true)} />} /> @@ -117,6 +115,8 @@ export function LayoutShell({ onUserSettings={onUserSettings} onLogout={onLogout} pageUsage={pageUsage} + theme={theme} + setTheme={setTheme} />
@@ -163,14 +163,14 @@ export function LayoutShell({ onUserSettings={onUserSettings} onLogout={onLogout} pageUsage={pageUsage} + theme={theme} + setTheme={setTheme} className="hidden md:flex border-r shrink-0" />
diff --git a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx index 57fba60c9..7dd01d75a 100644 --- a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx @@ -33,6 +33,8 @@ interface MobileSidebarProps { onUserSettings?: () => void; onLogout?: () => void; pageUsage?: PageUsage; + theme?: string; + setTheme?: (theme: "light" | "dark" | "system") => void; } export function MobileSidebarTrigger({ onClick }: { onClick: () => void }) { @@ -70,6 +72,8 @@ export function MobileSidebar({ onUserSettings, onLogout, pageUsage, + theme, + setTheme, }: MobileSidebarProps) { const handleSearchSpaceSelect = (id: number) => { onSearchSpaceSelect(id); @@ -145,6 +149,8 @@ export function MobileSidebar({ onUserSettings={onUserSettings} onLogout={onLogout} pageUsage={pageUsage} + theme={theme} + setTheme={setTheme} className="w-full border-none" />
diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx index 9a632506c..336b695ea 100644 --- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx @@ -35,6 +35,8 @@ interface SidebarProps { onUserSettings?: () => void; onLogout?: () => void; pageUsage?: PageUsage; + theme?: string; + setTheme?: (theme: "light" | "dark" | "system") => void; className?: string; } @@ -58,6 +60,8 @@ export function Sidebar({ onUserSettings, onLogout, pageUsage, + theme, + setTheme, className, }: SidebarProps) { const t = useTranslations("sidebar"); @@ -241,6 +245,8 @@ export function Sidebar({ onUserSettings={onUserSettings} onLogout={onLogout} isCollapsed={isCollapsed} + theme={theme} + setTheme={setTheme} />
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index 376f27d06..d468c7bbe 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronUp, Languages, LogOut, Settings } from "lucide-react"; +import { Check, ChevronUp, Laptop, Languages, LogOut, Moon, Settings, Sun } from "lucide-react"; import { useTranslations } from "next-intl"; import { DropdownMenu, @@ -25,11 +25,20 @@ const LANGUAGES = [ { code: "zh" as const, name: "简体中文", flag: "🇨🇳" }, ]; +// Supported themes configuration +const THEMES = [ + { value: "light" as const, name: "Light", icon: Sun }, + { value: "dark" as const, name: "Dark", icon: Moon }, + { value: "system" as const, name: "System", icon: Laptop }, +]; + interface SidebarUserProfileProps { user: User; onUserSettings?: () => void; onLogout?: () => void; isCollapsed?: boolean; + theme?: string; + setTheme?: (theme: "light" | "dark" | "system") => void; } /** @@ -110,6 +119,8 @@ export function SidebarUserProfile({ onUserSettings, onLogout, isCollapsed = false, + theme, + setTheme, }: SidebarUserProfileProps) { const t = useTranslations("sidebar"); const { locale, setLocale } = useLocaleContext(); @@ -121,6 +132,10 @@ export function SidebarUserProfile({ setLocale(newLocale); }; + const handleThemeChange = (newTheme: "light" | "dark" | "system") => { + setTheme?.(newTheme); + }; + // Collapsed view - just show avatar with dropdown if (isCollapsed) { return ( @@ -164,6 +179,38 @@ export function SidebarUserProfile({ {t("user_settings")} + {setTheme && ( + + + + {t("theme")} + + + + {THEMES.map((themeOption) => { + const Icon = themeOption.icon; + const isSelected = theme === themeOption.value; + return ( + handleThemeChange(themeOption.value)} + className={cn( + "mb-1 last:mb-0", + !isSelected && "focus:bg-transparent hover:bg-transparent", + isSelected && "bg-accent focus:!bg-accent hover:!bg-accent" + )} + > + + {t(themeOption.value)} + {isSelected && } + + ); + })} + + + + )} + @@ -249,6 +296,38 @@ export function SidebarUserProfile({ {t("user_settings")} + {setTheme && ( + + + + {t("theme")} + + + + {THEMES.map((themeOption) => { + const Icon = themeOption.icon; + const isSelected = theme === themeOption.value; + return ( + handleThemeChange(themeOption.value)} + className={cn( + "mb-1 last:mb-0", + !isSelected && "focus:bg-transparent hover:bg-transparent", + isSelected && "bg-accent focus:!bg-accent hover:!bg-accent" + )} + > + + {t(themeOption.value)} + {isSelected && } + + ); + })} + + + + )} + diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index f9ebc0077..48141ed64 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -232,7 +232,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
Date: Tue, 20 Jan 2026 16:14:57 +0530 Subject: [PATCH 03/72] feat: integrate chat sharing functionality in header component --- .../new-chat/[[...chat_id]]/page.tsx | 2 -- .../components/layout/ui/header/Header.tsx | 35 +++++++++++++++++++ .../components/new-chat/chat-header.tsx | 7 +--- 3 files changed, 36 insertions(+), 8 deletions(-) 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 43c33ba5a..3fee0d0c9 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 @@ -1063,8 +1063,6 @@ export default function NewChatPage() { header={ } /> diff --git a/surfsense_web/components/layout/ui/header/Header.tsx b/surfsense_web/components/layout/ui/header/Header.tsx index 0a1bd225e..4f17aa303 100644 --- a/surfsense_web/components/layout/ui/header/Header.tsx +++ b/surfsense_web/components/layout/ui/header/Header.tsx @@ -1,6 +1,11 @@ "use client"; +import { useQuery } from "@tanstack/react-query"; +import { useParams, usePathname } from "next/navigation"; import { NotificationButton } from "@/components/notifications/NotificationButton"; +import { ChatShareButton } from "@/components/new-chat/chat-share-button"; +import { getThreadFull } from "@/lib/chat/thread-persistence"; +import type { ChatVisibility } from "@/lib/chat/thread-persistence"; interface HeaderProps { breadcrumb?: React.ReactNode; @@ -11,6 +16,29 @@ 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) + : 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 + // This callback can be used for additional side effects if needed + }; + return (
{/* Left side - Mobile menu trigger + Breadcrumb */} @@ -23,6 +51,13 @@ export function Header({
{/* Notifications */} + {/* Share button - only show on chat pages when thread exists */} + {isChatPage && currentThread && ( + + )}
); diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index fc01b6dd6..a6cf8df3a 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -5,18 +5,14 @@ import type { GlobalNewLLMConfig, NewLLMConfigPublic, } from "@/contracts/types/new-llm-config.types"; -import type { ChatVisibility, ThreadRecord } from "@/lib/chat/thread-persistence"; -import { ChatShareButton } from "./chat-share-button"; import { ModelConfigSidebar } from "./model-config-sidebar"; import { ModelSelector } from "./model-selector"; interface ChatHeaderProps { searchSpaceId: number; - thread?: ThreadRecord | null; - onThreadVisibilityChange?: (visibility: ChatVisibility) => void; } -export function ChatHeader({ searchSpaceId, thread, onThreadVisibilityChange }: ChatHeaderProps) { +export function ChatHeader({ searchSpaceId }: ChatHeaderProps) { const [sidebarOpen, setSidebarOpen] = useState(false); const [selectedConfig, setSelectedConfig] = useState< NewLLMConfigPublic | GlobalNewLLMConfig | null @@ -52,7 +48,6 @@ export function ChatHeader({ searchSpaceId, thread, onThreadVisibilityChange }: return (
- Date: Tue, 20 Jan 2026 16:33:42 +0530 Subject: [PATCH 04/72] feat: add thread visibility management using Jotai atoms in chat components --- .../atoms/chat/current-thread.atom.ts | 8 ++++ .../components/layout/ui/header/Header.tsx | 46 +++++++++++-------- .../components/new-chat/chat-share-button.tsx | 18 ++++++-- .../new-chat/model-config-sidebar.tsx | 41 +++++++++++------ 4 files changed, 76 insertions(+), 37 deletions(-) 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({
-
{config.description && (
-
)} @@ -264,15 +273,15 @@ export function ModelConfigSidebar({
-
-
@@ -281,9 +290,9 @@ export function ModelConfigSidebar({
-