diff --git a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx index 8dbc6b919..ad523027f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/onboard/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useAtomValue } from "jotai"; +import { useAtomValue, useSetAtom } from "jotai"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; @@ -13,6 +13,7 @@ import { globalNewLLMConfigsAtom, llmPreferencesAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; +import { searchSpaceSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { Logo } from "@/components/Logo"; import { LLMConfigForm, type LLMConfigFormData } from "@/components/shared/llm-config-form"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -23,6 +24,7 @@ export default function OnboardPage() { const router = useRouter(); const params = useParams(); const searchSpaceId = Number(params.search_space_id); + const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); // Queries const { @@ -259,7 +261,9 @@ export default function OnboardPage() { You can add more configurations and customize settings anytime in{" "} - + + )} - - + ); } diff --git a/surfsense_web/atoms/settings/settings-dialog.atoms.ts b/surfsense_web/atoms/settings/settings-dialog.atoms.ts new file mode 100644 index 000000000..4b573808c --- /dev/null +++ b/surfsense_web/atoms/settings/settings-dialog.atoms.ts @@ -0,0 +1,21 @@ +import { atom } from "jotai"; + +export interface SearchSpaceSettingsDialogState { + open: boolean; + initialTab: string; +} + +export interface UserSettingsDialogState { + open: boolean; + initialTab: string; +} + +export const searchSpaceSettingsDialogAtom = atom({ + open: false, + initialTab: "general", +}); + +export const userSettingsDialogAtom = atom({ + open: false, + initialTab: "profile", +}); diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 11f48a80b..6c42a3ffc 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -1,8 +1,7 @@ "use client"; -import { useAtomValue } from "jotai"; +import { useAtomValue, useSetAtom } from "jotai"; import { AlertTriangle, Cable, Settings } from "lucide-react"; -import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { type FC, forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; @@ -12,6 +11,7 @@ import { llmPreferencesAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { searchSpaceSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; @@ -50,6 +50,7 @@ export const ConnectorIndicator = forwardRef { const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const searchParams = useSearchParams(); + const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); const { data: currentUser } = useAtomValue(currentUserAtom); const { data: preferences = {}, isFetching: preferencesLoading } = useAtomValue(llmPreferencesAtom); @@ -417,12 +418,20 @@ export const ConnectorIndicator = forwardRef - + )} diff --git a/surfsense_web/components/assistant-ui/document-upload-popup.tsx b/surfsense_web/components/assistant-ui/document-upload-popup.tsx index c5a2eb977..15c29c6d0 100644 --- a/surfsense_web/components/assistant-ui/document-upload-popup.tsx +++ b/surfsense_web/components/assistant-ui/document-upload-popup.tsx @@ -1,8 +1,7 @@ "use client"; -import { useAtomValue } from "jotai"; -import { AlertTriangle, Settings, Upload } from "lucide-react"; -import Link from "next/link"; +import { useAtomValue, useSetAtom } from "jotai"; +import { AlertTriangle, Settings } from "lucide-react"; import { createContext, type FC, @@ -17,6 +16,7 @@ import { llmPreferencesAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { searchSpaceSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; @@ -91,6 +91,7 @@ const DocumentUploadPopupContent: FC<{ onOpenChange: (open: boolean) => void; }> = ({ isOpen, onOpenChange }) => { const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); + const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); const { data: preferences = {}, isFetching: preferencesLoading } = useAtomValue(llmPreferencesAtom); const { data: globalConfigs = [], isFetching: globalConfigsLoading } = @@ -157,12 +158,20 @@ const DocumentUploadPopupContent: FC<{ ? "Auto mode is selected but no global LLM configurations are available. Please configure a custom LLM in Settings to process and summarize your uploaded documents." : "You need to configure a Document Summary LLM before uploading files. This LLM is used to process and summarize your uploaded documents."}

- + ) : ( diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 3de1b7ac5..5de9ab2cd 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -14,6 +14,10 @@ import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom"; import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { + searchSpaceSettingsDialogAtom, + userSettingsDialogAtom, +} from "@/atoms/settings/settings-dialog.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { AlertDialog, @@ -47,6 +51,8 @@ import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-pers import { cleanupElectric } from "@/lib/electric/client"; import { resetUser, trackLogout } from "@/lib/posthog/events"; import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog"; +import { UserSettingsDialog } from "@/components/settings/user-settings-dialog"; import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types"; import { CreateSearchSpaceDialog } from "../ui/dialogs"; import { LayoutShell } from "../ui/shell"; @@ -390,15 +396,18 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid setIsCreateSearchSpaceDialogOpen(true); }, []); + const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom); + const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); + const handleUserSettings = useCallback(() => { - router.push(`/dashboard/${searchSpaceId}/user-settings?tab=profile`); - }, [router, searchSpaceId]); + setUserSettingsDialog({ open: true, initialTab: "profile" }); + }, [setUserSettingsDialog]); const handleSearchSpaceSettings = useCallback( - (space: SearchSpace) => { - router.push(`/dashboard/${space.id}/settings?tab=general`); + (_space: SearchSpace) => { + setSearchSpaceSettingsDialog({ open: true, initialTab: "general" }); }, - [router] + [setSearchSpaceSettingsDialog] ); const handleSearchSpaceDeleteClick = useCallback((space: SearchSpace) => { @@ -582,8 +591,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid ); const handleSettings = useCallback(() => { - router.push(`/dashboard/${searchSpaceId}/settings?tab=general`); - }, [router, searchSpaceId]); + setSearchSpaceSettingsDialog({ open: true, initialTab: "general" }); + }, [setSearchSpaceSettingsDialog]); const handleManageMembers = useCallback(() => { router.push(`/dashboard/${searchSpaceId}/team`); @@ -934,6 +943,10 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid open={isCreateSearchSpaceDialogOpen} onOpenChange={setIsCreateSearchSpaceDialogOpen} /> + + {/* Settings Dialogs */} + + ); } diff --git a/surfsense_web/components/new-chat/chat-share-button.tsx b/surfsense_web/components/new-chat/chat-share-button.tsx index b0f53d06b..472dd874c 100644 --- a/surfsense_web/components/new-chat/chat-share-button.tsx +++ b/surfsense_web/components/new-chat/chat-share-button.tsx @@ -3,12 +3,14 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtomValue, useSetAtom } from "jotai"; import { Earth, User, Users } from "lucide-react"; -import { useParams, useRouter } from "next/navigation"; + + import { useCallback, useMemo, useState } from "react"; import { toast } from "sonner"; import { currentThreadAtom, setThreadVisibilityAtom } from "@/atoms/chat/current-thread.atom"; import { myAccessAtom } from "@/atoms/members/members-query.atoms"; import { createPublicChatSnapshotMutationAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-mutation.atoms"; +import { searchSpaceSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -48,9 +50,8 @@ const visibilityOptions: { export function ChatShareButton({ thread, onVisibilityChange, className }: ChatShareButtonProps) { const queryClient = useQueryClient(); - const router = useRouter(); - const params = useParams(); const [open, setOpen] = useState(false); + const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); // Use Jotai atom for visibility (single source of truth) const currentThreadState = useAtomValue(currentThreadAtom); @@ -148,7 +149,10 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS + + Open link + + + {canDelete && ( - + - Open link + Delete - {canDelete && ( - - - - - - Delete - - - )} - + )} + {/* Message count badge */} @@ -125,25 +127,25 @@ export function PublicChatSnapshotRow({ {snapshot.public_url}

- - - - - - {copied ? "Copied!" : "Copy link"} - - + + + + + + {copied ? "Copied!" : "Copy link"} + + {/* Footer: Date + Creator */} @@ -152,33 +154,33 @@ export function PublicChatSnapshotRow({ {member && ( <> · - - - -
- {member.avatarUrl ? ( - {member.name} - ) : ( -
- - {getInitials(member.name)} - -
- )} - - {member.name} - -
-
- {member.email || member.name} -
-
+ + + +
+ {member.avatarUrl ? ( + {member.name} + ) : ( +
+ + {getInitials(member.name)} + +
+ )} + + {member.name} + +
+
+ {member.email || member.name} +
+
)} diff --git a/surfsense_web/components/settings/general-settings-manager.tsx b/surfsense_web/components/settings/general-settings-manager.tsx index bd22e9180..8c9a43780 100644 --- a/surfsense_web/components/settings/general-settings-manager.tsx +++ b/surfsense_web/components/settings/general-settings-manager.tsx @@ -160,26 +160,27 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager - {/* Action Buttons */} -
- - -
+ {/* Action Buttons */} +
+ + +
{hasChanges && ( = 2) { @@ -101,6 +91,7 @@ function getInitials(name: string): string { } export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { + const isDesktop = useMediaQuery("(min-width: 768px)"); // Image gen config atoms const { mutateAsync: createConfig, @@ -281,46 +272,40 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Header */}
+ + {canCreate && ( - {canCreate && ( - - )} + )}
{/* Errors */} - - {errors.map((err) => ( - - - - {err?.message} - - - ))} - + {errors.map((err) => ( +
+ + + {err?.message} + +
+ ))} {/* Read-only / Limited permissions notice */} {access && !isLoading && isReadOnly && ( - +
@@ -328,10 +313,10 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { configurations. Contact a space owner to request additional permissions. - +
)} {access && !isLoading && !isReadOnly && (!canCreate || !canDelete) && ( - +
@@ -343,7 +328,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { {!canDelete && ", but cannot delete them"}. - +
)} {/* Global info */} @@ -429,23 +414,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { ) : ( - - - {userConfigs?.map((config) => { - const member = config.user_id ? memberMap.get(config.user_id) : null; +
+ {userConfigs?.map((config) => { + const member = config.user_id ? memberMap.get(config.user_id) : null; - return ( - + return ( +
{/* Header: Name + Actions */} @@ -464,7 +438,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{canUpdate && ( - +
+ ); + })} +
)}
)} @@ -732,22 +705,20 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)} - {/* Actions */} -
- - + {isAssignmentComplete && !isLoading && !hasError && ( {/* Error Alert */} - - {hasError && ( - - - - - {(configsError?.message ?? "Failed to load LLM configurations") || - (preferencesError?.message ?? "Failed to load preferences") || - (globalConfigsError?.message ?? "Failed to load global configurations")} - - - - )} - + {hasError && ( +
+ + + + {(configsError?.message ?? "Failed to load LLM configurations") || + (preferencesError?.message ?? "Failed to load preferences") || + (globalConfigsError?.message ?? "Failed to load global configurations")} + + +
+ )} {/* Loading Skeleton */} {isLoading && ( @@ -322,13 +314,8 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { {/* Role Assignment Cards */} {!isLoading && !hasError && hasAnyConfigs && ( - - {Object.entries(ROLE_DESCRIPTIONS).map(([key, role], index) => { +
+ {Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => { const IconComponent = role.icon; const isImageRole = role.configType === "image"; const currentAssignment = assignments[role.prefKey as keyof typeof assignments]; @@ -349,12 +336,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { assignedConfig && "is_auto_mode" in assignedConfig && assignedConfig.is_auto_mode; return ( - +
{/* Role Header */} @@ -542,47 +524,39 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { )} - +
); })} -
+
)} {/* Save / Reset Bar */} - - {hasChanges && ( - -

You have unsaved changes

-
- - -
-
- )} -
+ {hasChanges && ( +
+

You have unsaved changes

+
+ + +
+
+ )}
); } diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index ba3445aad..343fea1d2 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -12,7 +12,6 @@ import { Trash2, Wand2, } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; import Image from "next/image"; import { useCallback, useMemo, useState } from "react"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; @@ -51,6 +50,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import type { NewLLMConfig } from "@/contracts/types/new-llm-config.types"; +import { useMediaQuery } from "@/hooks/use-media-query"; import { getProviderIcon } from "@/lib/provider-icons"; import { cn } from "@/lib/utils"; @@ -58,21 +58,6 @@ interface ModelConfigManagerProps { searchSpaceId: number; } -const container = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - staggerChildren: 0.05, - }, - }, -}; - -const item = { - hidden: { opacity: 0, y: 20 }, - show: { opacity: 1, y: 0 }, -}; - function getInitials(name: string): string { const parts = name.trim().split(/\s+/); if (parts.length >= 2) { @@ -82,6 +67,7 @@ function getInitials(name: string): string { } export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { + const isDesktop = useMediaQuery("(min-width: 768px)"); // Mutations const { mutateAsync: createConfig, isPending: isCreating } = useAtomValue( createNewLLMConfigMutationAtom @@ -194,49 +180,42 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Header actions */}
+ + {canCreate && ( - {canCreate && ( - )}
{/* Fetch Error Alert */} - - {fetchError && ( - - - - - {fetchError?.message ?? "Failed to load configurations"} - - - - )} - + {fetchError && ( +
+ + + + {fetchError?.message ?? "Failed to load configurations"} + + +
+ )} {/* Read-only / Limited permissions notice */} {access && !isLoading && isReadOnly && ( - +
@@ -244,10 +223,10 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { Contact a space owner to request additional permissions. - +
)} {access && !isLoading && !isReadOnly && (!canCreate || !canUpdate || !canDelete) && ( - +
@@ -259,12 +238,12 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { {!canDelete && ", but cannot delete them"}. - +
)} {/* Global Configs Info */} {globalConfigs.length > 0 && ( - +
@@ -275,7 +254,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { - +
)} {/* Loading Skeleton */} @@ -317,7 +296,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { {!isLoading && (
{configs?.length === 0 ? ( - +
@@ -343,25 +322,14 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { )} - +
) : ( - - - {configs?.map((config) => { - const member = config.user_id ? memberMap.get(config.user_id) : null; +
+ {configs?.map((config) => { + const member = config.user_id ? memberMap.get(config.user_id) : null; - return ( - + return ( +
{/* Header: Name + Actions */} @@ -380,7 +348,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{canUpdate && ( - +
); })} - - +
)}
)} diff --git a/surfsense_web/components/settings/prompt-config-manager.tsx b/surfsense_web/components/settings/prompt-config-manager.tsx index 54058759f..91db93994 100644 --- a/surfsense_web/components/settings/prompt-config-manager.tsx +++ b/surfsense_web/components/settings/prompt-config-manager.tsx @@ -183,26 +183,27 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
- {/* Action Buttons */} -
- - -
+ {/* Action Buttons */} +
+ + +
{hasChanges && ( r.id === editingRoleId) : null; return ( - +
{canCreate && (
@@ -516,13 +508,8 @@ function RolesContent({ )}
- {roles.map((role, index) => ( - + {roles.map((role) => ( +
))}
- +
); } @@ -695,18 +682,11 @@ function PermissionsEditor({ return (
-
toggleCategoryExpanded(category)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - toggleCategoryExpanded(category); - } - }} - > +
-
+ {isExpanded && ( - +
{perms.map((perm) => { const action = perm.value.split(":")[1]; @@ -759,21 +735,14 @@ function PermissionsEditor({ const isSelected = selectedPermissions.includes(perm.value); return ( -
onTogglePermission(perm.value)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onTogglePermission(perm.value); - } - }} >
{actionLabel} @@ -787,11 +756,11 @@ function PermissionsEditor({ onClick={(e) => e.stopPropagation()} className="shrink-0" /> -
+ ); })}
- +
)}
); @@ -964,11 +933,11 @@ function CreateRoleDialog({ />
-
- - +
-
- +
+ + ))} +
+ + + {/* Mobile: Top header + horizontal tabs */} +
+
+

{title}

+
+
+
+ {navItems.map((item) => ( + + ))} +
+
+
+ + {/* Content area */} +
+
+

+ {navItems.find((i) => i.value === activeItem)?.label ?? title} +

+ +
+
+
{children}
+
+
+ + + ); +} diff --git a/surfsense_web/components/settings/user-settings-dialog.tsx b/surfsense_web/components/settings/user-settings-dialog.tsx new file mode 100644 index 000000000..426903a23 --- /dev/null +++ b/surfsense_web/components/settings/user-settings-dialog.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useAtom } from "jotai"; +import { KeyRound, User } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; +import { ApiKeyContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent"; +import { ProfileContent } from "@/app/dashboard/[search_space_id]/user-settings/components/ProfileContent"; +import { SettingsDialog } from "@/components/settings/settings-dialog"; + +export function UserSettingsDialog() { + const t = useTranslations("userSettings"); + const [state, setState] = useAtom(userSettingsDialogAtom); + + const navItems = [ + { value: "profile", label: t("profile_nav_label"), icon: }, + { + value: "api-key", + label: t("api_key_nav_label"), + icon: , + }, + ]; + + return ( + setState((prev) => ({ ...prev, open }))} + title={t("title")} + navItems={navItems} + activeItem={state.initialTab} + onItemChange={(tab) => setState((prev) => ({ ...prev, initialTab: tab }))} + > +
+ {state.initialTab === "profile" && } + {state.initialTab === "api-key" && } +
+
+ ); +}