mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-14 22:52:15 +02:00
feat: add language selection feature to user profile sidebar, fixed some hover color logic for both model selector and language selection
This commit is contained in:
parent
a41b75463f
commit
24049345a2
9 changed files with 87 additions and 24 deletions
|
|
@ -16,7 +16,6 @@ import {
|
||||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||||
import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup";
|
import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup";
|
||||||
import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
|
import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
|
||||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
|
||||||
import { LayoutDataProvider } from "@/components/layout";
|
import { LayoutDataProvider } from "@/components/layout";
|
||||||
import { OnboardingTour } from "@/components/onboarding-tour";
|
import { OnboardingTour } from "@/components/onboarding-tour";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
@ -200,7 +199,6 @@ export function DashboardClientLayout({
|
||||||
<LayoutDataProvider
|
<LayoutDataProvider
|
||||||
searchSpaceId={searchSpaceId}
|
searchSpaceId={searchSpaceId}
|
||||||
breadcrumb={<DashboardBreadcrumb />}
|
breadcrumb={<DashboardBreadcrumb />}
|
||||||
languageSwitcher={<LanguageSwitcher />}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</LayoutDataProvider>
|
</LayoutDataProvider>
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,12 @@ interface LayoutDataProviderProps {
|
||||||
searchSpaceId: string;
|
searchSpaceId: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
breadcrumb?: React.ReactNode;
|
breadcrumb?: React.ReactNode;
|
||||||
languageSwitcher?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LayoutDataProvider({
|
export function LayoutDataProvider({
|
||||||
searchSpaceId,
|
searchSpaceId,
|
||||||
children,
|
children,
|
||||||
breadcrumb,
|
breadcrumb,
|
||||||
languageSwitcher,
|
|
||||||
}: LayoutDataProviderProps) {
|
}: LayoutDataProviderProps) {
|
||||||
const t = useTranslations("dashboard");
|
const t = useTranslations("dashboard");
|
||||||
const tCommon = useTranslations("common");
|
const tCommon = useTranslations("common");
|
||||||
|
|
@ -375,7 +373,6 @@ export function LayoutDataProvider({
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
breadcrumb={breadcrumb}
|
breadcrumb={breadcrumb}
|
||||||
languageSwitcher={languageSwitcher}
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onToggleTheme={handleToggleTheme}
|
onToggleTheme={handleToggleTheme}
|
||||||
isChatPage={isChatPage}
|
isChatPage={isChatPage}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { NotificationButton } from "@/components/notifications/NotificationButto
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
breadcrumb?: React.ReactNode;
|
breadcrumb?: React.ReactNode;
|
||||||
languageSwitcher?: React.ReactNode;
|
|
||||||
theme?: string;
|
theme?: string;
|
||||||
onToggleTheme?: () => void;
|
onToggleTheme?: () => void;
|
||||||
mobileMenuTrigger?: React.ReactNode;
|
mobileMenuTrigger?: React.ReactNode;
|
||||||
|
|
@ -15,7 +14,6 @@ interface HeaderProps {
|
||||||
|
|
||||||
export function Header({
|
export function Header({
|
||||||
breadcrumb,
|
breadcrumb,
|
||||||
languageSwitcher,
|
|
||||||
theme,
|
theme,
|
||||||
onToggleTheme,
|
onToggleTheme,
|
||||||
mobileMenuTrigger,
|
mobileMenuTrigger,
|
||||||
|
|
@ -45,8 +43,6 @@ export function Header({
|
||||||
<TooltipContent>{theme === "dark" ? "Light mode" : "Dark mode"}</TooltipContent>
|
<TooltipContent>{theme === "dark" ? "Light mode" : "Dark mode"}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{languageSwitcher}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ interface LayoutShellProps {
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
pageUsage?: PageUsage;
|
pageUsage?: PageUsage;
|
||||||
breadcrumb?: React.ReactNode;
|
breadcrumb?: React.ReactNode;
|
||||||
languageSwitcher?: React.ReactNode;
|
|
||||||
theme?: string;
|
theme?: string;
|
||||||
onToggleTheme?: () => void;
|
onToggleTheme?: () => void;
|
||||||
defaultCollapsed?: boolean;
|
defaultCollapsed?: boolean;
|
||||||
|
|
@ -69,7 +68,6 @@ export function LayoutShell({
|
||||||
onLogout,
|
onLogout,
|
||||||
pageUsage,
|
pageUsage,
|
||||||
breadcrumb,
|
breadcrumb,
|
||||||
languageSwitcher,
|
|
||||||
theme,
|
theme,
|
||||||
onToggleTheme,
|
onToggleTheme,
|
||||||
defaultCollapsed = false,
|
defaultCollapsed = false,
|
||||||
|
|
@ -88,7 +86,6 @@ export function LayoutShell({
|
||||||
<div className={cn("flex h-screen w-full flex-col bg-background", className)}>
|
<div className={cn("flex h-screen w-full flex-col bg-background", className)}>
|
||||||
<Header
|
<Header
|
||||||
breadcrumb={breadcrumb}
|
breadcrumb={breadcrumb}
|
||||||
languageSwitcher={languageSwitcher}
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onToggleTheme={onToggleTheme}
|
onToggleTheme={onToggleTheme}
|
||||||
mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />}
|
mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />}
|
||||||
|
|
@ -172,7 +169,6 @@ export function LayoutShell({
|
||||||
<main className="flex-1 flex flex-col min-w-0">
|
<main className="flex-1 flex flex-col min-w-0">
|
||||||
<Header
|
<Header
|
||||||
breadcrumb={breadcrumb}
|
breadcrumb={breadcrumb}
|
||||||
languageSwitcher={languageSwitcher}
|
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onToggleTheme={onToggleTheme}
|
onToggleTheme={onToggleTheme}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,30 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronUp, LogOut, Settings } from "lucide-react";
|
import { ChevronUp, Languages, LogOut, Settings } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useLocaleContext } from "@/contexts/LocaleContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { User } from "../../types/layout.types";
|
import type { User } from "../../types/layout.types";
|
||||||
|
|
||||||
|
// Supported languages configuration
|
||||||
|
const LANGUAGES = [
|
||||||
|
{ code: "en" as const, name: "English", flag: "🇺🇸" },
|
||||||
|
{ code: "zh" as const, name: "简体中文", flag: "🇨🇳" },
|
||||||
|
];
|
||||||
|
|
||||||
interface SidebarUserProfileProps {
|
interface SidebarUserProfileProps {
|
||||||
user: User;
|
user: User;
|
||||||
onUserSettings?: () => void;
|
onUserSettings?: () => void;
|
||||||
|
|
@ -101,10 +112,15 @@ export function SidebarUserProfile({
|
||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
}: SidebarUserProfileProps) {
|
}: SidebarUserProfileProps) {
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
|
const { locale, setLocale } = useLocaleContext();
|
||||||
const bgColor = stringToColor(user.email);
|
const bgColor = stringToColor(user.email);
|
||||||
const initials = getInitials(user.email);
|
const initials = getInitials(user.email);
|
||||||
const displayName = user.name || user.email.split("@")[0];
|
const displayName = user.name || user.email.split("@")[0];
|
||||||
|
|
||||||
|
const handleLanguageChange = (newLocale: "en" | "zh") => {
|
||||||
|
setLocale(newLocale);
|
||||||
|
};
|
||||||
|
|
||||||
// Collapsed view - just show avatar with dropdown
|
// Collapsed view - just show avatar with dropdown
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -118,7 +134,8 @@ export function SidebarUserProfile({
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full items-center justify-center rounded-md",
|
"flex h-10 w-full items-center justify-center rounded-md",
|
||||||
"hover:bg-accent transition-colors",
|
"hover:bg-accent transition-colors",
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
"focus:outline-none focus-visible:outline-none",
|
||||||
|
"data-[state=open]:bg-transparent"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -129,7 +146,7 @@ export function SidebarUserProfile({
|
||||||
<TooltipContent side="right">{displayName}</TooltipContent>
|
<TooltipContent side="right">{displayName}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-56" side="right" align="end" sideOffset={8}>
|
<DropdownMenuContent className="w-56" side="right" align="center" sideOffset={8}>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -147,6 +164,34 @@ export function SidebarUserProfile({
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<Languages className="mr-2 h-4 w-4" />
|
||||||
|
{t("language")}
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent className="gap-1">
|
||||||
|
{LANGUAGES.map((language) => {
|
||||||
|
const isSelected = locale === language.code;
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={language.code}
|
||||||
|
onClick={() => handleLanguageChange(language.code)}
|
||||||
|
className={cn(
|
||||||
|
"mb-1 last:mb-0",
|
||||||
|
!isSelected && "focus:bg-transparent hover:bg-transparent",
|
||||||
|
isSelected && "bg-accent focus:!bg-accent hover:!bg-accent"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="mr-2">{language.flag}</span>
|
||||||
|
<span className="flex-1">{language.name}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onLogout}>
|
<DropdownMenuItem onClick={onLogout}>
|
||||||
|
|
@ -169,7 +214,8 @@ export function SidebarUserProfile({
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 px-2 py-3 text-left",
|
"flex w-full items-center gap-2 px-2 py-3 text-left",
|
||||||
"hover:bg-accent transition-colors",
|
"hover:bg-accent transition-colors",
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
"focus:outline-none focus-visible:outline-none",
|
||||||
|
"data-[state=open]:bg-transparent"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -185,7 +231,7 @@ export function SidebarUserProfile({
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-56" side="top" align="start" sideOffset={4}>
|
<DropdownMenuContent className="w-56" side="top" align="center" sideOffset={4}>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -203,6 +249,34 @@ export function SidebarUserProfile({
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<Languages className="mr-2 h-4 w-4" />
|
||||||
|
{t("language")}
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent className="gap-1">
|
||||||
|
{LANGUAGES.map((language) => {
|
||||||
|
const isSelected = locale === language.code;
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={language.code}
|
||||||
|
onClick={() => handleLanguageChange(language.code)}
|
||||||
|
className={cn(
|
||||||
|
"mb-1 last:mb-0",
|
||||||
|
!isSelected && "focus:bg-transparent hover:bg-transparent",
|
||||||
|
isSelected && "bg-accent focus:!bg-accent hover:!bg-accent"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="mr-2">{language.flag}</span>
|
||||||
|
<span className="flex-1">{language.name}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onLogout}>
|
<DropdownMenuItem onClick={onLogout}>
|
||||||
|
|
|
||||||
|
|
@ -265,8 +265,8 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
onSelect={() => handleSelectConfig(config)}
|
onSelect={() => handleSelectConfig(config)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-2 rounded-lg mb-1 cursor-pointer",
|
"mx-2 rounded-lg mb-1 cursor-pointer",
|
||||||
"aria-selected:bg-accent/50",
|
!isSelected && "data-[selected=true]:bg-transparent hover:bg-transparent",
|
||||||
isSelected && "bg-accent/80"
|
isSelected && "bg-accent/80 data-[selected=true]:!bg-accent/80 hover:!bg-accent/80"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full gap-2">
|
<div className="flex items-center justify-between w-full gap-2">
|
||||||
|
|
@ -327,8 +327,8 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
onSelect={() => handleSelectConfig(config)}
|
onSelect={() => handleSelectConfig(config)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-2 rounded-lg mb-1 cursor-pointer",
|
"mx-2 rounded-lg mb-1 cursor-pointer",
|
||||||
"aria-selected:bg-accent/50",
|
!isSelected && "data-[selected=true]:bg-transparent hover:bg-transparent",
|
||||||
isSelected && "bg-accent/80"
|
isSelected && "bg-accent/80 data-[selected=true]:!bg-accent/80 hover:!bg-accent/80"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full gap-2">
|
<div className="flex items-center justify-between w-full gap-2">
|
||||||
|
|
|
||||||
|
|
@ -182,13 +182,13 @@ function DropdownMenuSubTrigger({
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
<ChevronRightIcon className="ml-auto size-4 text-muted-foreground" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -687,6 +687,7 @@
|
||||||
"expand_sidebar": "Expand sidebar",
|
"expand_sidebar": "Expand sidebar",
|
||||||
"collapse_sidebar": "Collapse sidebar",
|
"collapse_sidebar": "Collapse sidebar",
|
||||||
"user_settings": "User settings",
|
"user_settings": "User settings",
|
||||||
|
"language": "Language",
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
|
||||||
|
|
@ -672,6 +672,7 @@
|
||||||
"expand_sidebar": "展开侧边栏",
|
"expand_sidebar": "展开侧边栏",
|
||||||
"collapse_sidebar": "收起侧边栏",
|
"collapse_sidebar": "收起侧边栏",
|
||||||
"user_settings": "用户设置",
|
"user_settings": "用户设置",
|
||||||
|
"language": "语言",
|
||||||
"logout": "退出登录"
|
"logout": "退出登录"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue