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 d53a6b26c..fde676dff 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx @@ -87,7 +87,8 @@ function SettingsSidebar({ @@ -286,20 +287,24 @@ export default function SettingsPage() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }} - className="h-full flex bg-background" + className="fixed inset-0 z-50 flex bg-muted/40" > - setIsSidebarOpen(false)} - /> - setIsSidebarOpen(true)} - /> +
+
+ setIsSidebarOpen(false)} + /> + setIsSidebarOpen(true)} + /> +
+
); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 13124d756..8ffb6c3bf 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -3,7 +3,6 @@ import { useQuery } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { - ArrowLeft, Calendar, Check, Clock, @@ -27,7 +26,7 @@ import { Users, } from "lucide-react"; import { motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { @@ -144,7 +143,6 @@ const cardVariants = { }; export default function TeamManagementPage() { - const router = useRouter(); const params = useParams(); const searchSpaceId = Number(params.search_space_id); const [activeTab, setActiveTab] = useState("members"); @@ -334,14 +332,6 @@ export default function TeamManagementPage() {
-
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!" : "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"} - - -
-
-
-
-
-
-
- -
-
- ); -}; - -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")} +

+
+ + +
+
+ ); +} + +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")}

- - - - - -
- -
- {searchSpaces && - searchSpaces.length > 0 && - searchSpaces.map((space) => ( - - - -
-
- - {space.name} -
- -
-
- - - - - - - {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")} -

- - - -
- )} - - {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 && ( + + )} + + + + + ); +} + +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 ? 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")} +
+
+
+ +
+ + ( + + {t("name_label")} + + + + + + )} + /> + + ( + + + {t("description_label")}{" "} + + ({tCommon("optional")}) + + + + + + + + )} + /> + + + + + + + +
+
+ ); +} + 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
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 && ( + + )} +
+ ) : ( + searchSpaces.map((space) => ( + + + + 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 && ( +
+ +
+ )} + + + + !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": {