-
router.push(`/dashboard/${searchSpaceId}`)}
- className="flex items-center justify-center h-9 w-9 md:h-10 md:w-10 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors shrink-0"
- aria-label="Back to Dashboard"
- type="button"
- >
-
-
diff --git a/surfsense_web/app/dashboard/api-key/api-key-client.tsx b/surfsense_web/app/dashboard/api-key/api-key-client.tsx
deleted file mode 100644
index 9163b52d8..000000000
--- a/surfsense_web/app/dashboard/api-key/api-key-client.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-"use client";
-
-import { IconCheck, IconCopy, IconKey } from "@tabler/icons-react";
-import { ArrowLeft } from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
-import { useRouter } from "next/navigation";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
-import { useApiKey } from "@/hooks/use-api-key";
-
-const fadeIn = {
- hidden: { opacity: 0 },
- visible: { opacity: 1, transition: { duration: 0.4 } },
-};
-
-const staggerContainer = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.1,
- },
- },
-};
-
-const ApiKeyClient = () => {
- const { apiKey, isLoading, copied, copyToClipboard } = useApiKey();
- const router = useRouter();
- return (
-
-
-
- API Key
-
- Your API key for authenticating with the SurfSense API.
-
-
-
-
-
-
- Important
-
- Your API key grants full access to your account. Never share it publicly or with
- unauthorized users.
-
-
-
-
-
-
-
- Your API Key
- Use this key to authenticate your API requests.
-
-
-
- {isLoading ? (
-
- ) : apiKey ? (
-
-
-
- {apiKey}
-
-
-
-
-
-
-
- {copied ? (
-
- ) : (
-
- )}
-
-
-
-
- {copied ? "Copied!" : "Copy to clipboard"}
-
-
-
-
- ) : (
-
- No API key found.
-
- )}
-
-
-
-
-
-
- How to use your API key
-
-
-
-
- Authentication
-
- Include your API key in the Authorization header of your requests:
-
-
-
- Authorization: Bearer {apiKey || "YOUR_API_KEY"}
-
-
-
-
-
-
-
-
-
-
router.push("/dashboard")}
- className="flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 hover:bg-primary/30 transition-colors"
- aria-label="Back to Dashboard"
- type="button"
- >
-
-
-
-
- );
-};
-
-export default ApiKeyClient;
diff --git a/surfsense_web/app/dashboard/api-key/client-wrapper.tsx b/surfsense_web/app/dashboard/api-key/client-wrapper.tsx
deleted file mode 100644
index 4397005ef..000000000
--- a/surfsense_web/app/dashboard/api-key/client-wrapper.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-"use client";
-
-import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
-
-// Loading component with animation
-const LoadingComponent = () => (
-
-
-
Loading API Key Management...
-
-);
-
-// Dynamically import the ApiKeyClient component
-const ApiKeyClient = dynamic(() => import("./api-key-client"), {
- ssr: false,
- loading: () =>
,
-});
-
-export default function ClientWrapper() {
- const [isMounted, setIsMounted] = useState(false);
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- if (!isMounted) {
- return
;
- }
-
- return
;
-}
diff --git a/surfsense_web/app/dashboard/api-key/page.tsx b/surfsense_web/app/dashboard/api-key/page.tsx
deleted file mode 100644
index 26e0560de..000000000
--- a/surfsense_web/app/dashboard/api-key/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-"use client";
-
-import ClientWrapper from "./client-wrapper";
-
-export default function ApiKeyPage() {
- return
;
-}
diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx
index ad1c6ad9d..3e6d71829 100644
--- a/surfsense_web/app/dashboard/page.tsx
+++ b/surfsense_web/app/dashboard/page.tsx
@@ -1,32 +1,14 @@
"use client";
import { useAtomValue } from "jotai";
-import { AlertCircle, Loader2, Plus, Search, Trash2, UserCheck, Users } from "lucide-react";
-import { motion, type Variants } from "motion/react";
-import Image from "next/image";
-import Link from "next/link";
+import { AlertCircle, Loader2, Plus, Search } from "lucide-react";
+import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
-import { useEffect } from "react";
-import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
+import { useEffect, useState } from "react";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
-import { currentUserAtom } from "@/atoms/user/user-query.atoms";
-import { Logo } from "@/components/Logo";
-import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
-import { UserDropdown } from "@/components/UserDropdown";
+import { CreateSearchSpaceDialog } from "@/components/layout";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -36,29 +18,11 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
-import { Spotlight } from "@/components/ui/spotlight";
-import { Tilt } from "@/components/ui/tilt";
-/**
- * Formats a date string into a readable format
- * @param dateString - The date string to format
- * @returns Formatted date string (e.g., "Jan 1, 2023")
- */
-const formatDate = (dateString: string): string => {
- return new Date(dateString).toLocaleDateString("en-US", {
- year: "numeric",
- month: "short",
- day: "numeric",
- });
-};
-
-/**
- * Loading screen component with animation
- */
-const LoadingScreen = () => {
+function LoadingScreen() {
const t = useTranslations("dashboard");
return (
-
+
{
@@ -84,23 +48,20 @@ const LoadingScreen = () => {
);
-};
+}
-/**
- * Error screen component with animation
- */
-const ErrorScreen = ({ message }: { message: string }) => {
+function ErrorScreen({ message }: { message: string }) {
const t = useTranslations("dashboard");
const router = useRouter();
return (
-
+
-
+
@@ -109,7 +70,7 @@ const ErrorScreen = ({ message }: { message: string }) => {
{t("something_wrong")}
-
+
{t("error_details")}
{message}
@@ -125,269 +86,68 @@ const ErrorScreen = ({ message }: { message: string }) => {
);
-};
+}
-const DashboardPage = () => {
- const t = useTranslations("dashboard");
- const tCommon = useTranslations("common");
+function EmptyState({ onCreateClick }: { onCreateClick: () => void }) {
+ const t = useTranslations("searchSpace");
+
+ return (
+
+
+
+
+
+
+
+
{t("welcome_title")}
+
+ {t("welcome_description")}
+
+
+
+
+
+ {t("create_first_button")}
+
+
+
+ );
+}
+
+export default function DashboardPage() {
const router = useRouter();
-
- // Animation variants
- const containerVariants: Variants = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: {
- staggerChildren: 0.1,
- },
- },
- };
-
- const itemVariants: Variants = {
- hidden: { y: 20, opacity: 0 },
- visible: {
- y: 0,
- opacity: 1,
- transition: {
- type: "spring",
- stiffness: 300,
- damping: 24,
- },
- },
- };
+ const [showCreateDialog, setShowCreateDialog] = useState(false);
const {
data: searchSpaces = [],
- isLoading: loading,
+ isLoading,
error,
- refetch: refreshSearchSpaces,
} = useAtomValue(searchSpacesAtom);
- const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
- const { data: user, isPending: isLoadingUser, error: userError } = useAtomValue(currentUserAtom);
-
- // Auto-redirect to chat for users with exactly 1 search space
useEffect(() => {
- if (loading) return;
+ if (isLoading) return;
- if (searchSpaces.length === 1) {
+ if (searchSpaces.length > 0) {
router.replace(`/dashboard/${searchSpaces[0].id}/new-chat`);
}
- }, [loading, searchSpaces, router]);
+ }, [isLoading, searchSpaces, router]);
- // Create user object for UserDropdown
- const customUser = {
- name: user?.email ? user.email.split("@")[0] : "User",
- email:
- user?.email ||
- (isLoadingUser ? "Loading..." : userError ? "Error loading user" : "Unknown User"),
- avatar: "/icon-128.svg", // Default avatar
- };
-
- // Show loading while loading or auto-redirecting (single search space)
- if (loading || (searchSpaces.length === 1 && !error)) return ;
+ if (isLoading) return ;
if (error) return ;
- const handleDeleteSearchSpace = async (id: number) => {
- await deleteSearchSpace({ id });
- refreshSearchSpaces();
- };
+ if (searchSpaces.length > 0) {
+ return ;
+ }
return (
-
-
-
-
-
-
-
{t("surfsense_dashboard")}
-
{t("welcome_message")}
-
-
-
-
-
-
-
-
-
-
-
{t("your_search_spaces")}
-
-
-
-
- {t("create_search_space")}
-
-
-
-
-
-
- {searchSpaces &&
- searchSpaces.length > 0 &&
- searchSpaces.map((space) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t("delete_search_space")}
-
- {t("delete_space_confirm", { name: space.name })}
-
-
-
- {tCommon("cancel")}
- handleDeleteSearchSpace(space.id)}
- className="bg-destructive hover:bg-destructive/90"
- >
- {tCommon("delete")}
-
-
-
-
-
-
-
-
-
-
-
-
{space.name}
- {!space.is_owner && (
-
- {t("shared")}
-
- )}
-
-
- {space.description}
-
-
-
-
- {t("created")} {formatDate(space.created_at)}
-
-
- {space.is_owner ? (
-
- ) : (
-
- )}
- {space.member_count}
-
-
-
-
-
-
-
- ))}
-
- {searchSpaces.length === 0 && (
-
-
-
-
- {t("no_spaces_found")}
-
- {t("create_first_space")}
-
-
-
-
- {t("create_search_space")}
-
-
-
- )}
-
- {searchSpaces.length > 0 && (
-
-
-
-
-
-
- {t("add_new_search_space")}
-
-
-
-
-
- )}
-
-
-
-
+ <>
+ setShowCreateDialog(true)} />
+
+ >
);
-};
-
-export default DashboardPage;
+}
diff --git a/surfsense_web/app/dashboard/searchspaces/page.tsx b/surfsense_web/app/dashboard/searchspaces/page.tsx
deleted file mode 100644
index b40eb5d82..000000000
--- a/surfsense_web/app/dashboard/searchspaces/page.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-"use client";
-
-import { useAtomValue } from "jotai";
-import { motion } from "motion/react";
-import { useRouter } from "next/navigation";
-import { createSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
-import { SearchSpaceForm } from "@/components/search-space-form";
-import { trackSearchSpaceCreated } from "@/lib/posthog/events";
-
-export default function SearchSpacesPage() {
- const router = useRouter();
- const { mutateAsync: createSearchSpace } = useAtomValue(createSearchSpaceMutationAtom);
-
- const handleCreateSearchSpace = async (data: { name: string; description?: string }) => {
- const result = await createSearchSpace({
- name: data.name,
- description: data.description || "",
- });
-
- // Track search space creation
- trackSearchSpaceCreated(result.id, data.name);
-
- // Redirect to the newly created search space's onboarding
- router.push(`/dashboard/${result.id}/onboard`);
-
- return result;
- };
-
- return (
-
-
-
-
-
- );
-}
diff --git a/surfsense_web/app/dashboard/user/settings/page.tsx b/surfsense_web/app/dashboard/user/settings/page.tsx
new file mode 100644
index 000000000..5b0ac5fa0
--- /dev/null
+++ b/surfsense_web/app/dashboard/user/settings/page.tsx
@@ -0,0 +1,328 @@
+"use client";
+
+import {
+ ArrowLeft,
+ Check,
+ ChevronRight,
+ Copy,
+ Key,
+ type LucideIcon,
+ Menu,
+ Shield,
+ X,
+} from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { useCallback, useState } from "react";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { useApiKey } from "@/hooks/use-api-key";
+import { cn } from "@/lib/utils";
+
+interface SettingsNavItem {
+ id: string;
+ label: string;
+ description: string;
+ icon: LucideIcon;
+}
+
+function UserSettingsSidebar({
+ activeSection,
+ onSectionChange,
+ onBackToApp,
+ isOpen,
+ onClose,
+ navItems,
+}: {
+ activeSection: string;
+ onSectionChange: (section: string) => void;
+ onBackToApp: () => void;
+ isOpen: boolean;
+ onClose: () => void;
+ navItems: SettingsNavItem[];
+}) {
+ const t = useTranslations("userSettings");
+
+ const handleNavClick = (sectionId: string) => {
+ onSectionChange(sectionId);
+ onClose();
+ };
+
+ return (
+ <>
+
+ {isOpen && (
+
+ )}
+
+
+
+
+
+
+ {t("back_to_app")}
+
+
+
+
+
+
+
+ {navItems.map((item, index) => {
+ const isActive = activeSection === item.id;
+ const Icon = item.icon;
+
+ return (
+ handleNavClick(item.id)}
+ whileHover={{ scale: 1.01 }}
+ whileTap={{ scale: 0.99 }}
+ className={cn(
+ "relative flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left transition-all duration-200",
+ isActive ? "border border-border bg-muted shadow-sm" : "hover:bg-muted/60"
+ )}
+ >
+ {isActive && (
+
+ )}
+
+
+
+
+
+ {item.label}
+
+
{item.description}
+
+
+
+ );
+ })}
+
+
+
+
+ >
+ );
+}
+
+function ApiKeyContent({ onMenuClick }: { onMenuClick: () => void }) {
+ const t = useTranslations("userSettings");
+ const { apiKey, isLoading, copied, copyToClipboard } = useApiKey();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("api_key_title")}
+
+
{t("api_key_description")}
+
+
+
+
+
+
+
+
+
+ {t("api_key_warning_title")}
+ {t("api_key_warning_description")}
+
+
+
+
{t("your_api_key")}
+ {isLoading ? (
+
+ ) : apiKey ? (
+
+
+ {apiKey}
+
+
+
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+ {copied ? t("copied") : t("copy")}
+
+
+
+
+ ) : (
+
{t("no_api_key")}
+ )}
+
+
+
+
{t("usage_title")}
+
{t("usage_description")}
+
+ Authorization: Bearer {apiKey || "YOUR_API_KEY"}
+
+
+
+
+
+
+
+ );
+}
+
+export default function UserSettingsPage() {
+ const t = useTranslations("userSettings");
+ const router = useRouter();
+ const [activeSection, setActiveSection] = useState("api-key");
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+
+ const navItems: SettingsNavItem[] = [
+ {
+ id: "api-key",
+ label: t("api_key_nav_label"),
+ description: t("api_key_nav_description"),
+ icon: Key,
+ },
+ ];
+
+ const handleBackToApp = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ return (
+
+
+
+
setIsSidebarOpen(false)}
+ navItems={navItems}
+ />
+ {activeSection === "api-key" && (
+ setIsSidebarOpen(true)} />
+ )}
+
+
+
+ );
+}
diff --git a/surfsense_web/components/layout/index.ts b/surfsense_web/components/layout/index.ts
index 745075b6f..b9c271915 100644
--- a/surfsense_web/components/layout/index.ts
+++ b/surfsense_web/components/layout/index.ts
@@ -8,10 +8,12 @@ export type {
PageUsage,
SidebarSectionProps,
User,
- Workspace,
+ SearchSpace,
} from "./types/layout.types";
export {
+ AllSearchSpacesSheet,
ChatListItem,
+ CreateSearchSpaceDialog,
Header,
IconRail,
LayoutShell,
@@ -26,5 +28,5 @@ export {
SidebarHeader,
SidebarSection,
SidebarUserProfile,
- WorkspaceAvatar,
+ SearchSpaceAvatar,
} from "./ui";
diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
index ea750a365..8f42e22aa 100644
--- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
@@ -8,6 +8,7 @@ import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
import { useCallback, useMemo, useState } from "react";
import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms";
+import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { Button } from "@/components/ui/button";
@@ -25,8 +26,10 @@ import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { deleteThread, fetchThreads } from "@/lib/chat/thread-persistence";
import { resetUser, trackLogout } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys";
-import type { ChatItem, NavItem, NoteItem, Workspace } from "../types/layout.types";
+import type { ChatItem, NavItem, NoteItem, SearchSpace } from "../types/layout.types";
+import { CreateSearchSpaceDialog } from "../ui/dialogs";
import { LayoutShell } from "../ui/shell";
+import { AllSearchSpacesSheet } from "../ui/sheets";
import { AllChatsSidebar } from "../ui/sidebar/AllChatsSidebar";
import { AllNotesSidebar } from "../ui/sidebar/AllNotesSidebar";
@@ -53,7 +56,8 @@ export function LayoutDataProvider({
// Atoms
const { data: user } = useAtomValue(currentUserAtom);
- const { data: searchSpacesData } = useAtomValue(searchSpacesAtom);
+ const { data: searchSpacesData, refetch: refetchSearchSpaces } = useAtomValue(searchSpacesAtom);
+ const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
const hasUnsavedEditorChanges = useAtomValue(hasUnsavedEditorChangesAtom);
const setPendingNavigation = useSetAtom(pendingEditorNavigationAtom);
@@ -110,6 +114,10 @@ export function LayoutDataProvider({
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
+ // Search space sheet and dialog state
+ const [isAllSearchSpacesSheetOpen, setIsAllSearchSpacesSheetOpen] = useState(false);
+ const [isCreateSearchSpaceDialogOpen, setIsCreateSearchSpaceDialogOpen] = useState(false);
+
// Delete dialogs state
const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
@@ -123,8 +131,7 @@ export function LayoutDataProvider({
} | null>(null);
const [isDeletingNote, setIsDeletingNote] = useState(false);
- // Transform workspaces (API returns array directly, not { items: [...] })
- const workspaces: Workspace[] = useMemo(() => {
+ const searchSpaces: SearchSpace[] = useMemo(() => {
if (!searchSpacesData || !Array.isArray(searchSpacesData)) return [];
return searchSpacesData.map((space) => ({
id: space.id,
@@ -132,19 +139,15 @@ export function LayoutDataProvider({
description: space.description,
isOwner: space.is_owner,
memberCount: space.member_count || 0,
+ createdAt: space.created_at,
}));
}, [searchSpacesData]);
- // Use searchSpace query result for current workspace (more reliable than finding in list)
- const activeWorkspace: Workspace | null = searchSpace
- ? {
- id: searchSpace.id,
- name: searchSpace.name,
- description: searchSpace.description,
- isOwner: searchSpace.is_owner,
- memberCount: searchSpace.member_count || 0,
- }
- : null;
+ // Find active search space from list (has is_owner and member_count)
+ const activeSearchSpace: SearchSpace | null = useMemo(() => {
+ if (!searchSpaceId || !searchSpaces.length) return null;
+ return searchSpaces.find((s) => s.id === Number(searchSpaceId)) ?? null;
+ }, [searchSpaceId, searchSpaces]);
// Transform chats
const chats: ChatItem[] = useMemo(() => {
@@ -196,20 +199,47 @@ export function LayoutDataProvider({
);
// Handlers
- const handleWorkspaceSelect = useCallback(
+ const handleSearchSpaceSelect = useCallback(
(id: number) => {
router.push(`/dashboard/${id}/new-chat`);
},
[router]
);
- const handleAddWorkspace = useCallback(() => {
- router.push("/dashboard/searchspaces");
+ const handleAddSearchSpace = useCallback(() => {
+ setIsCreateSearchSpaceDialogOpen(true);
+ }, []);
+
+ const handleSeeAllSearchSpaces = useCallback(() => {
+ setIsAllSearchSpacesSheetOpen(true);
+ }, []);
+
+ const handleUserSettings = useCallback(() => {
+ router.push("/dashboard/user/settings");
}, [router]);
- const handleSeeAllWorkspaces = useCallback(() => {
- router.push("/dashboard");
- }, [router]);
+ const handleSearchSpaceSettings = useCallback(
+ (id: number) => {
+ router.push(`/dashboard/${id}/settings`);
+ },
+ [router]
+ );
+
+ const handleDeleteSearchSpace = useCallback(
+ async (id: number) => {
+ await deleteSearchSpace({ id });
+ refetchSearchSpaces();
+ if (Number(searchSpaceId) === id && searchSpaces.length > 1) {
+ const remaining = searchSpaces.filter((s) => s.id !== id);
+ if (remaining.length > 0) {
+ router.push(`/dashboard/${remaining[0].id}/new-chat`);
+ }
+ } else if (searchSpaces.length === 1) {
+ router.push("/dashboard");
+ }
+ },
+ [deleteSearchSpace, refetchSearchSpaces, searchSpaceId, searchSpaces, router]
+ );
const handleNavItemClick = useCallback(
(item: NavItem) => {
@@ -266,7 +296,7 @@ export function LayoutDataProvider({
router.push(`/dashboard/${searchSpaceId}/settings`);
}, [router, searchSpaceId]);
- const handleInviteMembers = useCallback(() => {
+ const handleManageMembers = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/team`);
}, [router, searchSpaceId]);
@@ -347,11 +377,11 @@ export function LayoutDataProvider({
return (
<>
+ {/* All Search Spaces Sheet */}
+ {
+ setIsAllSearchSpacesSheetOpen(false);
+ setIsCreateSearchSpaceDialogOpen(true);
+ }}
+ onSettings={handleSearchSpaceSettings}
+ onDelete={handleDeleteSearchSpace}
+ />
+
+ {/* Create Search Space Dialog */}
+
+
{/* Delete Note Dialog */}
diff --git a/surfsense_web/components/layout/types/layout.types.ts b/surfsense_web/components/layout/types/layout.types.ts
index b11619c60..34598b43e 100644
--- a/surfsense_web/components/layout/types/layout.types.ts
+++ b/surfsense_web/components/layout/types/layout.types.ts
@@ -1,11 +1,12 @@
import type { LucideIcon } from "lucide-react";
-export interface Workspace {
+export interface SearchSpace {
id: number;
name: string;
description?: string | null;
isOwner: boolean;
memberCount: number;
+ createdAt?: string;
}
export interface User {
@@ -42,15 +43,15 @@ export interface PageUsage {
}
export interface IconRailProps {
- workspaces: Workspace[];
- activeWorkspaceId: number | null;
- onWorkspaceSelect: (id: number) => void;
- onAddWorkspace: () => void;
+ searchSpaces: SearchSpace[];
+ activeSearchSpaceId: number | null;
+ onSearchSpaceSelect: (id: number) => void;
+ onAddSearchSpace: () => void;
className?: string;
}
export interface SidebarHeaderProps {
- workspace: Workspace | null;
+ searchSpace: SearchSpace | null;
onSettings?: () => void;
}
@@ -94,15 +95,15 @@ export interface SidebarUserProfileProps {
user: User;
searchSpaceId?: string;
onSettings?: () => void;
- onInviteMembers?: () => void;
- onSwitchWorkspace?: () => void;
+ onManageMembers?: () => void;
+ onSwitchSearchSpace?: () => void;
onToggleTheme?: () => void;
onLogout?: () => void;
theme?: string;
}
export interface SidebarProps {
- workspace: Workspace | null;
+ searchSpace: SearchSpace | null;
searchSpaceId?: string;
navItems: NavItem[];
chats: ChatItem[];
@@ -120,8 +121,8 @@ export interface SidebarProps {
user: User;
theme?: string;
onSettings?: () => void;
- onInviteMembers?: () => void;
- onSwitchWorkspace?: () => void;
+ onManageMembers?: () => void;
+ onSeeAllSearchSpaces?: () => void;
onToggleTheme?: () => void;
onLogout?: () => void;
pageUsage?: PageUsage;
@@ -129,10 +130,10 @@ export interface SidebarProps {
}
export interface LayoutShellProps {
- workspaces: Workspace[];
- activeWorkspaceId: number | null;
- onWorkspaceSelect: (id: number) => void;
- onAddWorkspace: () => void;
+ searchSpaces: SearchSpace[];
+ activeSearchSpaceId: number | null;
+ onSearchSpaceSelect: (id: number) => void;
+ onAddSearchSpace: () => void;
sidebarProps: Omit;
children: React.ReactNode;
className?: string;
diff --git a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
new file mode 100644
index 000000000..978d46f6c
--- /dev/null
+++ b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
@@ -0,0 +1,166 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useAtomValue } from "jotai";
+import { Loader2, Plus, Search } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+import { createSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { trackSearchSpaceCreated } from "@/lib/posthog/events";
+
+const formSchema = z.object({
+ name: z.string().min(1, "Name is required"),
+ description: z.string().optional(),
+});
+
+type FormValues = z.infer;
+
+interface CreateSearchSpaceDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpaceDialogProps) {
+ const t = useTranslations("searchSpace");
+ const tCommon = useTranslations("common");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { mutateAsync: createSearchSpace } = useAtomValue(createSearchSpaceMutationAtom);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: "",
+ description: "",
+ },
+ });
+
+ const handleSubmit = async (values: FormValues) => {
+ setIsSubmitting(true);
+ try {
+ const result = await createSearchSpace({
+ name: values.name,
+ description: values.description || "",
+ });
+
+ trackSearchSpaceCreated(result.id, values.name);
+
+ // Hard redirect to ensure fresh state
+ window.location.href = `/dashboard/${result.id}/onboard`;
+ } catch (error) {
+ console.error("Failed to create search space:", error);
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleOpenChange = (newOpen: boolean) => {
+ if (!newOpen) {
+ form.reset();
+ }
+ onOpenChange(newOpen);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {t("create_title")}
+ {t("create_description")}
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/surfsense_web/components/layout/ui/dialogs/index.ts b/surfsense_web/components/layout/ui/dialogs/index.ts
new file mode 100644
index 000000000..28f3b387d
--- /dev/null
+++ b/surfsense_web/components/layout/ui/dialogs/index.ts
@@ -0,0 +1,2 @@
+export { CreateSearchSpaceDialog } from "./CreateSearchSpaceDialog";
+
diff --git a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
index 0d6b39cdc..3e8b14ba9 100644
--- a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
+++ b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx
@@ -5,34 +5,34 @@ 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 { Workspace } from "../../types/layout.types";
-import { WorkspaceAvatar } from "./WorkspaceAvatar";
+import type { SearchSpace } from "../../types/layout.types";
+import { SearchSpaceAvatar } from "./SearchSpaceAvatar";
interface IconRailProps {
- workspaces: Workspace[];
- activeWorkspaceId: number | null;
- onWorkspaceSelect: (id: number) => void;
- onAddWorkspace: () => void;
+ searchSpaces: SearchSpace[];
+ activeSearchSpaceId: number | null;
+ onSearchSpaceSelect: (id: number) => void;
+ onAddSearchSpace: () => void;
className?: string;
}
export function IconRail({
- workspaces,
- activeWorkspaceId,
- onWorkspaceSelect,
- onAddWorkspace,
+ searchSpaces,
+ activeSearchSpaceId,
+ onSearchSpaceSelect,
+ onAddSearchSpace,
className,
}: IconRailProps) {
return (
- {workspaces.map((workspace) => (
-
onWorkspaceSelect(workspace.id)}
+ {searchSpaces.map((searchSpace) => (
+ onSearchSpaceSelect(searchSpace.id)}
size="md"
/>
))}
@@ -42,15 +42,15 @@ export function IconRail({
- Add workspace
+ Add search space
- Add workspace
+ Add search space
diff --git a/surfsense_web/components/layout/ui/icon-rail/WorkspaceAvatar.tsx b/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
similarity index 86%
rename from surfsense_web/components/layout/ui/icon-rail/WorkspaceAvatar.tsx
rename to surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
index 1c4798d2a..397076cb6 100644
--- a/surfsense_web/components/layout/ui/icon-rail/WorkspaceAvatar.tsx
+++ b/surfsense_web/components/layout/ui/icon-rail/SearchSpaceAvatar.tsx
@@ -3,7 +3,7 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
-interface WorkspaceAvatarProps {
+interface SearchSpaceAvatarProps {
name: string;
isActive?: boolean;
onClick?: () => void;
@@ -11,7 +11,7 @@ interface WorkspaceAvatarProps {
}
/**
- * Generates a consistent color based on workspace name
+ * Generates a consistent color based on search space name
*/
function stringToColor(str: string): string {
let hash = 0;
@@ -32,7 +32,7 @@ function stringToColor(str: string): string {
}
/**
- * Gets initials from workspace name (max 2 chars)
+ * Gets initials from search space name (max 2 chars)
*/
function getInitials(name: string): string {
const words = name.trim().split(/\s+/);
@@ -42,7 +42,7 @@ function getInitials(name: string): string {
return name.slice(0, 2).toUpperCase();
}
-export function WorkspaceAvatar({ name, isActive, onClick, size = "md" }: WorkspaceAvatarProps) {
+export function SearchSpaceAvatar({ name, isActive, onClick, size = "md" }: SearchSpaceAvatarProps) {
const bgColor = stringToColor(name);
const initials = getInitials(name);
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
diff --git a/surfsense_web/components/layout/ui/icon-rail/index.ts b/surfsense_web/components/layout/ui/icon-rail/index.ts
index 0e7e8cd29..b635e7273 100644
--- a/surfsense_web/components/layout/ui/icon-rail/index.ts
+++ b/surfsense_web/components/layout/ui/icon-rail/index.ts
@@ -1,3 +1,3 @@
export { IconRail } from "./IconRail";
export { NavIcon } from "./NavIcon";
-export { WorkspaceAvatar } from "./WorkspaceAvatar";
+export { SearchSpaceAvatar } from "./SearchSpaceAvatar";
diff --git a/surfsense_web/components/layout/ui/index.ts b/surfsense_web/components/layout/ui/index.ts
index 74b1e9240..c5aba9250 100644
--- a/surfsense_web/components/layout/ui/index.ts
+++ b/surfsense_web/components/layout/ui/index.ts
@@ -1,6 +1,8 @@
+export { CreateSearchSpaceDialog } from "./dialogs";
export { Header } from "./header";
-export { IconRail, NavIcon, WorkspaceAvatar } from "./icon-rail";
+export { IconRail, NavIcon, SearchSpaceAvatar } from "./icon-rail";
export { LayoutShell } from "./shell";
+export { AllSearchSpacesSheet } from "./sheets";
export {
ChatListItem,
MobileSidebar,
diff --git a/surfsense_web/components/layout/ui/sheets/AllSearchSpacesSheet.tsx b/surfsense_web/components/layout/ui/sheets/AllSearchSpacesSheet.tsx
new file mode 100644
index 000000000..d144c79b3
--- /dev/null
+++ b/surfsense_web/components/layout/ui/sheets/AllSearchSpacesSheet.tsx
@@ -0,0 +1,236 @@
+"use client";
+
+import { Calendar, MoreHorizontal, Search, Settings, Share2, Trash2, UserCheck, Users } from "lucide-react";
+import { useTranslations } from "next-intl";
+import { useState } from "react";
+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 {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+import type { SearchSpace } from "../../types/layout.types";
+
+function formatDate(dateString: string): string {
+ return new Date(dateString).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ });
+}
+
+interface AllSearchSpacesSheetProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ searchSpaces: SearchSpace[];
+ onSearchSpaceSelect: (id: number) => void;
+ onCreateNew?: () => void;
+ onSettings?: (id: number) => void;
+ onDelete?: (id: number) => void;
+}
+
+export function AllSearchSpacesSheet({
+ open,
+ onOpenChange,
+ searchSpaces,
+ onSearchSpaceSelect,
+ onCreateNew,
+ onSettings,
+ onDelete,
+}: AllSearchSpacesSheetProps) {
+ const t = useTranslations("searchSpace");
+ const tCommon = useTranslations("common");
+
+ const [spaceToDelete, setSpaceToDelete] = useState(null);
+
+ const handleSelect = (id: number) => {
+ onSearchSpaceSelect(id);
+ onOpenChange(false);
+ };
+
+ const handleSettings = (e: React.MouseEvent, space: SearchSpace) => {
+ e.stopPropagation();
+ onOpenChange(false);
+ onSettings?.(space.id);
+ };
+
+ const handleDeleteClick = (e: React.MouseEvent, space: SearchSpace) => {
+ e.stopPropagation();
+ setSpaceToDelete(space);
+ };
+
+ const confirmDelete = () => {
+ if (spaceToDelete) {
+ onDelete?.(spaceToDelete.id);
+ setSpaceToDelete(null);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {t("all_search_spaces")}
+
+ {t("search_spaces_count", { count: searchSpaces.length })}
+
+
+
+
+
+
+ {searchSpaces.length === 0 ? (
+
+
+
+
+
+
{t("no_search_spaces")}
+
+ {t("create_first_search_space")}
+
+
+ {onCreateNew && (
+
+ {t("create_button")}
+
+ )}
+
+ ) : (
+ searchSpaces.map((space) => (
+
handleSelect(space.id)}
+ className="flex w-full flex-col gap-2 rounded-lg border p-4 text-left transition-colors hover:bg-accent hover:border-accent-foreground/20"
+ >
+
+
+
+ {space.name}
+
+ {space.description && (
+
+ {space.description}
+
+ )}
+
+
+
+ {space.memberCount > 1 && (
+
+
+ {tCommon("shared")}
+
+ )}
+
+ {space.isOwner && (
+
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ handleSettings(e, space)}>
+
+ {tCommon("settings")}
+
+
+ handleDeleteClick(e, space)}
+ className="text-destructive focus:text-destructive"
+ >
+
+ {tCommon("delete")}
+
+
+
+ )}
+
+
+
+
+
+ {space.isOwner ? (
+
+ ) : (
+
+ )}
+ {t("members_count", { count: space.memberCount })}
+
+ {space.createdAt && (
+
+
+ {formatDate(space.createdAt)}
+
+ )}
+
+
+ ))
+ )}
+
+
+ {searchSpaces.length > 0 && onCreateNew && (
+
+
+ {t("create_new_search_space")}
+
+
+ )}
+
+
+
+ !open && setSpaceToDelete(null)}>
+
+
+ {t("delete_title")}
+
+ {t("delete_confirm", { name: spaceToDelete?.name ?? "" })}
+
+
+
+ {tCommon("cancel")}
+
+ {tCommon("delete")}
+
+
+
+
+ >
+ );
+}
diff --git a/surfsense_web/components/layout/ui/sheets/index.ts b/surfsense_web/components/layout/ui/sheets/index.ts
new file mode 100644
index 000000000..b2d05f1a8
--- /dev/null
+++ b/surfsense_web/components/layout/ui/sheets/index.ts
@@ -0,0 +1,2 @@
+export { AllSearchSpacesSheet } from "./AllSearchSpacesSheet";
+
diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx
index 0d7b24113..ee2978113 100644
--- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx
+++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx
@@ -11,18 +11,18 @@ import type {
NoteItem,
PageUsage,
User,
- Workspace,
+ SearchSpace,
} from "../../types/layout.types";
import { Header } from "../header";
import { IconRail } from "../icon-rail";
import { MobileSidebar, MobileSidebarTrigger, Sidebar } from "../sidebar";
interface LayoutShellProps {
- workspaces: Workspace[];
- activeWorkspaceId: number | null;
- onWorkspaceSelect: (id: number) => void;
- onAddWorkspace: () => void;
- workspace: Workspace | null;
+ searchSpaces: SearchSpace[];
+ activeSearchSpaceId: number | null;
+ onSearchSpaceSelect: (id: number) => void;
+ onAddSearchSpace: () => void;
+ searchSpace: SearchSpace | null;
navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[];
@@ -39,8 +39,9 @@ interface LayoutShellProps {
onViewAllNotes?: () => void;
user: User;
onSettings?: () => void;
- onInviteMembers?: () => void;
- onSeeAllWorkspaces?: () => void;
+ onManageMembers?: () => void;
+ onSeeAllSearchSpaces?: () => void;
+ onUserSettings?: () => void;
onLogout?: () => void;
pageUsage?: PageUsage;
breadcrumb?: React.ReactNode;
@@ -54,11 +55,11 @@ interface LayoutShellProps {
}
export function LayoutShell({
- workspaces,
- activeWorkspaceId,
- onWorkspaceSelect,
- onAddWorkspace,
- workspace,
+ searchSpaces,
+ activeSearchSpaceId,
+ onSearchSpaceSelect,
+ onAddSearchSpace,
+ searchSpace,
navItems,
onNavItemClick,
chats,
@@ -75,8 +76,9 @@ export function LayoutShell({
onViewAllNotes,
user,
onSettings,
- onInviteMembers,
- onSeeAllWorkspaces,
+ onManageMembers,
+ onSeeAllSearchSpaces,
+ onUserSettings,
onLogout,
pageUsage,
breadcrumb,
@@ -108,11 +110,11 @@ export function LayoutShell({
@@ -149,16 +152,16 @@ export function LayoutShell({
void;
- workspaces: Workspace[];
- activeWorkspaceId: number | null;
- onWorkspaceSelect: (id: number) => void;
- onAddWorkspace: () => void;
- workspace: Workspace | null;
+ searchSpaces: SearchSpace[];
+ activeSearchSpaceId: number | null;
+ onSearchSpaceSelect: (id: number) => void;
+ onAddSearchSpace: () => void;
+ searchSpace: SearchSpace | null;
navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[];
@@ -39,8 +39,9 @@ interface MobileSidebarProps {
onViewAllNotes?: () => void;
user: User;
onSettings?: () => void;
- onInviteMembers?: () => void;
- onSeeAllWorkspaces?: () => void;
+ onManageMembers?: () => void;
+ onSeeAllSearchSpaces?: () => void;
+ onUserSettings?: () => void;
onLogout?: () => void;
pageUsage?: PageUsage;
}
@@ -57,11 +58,11 @@ export function MobileSidebarTrigger({ onClick }: { onClick: () => void }) {
export function MobileSidebar({
isOpen,
onOpenChange,
- workspaces,
- activeWorkspaceId,
- onWorkspaceSelect,
- onAddWorkspace,
- workspace,
+ searchSpaces,
+ activeSearchSpaceId,
+ onSearchSpaceSelect,
+ onAddSearchSpace,
+ searchSpace,
navItems,
onNavItemClick,
chats,
@@ -78,13 +79,14 @@ export function MobileSidebar({
onViewAllNotes,
user,
onSettings,
- onInviteMembers,
- onSeeAllWorkspaces,
+ onManageMembers,
+ onSeeAllSearchSpaces,
+ onUserSettings,
onLogout,
pageUsage,
}: MobileSidebarProps) {
- const handleWorkspaceSelect = (id: number) => {
- onWorkspaceSelect(id);
+ const handleSearchSpaceSelect = (id: number) => {
+ onSearchSpaceSelect(id);
};
const handleNavItemClick = (item: NavItem) => {
@@ -110,17 +112,17 @@ export function MobileSidebar({
void;
navItems: NavItem[];
@@ -43,15 +43,16 @@ interface SidebarProps {
onViewAllNotes?: () => void;
user: User;
onSettings?: () => void;
- onInviteMembers?: () => void;
- onSeeAllWorkspaces?: () => void;
+ onManageMembers?: () => void;
+ onSeeAllSearchSpaces?: () => void;
+ onUserSettings?: () => void;
onLogout?: () => void;
pageUsage?: PageUsage;
className?: string;
}
export function Sidebar({
- workspace,
+ searchSpace,
isCollapsed = false,
onToggleCollapse,
navItems,
@@ -70,8 +71,9 @@ export function Sidebar({
onViewAllNotes,
user,
onSettings,
- onInviteMembers,
- onSeeAllWorkspaces,
+ onManageMembers,
+ onSeeAllSearchSpaces,
+ onUserSettings,
onLogout,
pageUsage,
className,
@@ -86,7 +88,7 @@ export function Sidebar({
className
)}
>
- {/* Header - workspace name or collapse button when collapsed */}
+ {/* Header - search space name or collapse button when collapsed */}
{isCollapsed ? (
);
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx
index cf15a367e..9373a6169 100644
--- a/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx
@@ -1,6 +1,6 @@
"use client";
-import { ChevronsUpDown, LayoutGrid, Settings, UserPlus } from "lucide-react";
+import { ChevronsUpDown, LayoutGrid, Settings, Users } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import {
@@ -11,23 +11,23 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
-import type { Workspace } from "../../types/layout.types";
+import type { SearchSpace } from "../../types/layout.types";
interface SidebarHeaderProps {
- workspace: Workspace | null;
+ searchSpace: SearchSpace | null;
isCollapsed?: boolean;
onSettings?: () => void;
- onInviteMembers?: () => void;
- onSeeAllWorkspaces?: () => void;
+ onManageMembers?: () => void;
+ onSeeAllSearchSpaces?: () => void;
className?: string;
}
export function SidebarHeader({
- workspace,
+ searchSpace,
isCollapsed,
onSettings,
- onInviteMembers,
- onSeeAllWorkspaces,
+ onManageMembers,
+ onSeeAllSearchSpaces,
className,
}: SidebarHeaderProps) {
const t = useTranslations("sidebar");
@@ -43,24 +43,24 @@ export function SidebarHeader({
isCollapsed ? "w-10" : "w-50"
)}
>
- {workspace?.name ?? t("select_workspace")}
+ {searchSpace?.name ?? t("select_search_space")}
-
-
- {t("invite_members")}
+
+
+ {t("manage_members")}
- {t("workspace_settings")}
+ {t("search_space_settings")}
-
+
- {t("see_all_workspaces")}
+ {t("see_all_search_spaces")}
diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx
index 29b35b9a9..d3e97c8eb 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, LogOut } from "lucide-react";
+import { ChevronUp, LogOut, Settings } from "lucide-react";
import { useTranslations } from "next-intl";
import {
DropdownMenu,
@@ -16,6 +16,7 @@ import type { User } from "../../types/layout.types";
interface SidebarUserProfileProps {
user: User;
+ onUserSettings?: () => void;
onLogout?: () => void;
isCollapsed?: boolean;
}
@@ -62,6 +63,7 @@ function getInitials(email: string): string {
export function SidebarUserProfile({
user,
+ onUserSettings,
onLogout,
isCollapsed = false,
}: SidebarUserProfileProps) {
@@ -117,6 +119,13 @@ export function SidebarUserProfile({
+
+
+ {t("user_settings")}
+
+
+
+
{t("logout")}
@@ -177,6 +186,13 @@ export function SidebarUserProfile({
+
+
+ {t("user_settings")}
+
+
+
+
{t("logout")}
diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json
index b803d4b69..57f03a0fb 100644
--- a/surfsense_web/messages/en.json
+++ b/surfsense_web/messages/en.json
@@ -28,7 +28,10 @@
"info": "Information",
"required": "Required",
"optional": "Optional",
- "retry": "Retry"
+ "retry": "Retry",
+ "owner": "Owner",
+ "shared": "Shared",
+ "settings": "Settings"
},
"auth": {
"login": "Login",
@@ -77,6 +80,45 @@
"creating_account_btn": "Creating account...",
"redirecting_login": "Redirecting to login page..."
},
+ "searchSpace": {
+ "create_title": "Create Search Space",
+ "create_description": "Create a new search space to organize your knowledge",
+ "name_label": "Name",
+ "name_placeholder": "Enter search space name",
+ "description_label": "Description",
+ "description_placeholder": "What is this search space for?",
+ "create_button": "Create",
+ "creating": "Creating...",
+ "all_search_spaces": "All Search Spaces",
+ "search_spaces_count": "{count, plural, =0 {No search spaces} =1 {1 search space} other {# search spaces}}",
+ "no_search_spaces": "No search spaces yet",
+ "create_first_search_space": "Create your first search space to get started",
+ "members_count": "{count, plural, =1 {1 member} other {# members}}",
+ "create_new_search_space": "Create new search space",
+ "delete_title": "Delete Search Space",
+ "delete_confirm": "Are you sure you want to delete \"{name}\"? This action cannot be undone and will permanently remove all data.",
+ "welcome_title": "Welcome to SurfSense",
+ "welcome_description": "Create your first search space to start organizing your knowledge, connecting sources, and chatting with AI.",
+ "create_first_button": "Create your first search space"
+ },
+ "userSettings": {
+ "title": "User Settings",
+ "description": "Manage your account settings and API access",
+ "back_to_app": "Back to app",
+ "footer": "User Settings",
+ "api_key_nav_label": "API Key",
+ "api_key_nav_description": "Manage your API access token",
+ "api_key_title": "API Key",
+ "api_key_description": "Use this key to authenticate API requests",
+ "api_key_warning_title": "Keep it secret",
+ "api_key_warning_description": "Your API key grants full access to your account. Never share it publicly or commit it to version control.",
+ "your_api_key": "Your API Key",
+ "copied": "Copied!",
+ "copy": "Copy to clipboard",
+ "no_api_key": "No API key found",
+ "usage_title": "How to use",
+ "usage_description": "Include your API key in the Authorization header:"
+ },
"dashboard": {
"title": "Dashboard",
"search_spaces": "Search Spaces",
@@ -624,12 +666,13 @@
"no_archived_chats": "No archived chats",
"error_archiving_chat": "Failed to archive chat",
"new_chat": "New chat",
- "select_workspace": "Select Workspace",
- "invite_members": "Invite members",
- "workspace_settings": "Workspace settings",
- "see_all_workspaces": "See all search spaces",
+ "select_search_space": "Select Search Space",
+ "manage_members": "Manage members",
+ "search_space_settings": "Search Space settings",
+ "see_all_search_spaces": "See all search spaces",
"expand_sidebar": "Expand sidebar",
"collapse_sidebar": "Collapse sidebar",
+ "user_settings": "User settings",
"logout": "Logout"
},
"errors": {
diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json
index fa690bf39..89cb7813a 100644
--- a/surfsense_web/messages/zh.json
+++ b/surfsense_web/messages/zh.json
@@ -28,7 +28,10 @@
"info": "信息",
"required": "必填",
"optional": "可选",
- "retry": "重试"
+ "retry": "重试",
+ "owner": "所有者",
+ "shared": "共享",
+ "settings": "设置"
},
"auth": {
"login": "登录",
@@ -77,6 +80,45 @@
"creating_account_btn": "创建中...",
"redirecting_login": "正在跳转到登录页面..."
},
+ "searchSpace": {
+ "create_title": "创建搜索空间",
+ "create_description": "创建一个新的搜索空间来组织您的知识",
+ "name_label": "名称",
+ "name_placeholder": "输入搜索空间名称",
+ "description_label": "描述",
+ "description_placeholder": "这个搜索空间是做什么的?",
+ "create_button": "创建",
+ "creating": "创建中...",
+ "all_search_spaces": "所有搜索空间",
+ "search_spaces_count": "{count, plural, =0 {没有搜索空间} other {# 个搜索空间}}",
+ "no_search_spaces": "暂无搜索空间",
+ "create_first_search_space": "创建您的第一个搜索空间以开始使用",
+ "members_count": "{count, plural, other {# 位成员}}",
+ "create_new_search_space": "创建新的搜索空间",
+ "delete_title": "删除搜索空间",
+ "delete_confirm": "您确定要删除「{name}」吗?此操作无法撤销,将永久删除所有数据。",
+ "welcome_title": "欢迎使用 SurfSense",
+ "welcome_description": "创建您的第一个搜索空间,开始组织知识、连接数据源并与AI对话。",
+ "create_first_button": "创建第一个搜索空间"
+ },
+ "userSettings": {
+ "title": "用户设置",
+ "description": "管理您的账户设置和API访问",
+ "back_to_app": "返回应用",
+ "footer": "用户设置",
+ "api_key_nav_label": "API密钥",
+ "api_key_nav_description": "管理您的API访问令牌",
+ "api_key_title": "API密钥",
+ "api_key_description": "使用此密钥验证API请求",
+ "api_key_warning_title": "请保密",
+ "api_key_warning_description": "您的API密钥可以完全访问您的账户。请勿公开分享或提交到版本控制。",
+ "your_api_key": "您的API密钥",
+ "copied": "已复制!",
+ "copy": "复制到剪贴板",
+ "no_api_key": "未找到API密钥",
+ "usage_title": "使用方法",
+ "usage_description": "在Authorization请求头中包含您的API密钥:"
+ },
"dashboard": {
"title": "仪表盘",
"search_spaces": "搜索空间",
@@ -618,12 +660,13 @@
"view_all_notes": "查看所有笔记",
"add_note": "添加笔记",
"new_chat": "新对话",
- "select_workspace": "选择工作空间",
- "invite_members": "邀请成员",
- "workspace_settings": "工作空间设置",
- "see_all_workspaces": "查看所有搜索空间",
+ "select_search_space": "选择搜索空间",
+ "manage_members": "管理成员",
+ "search_space_settings": "搜索空间设置",
+ "see_all_search_spaces": "查看所有搜索空间",
"expand_sidebar": "展开侧边栏",
"collapse_sidebar": "收起侧边栏",
+ "user_settings": "用户设置",
"logout": "退出登录"
},
"errors": {