diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index 5625d7450..814cf49f4 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -52,18 +52,6 @@ export default function DashboardLayout({ }, ], }, - { - title: "Team", - url: `/dashboard/${search_space_id}/team`, - icon: "Users", - items: [], - }, - { - title: "Settings", - url: `/dashboard/${search_space_id}/settings`, - icon: "Settings2", - items: [], - }, { title: "Logs", url: `/dashboard/${search_space_id}/logs`, diff --git a/surfsense_web/app/dashboard/[search_space_id]/settings/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/settings/layout.tsx new file mode 100644 index 000000000..929a5cb62 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/settings/layout.tsx @@ -0,0 +1,9 @@ +import type React from "react"; + +/** + * Settings layout - renders children directly without the parent sidebar + * This creates a full-screen settings experience + */ +export default function SettingsLayout({ children }: { children: React.ReactNode }) { + return
{children}
; +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx index 685a7baf4..dd68e1a18 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx @@ -1,83 +1,302 @@ "use client"; -import { ArrowLeft, Bot, Brain, MessageSquare, Settings } from "lucide-react"; +import { + ArrowLeft, + Bot, + Brain, + ChevronRight, + type LucideIcon, + Menu, + MessageSquare, + Settings, + X, +} from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; import { LLMRoleManager } from "@/components/settings/llm-role-manager"; import { ModelConfigManager } from "@/components/settings/model-config-manager"; import { PromptConfigManager } from "@/components/settings/prompt-config-manager"; -import { Separator } from "@/components/ui/separator"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface SettingsNavItem { + id: string; + label: string; + description: string; + icon: LucideIcon; +} + +const settingsNavItems: SettingsNavItem[] = [ + { + id: "models", + label: "Model Configs", + description: "Configure AI models and providers", + icon: Bot, + }, + { + id: "roles", + label: "LLM Roles", + description: "Manage language model roles", + icon: Brain, + }, + { + id: "prompts", + label: "System Instructions", + description: "Customize system prompts", + icon: MessageSquare, + }, +]; + +function SettingsSidebar({ + activeSection, + onSectionChange, + onBackToApp, + isOpen, + onClose, +}: { + activeSection: string; + onSectionChange: (section: string) => void; + onBackToApp: () => void; + isOpen: boolean; + onClose: () => void; +}) { + const handleNavClick = (sectionId: string) => { + onSectionChange(sectionId); + onClose(); // Close sidebar on mobile after selection + }; + + return ( + <> + {/* Mobile overlay */} + + {isOpen && ( + + )} + + + {/* Sidebar */} + + + ); +} + +function SettingsContent({ + activeSection, + searchSpaceId, + onMenuClick, +}: { + activeSection: string; + searchSpaceId: number; + onMenuClick: () => void; +}) { + const activeItem = settingsNavItems.find((item) => item.id === activeSection); + const Icon = activeItem?.icon || Settings; + + return ( + +
+
+ {/* Section Header */} + + +
+ {/* Mobile menu button */} + + + + +
+

+ {activeItem?.label} +

+

+ {activeItem?.description} +

+
+
+
+
+ + {/* Section Content */} + + + {activeSection === "models" && } + {activeSection === "roles" && } + {activeSection === "prompts" && } + + +
+
+
+ ); +} export default function SettingsPage() { const router = useRouter(); const params = useParams(); const searchSpaceId = Number(params.search_space_id); + const [activeSection, setActiveSection] = useState("models"); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + const handleBackToApp = useCallback(() => { + router.push(`/dashboard/${searchSpaceId}/researcher`); + }, [router, searchSpaceId]); return ( -
-
-
- {/* Header Section */} -
-
- {/* Back Button */} - -
- -
-
-

Settings

-

- Manage your settings for this search space. -

-
-
- -
- - {/* Settings Content */} - -
- - - - Model Configs - Models - - - - LLM Roles - Roles - - - - System Instructions - System Instructions - - -
- - - - - - - - - - - - -
-
-
-
+ + setIsSidebarOpen(false)} + /> + setIsSidebarOpen(true)} + /> + ); } diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index 0b3450d20..21dbce82b 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -255,7 +255,7 @@ const DashboardPage = () => { />
- + {space.name} {
diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index 00d0211df..eea7ed2d8 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -28,7 +28,12 @@ export function MarkdownViewer({ content, className }: MarkdownViewerProps) {

), a: ({ node, children, ...props }: any) => ( - + {children} ), diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx index 06b56b24f..c8057ef08 100644 --- a/surfsense_web/components/settings/llm-role-manager.tsx +++ b/surfsense_web/components/settings/llm-role-manager.tsx @@ -255,93 +255,89 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) { {/* Stats Overview */} {!isLoading && !hasError && ( -
- - -
-
-

{availableConfigs.length}

-

Available Models

-
- 🌐 {globalConfigs.length} Global - • {llmConfigs.length} Custom +
+ +
+ +
+
+

{availableConfigs.length}

+

Available Models

+
+ {globalConfigs.length} Global + {llmConfigs.length} Custom
-
- +
+
- - -
-
-

{assignedConfigIds.length}

-

Assigned Roles

+ +
+ +
+
+

{assignedConfigIds.length}

+

Assigned Roles

-
- +
+
- - -
-
-

+ +

+ +
+
+

{Math.round((assignedConfigIds.length / 3) * 100)}%

-

Completion

+

Completion

{isAssignmentComplete ? ( - + ) : ( - + )}
- - -
-
+ +
+ +
+

{isAssignmentComplete ? "Ready" : "Setup"}

-

Status

+

Status

{isAssignmentComplete ? ( - + ) : ( - + )}
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index 3bd871135..abdde04e3 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -8,8 +8,6 @@ import { ChevronsUpDown, Clock, Edit3, - Eye, - EyeOff, Loader2, Plus, RefreshCw, @@ -20,6 +18,16 @@ import { AnimatePresence, motion } from "motion/react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -77,7 +85,6 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { const { globalConfigs } = useGlobalLLMConfigs(); const [isAddingNew, setIsAddingNew] = useState(false); const [editingConfig, setEditingConfig] = useState(null); - const [showApiKey, setShowApiKey] = useState>({}); const [formData, setFormData] = useState({ name: "", provider: "", @@ -91,6 +98,8 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { }); const [isSubmitting, setIsSubmitting] = useState(false); const [modelComboboxOpen, setModelComboboxOpen] = useState(false); + const [configToDelete, setConfigToDelete] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); // Populate form when editing useEffect(() => { @@ -162,19 +171,22 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { } }; - const handleDelete = async (id: number) => { - if ( - confirm("Are you sure you want to delete this configuration? This action cannot be undone.") - ) { - await deleteLLMConfig(id); - } + const handleDeleteClick = (config: LLMConfig) => { + setConfigToDelete(config); }; - const toggleApiKeyVisibility = (configId: number) => { - setShowApiKey((prev) => ({ - ...prev, - [configId]: !prev[configId], - })); + const handleConfirmDelete = async () => { + if (!configToDelete) return; + setIsDeleting(true); + try { + await deleteLLMConfig(configToDelete.id); + toast.success("Configuration deleted successfully"); + } catch (error) { + toast.error("Failed to delete configuration"); + } finally { + setIsDeleting(false); + setConfigToDelete(null); + } }; const selectedProvider = LLM_PROVIDERS.find((p) => p.value === formData.provider); @@ -184,13 +196,6 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { return LLM_PROVIDERS.find((p) => p.value === providerValue); }; - const maskApiKey = (apiKey: string) => { - if (apiKey.length <= 8) return "*".repeat(apiKey.length); - return ( - apiKey.substring(0, 4) + "*".repeat(apiKey.length - 8) + apiKey.substring(apiKey.length - 4) - ); - }; - return (
{/* Header */} @@ -258,46 +263,49 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { {/* Stats Overview */} {!loading && !error && ( -
- - -
-
-

{llmConfigs.length}

-

Total Configurations

+
+ +
+ +
+
+

{llmConfigs.length}

+

Total Configs

-
- +
+
- - -
-
-

+ +

+ +
+
+

{new Set(llmConfigs.map((c) => c.provider)).size}

-

Unique Providers

+

Providers

-
- +
+
- - -
-
-

Active

-

System Status

+ +
+ +
+
+

Active

+

Status

-
- +
+
@@ -352,119 +360,91 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.2 }} > - - -
-
- {/* Header */} -
-
- -
-
-
-

- {config.name} -

- - {config.provider} - + + +
+ {/* Left accent bar */} +
+ +
+
+ {/* Main content */} +
+
+
-

- {config.model_name} -

- {config.language && ( -
- - {config.language} - +
+ {/* Title row */} +
+

+ {config.name} +

+
+ + {config.provider} + + {config.language && ( + + {config.language} + + )} +
- )} -
-
- {/* Provider Description */} - {providerInfo && ( -

- {providerInfo.description} -

- )} + {/* Model name */} +
+ + {config.model_name} + +
- {/* Configuration Details */} -
-
- -
- - {showApiKey[config.id] - ? config.api_key - : maskApiKey(config.api_key)} - - +
+
+ + Active + +
+
- {config.api_base && ( -
- - - {config.api_base} - -
- )} -
- - {/* Metadata */} -
- {config.created_at && ( -
- - - Created {new Date(config.created_at).toLocaleDateString()} - -
- )} -
-
- Active + {/* Actions */} +
+ +
- - {/* Actions */} -
- - -
@@ -803,6 +783,46 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { + + {/* Delete Confirmation Dialog */} + !open && setConfigToDelete(null)} + > + + + + + Delete Configuration + + + Are you sure you want to delete{" "} + {configToDelete?.name}? This + action cannot be undone and will permanently remove this model configuration. + + + + Cancel + + {isDeleting ? ( + <> + + Deleting... + + ) : ( + <> + + Delete + + )} + + + +
); } diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index e100a4c33..76d92ba3b 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -193,6 +193,7 @@ export function AppSidebarProvider({ if (!isClient) { return ( = 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); +} + +/** + * Dynamic avatar component that generates an SVG based on email + */ +function UserAvatar({ email, size = 32 }: { email: string; size?: number }) { + const bgColor = stringToColor(email); + const initials = getInitials(email); + + return ( + + Avatar for {email} + + + {initials} + + + ); +} -import { Logo } from "@/components/Logo"; import { NavMain } from "@/components/sidebar/nav-main"; import { NavProjects } from "@/components/sidebar/nav-projects"; import { NavSecondary } from "@/components/sidebar/nav-secondary"; @@ -122,6 +212,7 @@ const defaultData = { }; interface AppSidebarProps extends React.ComponentProps { + searchSpaceId?: string; navMain?: { title: string; url: string; @@ -162,12 +253,22 @@ interface AppSidebarProps extends React.ComponentProps { // Memoized AppSidebar component for better performance export const AppSidebar = memo(function AppSidebar({ + searchSpaceId, navMain = defaultData.navMain, navSecondary = defaultData.navSecondary, RecentChats = defaultData.RecentChats, pageUsage, ...props }: AppSidebarProps) { + const router = useRouter(); + const { theme, setTheme } = useTheme(); + const { user, loading: isLoadingUser } = useUser(); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + // Process navMain to resolve icon names to components const processedNavMain = useMemo(() => { return navMain.map((item) => ({ @@ -194,28 +295,111 @@ export const AppSidebar = memo(function AppSidebar({ ); }, [RecentChats]); + // Get user display name from email + const userDisplayName = user?.email ? user.email.split("@")[0] : "User"; + const userEmail = user?.email || (isLoadingUser ? "Loading..." : "Unknown"); + + const handleLogout = () => { + try { + if (typeof window !== "undefined") { + localStorage.removeItem("surfsense_bearer_token"); + router.push("/"); + } + } catch (error) { + console.error("Error during logout:", error); + router.push("/"); + } + }; + return ( - - -
- SurfSense logo -
-
- SurfSense - beta v0.0.8 -
- -
+ + + +
+ {user?.email ? ( + + ) : ( +
+ )} +
+
+ {userDisplayName} + {userEmail} +
+ + + + + +
+
+ {user?.email ? ( + + ) : ( +
+ )} +
+
+ {userDisplayName} + {userEmail} +
+
+ + + + {searchSpaceId && ( + <> + router.push(`/dashboard/${searchSpaceId}/settings`)} + > + + Settings + + router.push(`/dashboard/${searchSpaceId}/team`)} + > + + Invite members + + + )} + router.push("/dashboard")}> + + Switch workspace + + + + + {isClient && ( + setTheme(theme === "dark" ? "light" : "dark")}> + {theme === "dark" ? ( + + ) : ( + + )} + {theme === "dark" ? "Light mode" : "Dark mode"} + + )} + + + + + Logout + + +