mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-02 22:01:05 +02:00
Add User Settings page with sidebar pattern and API key section
This commit is contained in:
parent
0d4d227c26
commit
34c9d24970
7 changed files with 390 additions and 2 deletions
323
surfsense_web/app/dashboard/user/settings/page.tsx
Normal file
323
surfsense_web/app/dashboard/user/settings/page.tsx
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
"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 (
|
||||||
|
<>
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm md:hidden"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
"fixed left-0 top-0 z-50 md:relative md:z-auto",
|
||||||
|
"flex h-full w-72 shrink-0 flex-col bg-background md:bg-muted/20",
|
||||||
|
"transition-transform duration-300 ease-out",
|
||||||
|
"md:translate-x-0",
|
||||||
|
isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onBackToApp}
|
||||||
|
className="group h-11 flex-1 justify-start gap-3 px-3 hover:bg-muted"
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 transition-colors group-hover:bg-primary/20">
|
||||||
|
<ArrowLeft className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium">{t("back_to_app")}</span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={onClose} className="h-9 w-9 md:hidden">
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 space-y-1 overflow-y-auto px-3 py-2">
|
||||||
|
{navItems.map((item, index) => {
|
||||||
|
const isActive = activeSection === item.id;
|
||||||
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={item.id}
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.1 + index * 0.05, duration: 0.3 }}
|
||||||
|
onClick={() => 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 && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="userSettingsActiveIndicator"
|
||||||
|
className="absolute left-0 top-1/2 h-8 w-1 -translate-y-1/2 rounded-r-full bg-primary"
|
||||||
|
initial={false}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 500,
|
||||||
|
damping: 35,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-9 items-center justify-center rounded-lg transition-colors",
|
||||||
|
isActive ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"truncate text-sm font-medium transition-colors",
|
||||||
|
isActive ? "text-foreground" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-xs text-muted-foreground/70">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 shrink-0 transition-all",
|
||||||
|
isActive
|
||||||
|
? "translate-x-0 text-primary opacity-100"
|
||||||
|
: "-translate-x-1 text-muted-foreground/40 opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-center text-xs text-muted-foreground">{t("footer")}</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApiKeyContent({ onMenuClick }: { onMenuClick: () => void }) {
|
||||||
|
const t = useTranslations("userSettings");
|
||||||
|
const { apiKey, isLoading, copied, copyToClipboard } = useApiKey();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2, duration: 0.4 }}
|
||||||
|
className="h-full min-w-0 flex-1 overflow-hidden bg-background"
|
||||||
|
>
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<div className="mx-auto max-w-4xl p-4 md:p-6 lg:p-10">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key="api-key-header"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="mb-6 md:mb-8"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={onMenuClick}
|
||||||
|
className="h-10 w-10 shrink-0 md:hidden"
|
||||||
|
>
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.1, duration: 0.3 }}
|
||||||
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-primary/10 bg-gradient-to-br from-primary/20 to-primary/5 shadow-sm md:h-14 md:w-14 md:rounded-2xl"
|
||||||
|
>
|
||||||
|
<Key className="h-5 w-5 text-primary md:h-7 md:w-7" />
|
||||||
|
</motion.div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="truncate text-lg font-bold tracking-tight md:text-2xl">
|
||||||
|
{t("api_key_title")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">{t("api_key_description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key="api-key-content"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.35, ease: [0.4, 0, 0.2, 1] }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<Alert>
|
||||||
|
<Shield className="h-4 w-4" />
|
||||||
|
<AlertTitle>{t("api_key_warning_title")}</AlertTitle>
|
||||||
|
<AlertDescription>{t("api_key_warning_description")}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="rounded-lg border bg-card p-6">
|
||||||
|
<h3 className="mb-4 font-medium">{t("your_api_key")}</h3>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="h-12 w-full animate-pulse rounded-md bg-muted" />
|
||||||
|
) : apiKey ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 overflow-x-auto rounded-md bg-muted p-3 font-mono text-sm">
|
||||||
|
{apiKey}
|
||||||
|
</div>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{copied ? t("copied") : t("copy")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground">{t("no_api_key")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border bg-card p-6">
|
||||||
|
<h3 className="mb-2 font-medium">{t("usage_title")}</h3>
|
||||||
|
<p className="mb-4 text-sm text-muted-foreground">{t("usage_description")}</p>
|
||||||
|
<pre className="overflow-x-auto rounded-md bg-muted p-3 text-sm">
|
||||||
|
<code>Authorization: Bearer {apiKey || "YOUR_API_KEY"}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="fixed inset-0 z-50 flex bg-background"
|
||||||
|
>
|
||||||
|
<UserSettingsSidebar
|
||||||
|
activeSection={activeSection}
|
||||||
|
onSectionChange={setActiveSection}
|
||||||
|
onBackToApp={handleBackToApp}
|
||||||
|
isOpen={isSidebarOpen}
|
||||||
|
onClose={() => setIsSidebarOpen(false)}
|
||||||
|
navItems={navItems}
|
||||||
|
/>
|
||||||
|
{activeSection === "api-key" && (
|
||||||
|
<ApiKeyContent onMenuClick={() => setIsSidebarOpen(true)} />
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -214,6 +214,10 @@ export function LayoutDataProvider({
|
||||||
setIsAllSearchSpacesSheetOpen(true);
|
setIsAllSearchSpacesSheetOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleUserSettings = useCallback(() => {
|
||||||
|
router.push("/dashboard/user/settings");
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const handleSearchSpaceSettings = useCallback(
|
const handleSearchSpaceSettings = useCallback(
|
||||||
(id: number) => {
|
(id: number) => {
|
||||||
router.push(`/dashboard/${id}/settings`);
|
router.push(`/dashboard/${id}/settings`);
|
||||||
|
|
@ -396,6 +400,7 @@ export function LayoutDataProvider({
|
||||||
onSettings={handleSettings}
|
onSettings={handleSettings}
|
||||||
onManageMembers={handleManageMembers}
|
onManageMembers={handleManageMembers}
|
||||||
onSeeAllSearchSpaces={handleSeeAllSearchSpaces}
|
onSeeAllSearchSpaces={handleSeeAllSearchSpaces}
|
||||||
|
onUserSettings={handleUserSettings}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
breadcrumb={breadcrumb}
|
breadcrumb={breadcrumb}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ interface LayoutShellProps {
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
onSeeAllSearchSpaces?: () => void;
|
onSeeAllSearchSpaces?: () => void;
|
||||||
|
onUserSettings?: () => void;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
pageUsage?: PageUsage;
|
pageUsage?: PageUsage;
|
||||||
breadcrumb?: React.ReactNode;
|
breadcrumb?: React.ReactNode;
|
||||||
|
|
@ -77,6 +78,7 @@ export function LayoutShell({
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
onSeeAllSearchSpaces,
|
onSeeAllSearchSpaces,
|
||||||
|
onUserSettings,
|
||||||
onLogout,
|
onLogout,
|
||||||
pageUsage,
|
pageUsage,
|
||||||
breadcrumb,
|
breadcrumb,
|
||||||
|
|
@ -131,6 +133,7 @@ export function LayoutShell({
|
||||||
onSettings={onSettings}
|
onSettings={onSettings}
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
||||||
|
onUserSettings={onUserSettings}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
/>
|
/>
|
||||||
|
|
@ -179,6 +182,7 @@ export function LayoutShell({
|
||||||
onSettings={onSettings}
|
onSettings={onSettings}
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
||||||
|
onUserSettings={onUserSettings}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
className="hidden md:flex border-r shrink-0"
|
className="hidden md:flex border-r shrink-0"
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ interface SidebarProps {
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
onSeeAllSearchSpaces?: () => void;
|
onSeeAllSearchSpaces?: () => void;
|
||||||
|
onUserSettings?: () => void;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
pageUsage?: PageUsage;
|
pageUsage?: PageUsage;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -72,6 +73,7 @@ export function Sidebar({
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
onSeeAllSearchSpaces,
|
onSeeAllSearchSpaces,
|
||||||
|
onUserSettings,
|
||||||
onLogout,
|
onLogout,
|
||||||
pageUsage,
|
pageUsage,
|
||||||
className,
|
className,
|
||||||
|
|
@ -287,7 +289,7 @@ export function Sidebar({
|
||||||
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SidebarUserProfile user={user} onLogout={onLogout} isCollapsed={isCollapsed} />
|
<SidebarUserProfile user={user} onUserSettings={onUserSettings} onLogout={onLogout} isCollapsed={isCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronUp, LogOut } from "lucide-react";
|
import { ChevronUp, LogOut, Settings } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|
@ -16,6 +16,7 @@ import type { User } from "../../types/layout.types";
|
||||||
|
|
||||||
interface SidebarUserProfileProps {
|
interface SidebarUserProfileProps {
|
||||||
user: User;
|
user: User;
|
||||||
|
onUserSettings?: () => void;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -62,6 +63,7 @@ function getInitials(email: string): string {
|
||||||
|
|
||||||
export function SidebarUserProfile({
|
export function SidebarUserProfile({
|
||||||
user,
|
user,
|
||||||
|
onUserSettings,
|
||||||
onLogout,
|
onLogout,
|
||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
}: SidebarUserProfileProps) {
|
}: SidebarUserProfileProps) {
|
||||||
|
|
@ -117,6 +119,13 @@ export function SidebarUserProfile({
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
{t("user_settings")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onLogout}>
|
<DropdownMenuItem onClick={onLogout}>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
{t("logout")}
|
{t("logout")}
|
||||||
|
|
@ -177,6 +186,13 @@ export function SidebarUserProfile({
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
{t("user_settings")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onLogout}>
|
<DropdownMenuItem onClick={onLogout}>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
{t("logout")}
|
{t("logout")}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,24 @@
|
||||||
"welcome_description": "Create your first search space to start organizing your knowledge, connecting sources, and chatting with AI.",
|
"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"
|
"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": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"search_spaces": "Search Spaces",
|
"search_spaces": "Search Spaces",
|
||||||
|
|
@ -658,6 +676,7 @@
|
||||||
"see_all_search_spaces": "See all search spaces",
|
"see_all_search_spaces": "See all search spaces",
|
||||||
"expand_sidebar": "Expand sidebar",
|
"expand_sidebar": "Expand sidebar",
|
||||||
"collapse_sidebar": "Collapse sidebar",
|
"collapse_sidebar": "Collapse sidebar",
|
||||||
|
"user_settings": "User settings",
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,24 @@
|
||||||
"welcome_description": "创建您的第一个搜索空间,开始组织知识、连接数据源并与AI对话。",
|
"welcome_description": "创建您的第一个搜索空间,开始组织知识、连接数据源并与AI对话。",
|
||||||
"create_first_button": "创建第一个搜索空间"
|
"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": {
|
"dashboard": {
|
||||||
"title": "仪表盘",
|
"title": "仪表盘",
|
||||||
"search_spaces": "搜索空间",
|
"search_spaces": "搜索空间",
|
||||||
|
|
@ -652,6 +670,7 @@
|
||||||
"see_all_search_spaces": "查看所有搜索空间",
|
"see_all_search_spaces": "查看所有搜索空间",
|
||||||
"expand_sidebar": "展开侧边栏",
|
"expand_sidebar": "展开侧边栏",
|
||||||
"collapse_sidebar": "收起侧边栏",
|
"collapse_sidebar": "收起侧边栏",
|
||||||
|
"user_settings": "用户设置",
|
||||||
"logout": "退出登录"
|
"logout": "退出登录"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue