diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx index 4ee8bad1c..74d8ab140 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx @@ -27,14 +27,12 @@ export function ApiKeyContent() { return (
- - - - {t("api_key_warning_description")} - + + + {t("api_key_warning_description")} -
+

{t("your_api_key")}

{isLoading ? (
@@ -70,7 +68,7 @@ export function ApiKeyContent() { )}
-
+

{t("usage_title")}

{t("usage_description")}

diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx index 309bcda06..89bc362eb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx @@ -11,8 +11,17 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; +import { getUserAvatarColor, getUserInitials } from "@/lib/user-avatar"; -function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) { +function AvatarDisplay({ + url, + fallback, + bgColor, +}: { + url?: string; + fallback: string; + bgColor: string; +}) { const [errorUrl, setErrorUrl] = useState(); const hasError = errorUrl === url; @@ -23,15 +32,19 @@ function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) { alt="Avatar" width={64} height={64} - className="h-16 w-16 rounded-xl object-cover" + className="h-16 w-16 rounded-full object-cover select-none" onError={() => setErrorUrl(url)} + referrerPolicy="no-referrer" unoptimized /> ); } return ( -
+
{fallback}
); @@ -50,11 +63,6 @@ export function ProfileContent() { } }, [user]); - const getInitials = (email: string) => { - const name = email.split("@")[0]; - return name.slice(0, 2).toUpperCase(); - }; - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -69,6 +77,7 @@ export function ProfileContent() { }; const hasChanges = displayName !== (user?.display_name || ""); + const avatarBgColor = getUserAvatarColor(user?.email || ""); return (
@@ -78,13 +87,13 @@ export function ProfileContent() {
) : (
-
+
-
diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx index b15525c78..b18f536ed 100644 --- a/surfsense_web/components/assistant-ui/assistant-message.tsx +++ b/surfsense_web/components/assistant-ui/assistant-message.tsx @@ -316,7 +316,7 @@ const MessageInfoDropdown: FC<{ chatTurnId: string | null | undefined }> = ({ ch )} {hasUsage && ( <> - + {models.length > 0 ? ( models.map(([model, counts]) => { const { name, icon } = resolveModel(model); @@ -586,7 +586,7 @@ const AssistantActionBar: FC = () => { className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground md:data-floating:absolute md:data-floating:rounded-md md:data-floating:p-1 [&>button]:opacity-100 md:[&>button]:opacity-[var(--aui-button-opacity,1)]" > - + message.isCopied}> diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index 8d8cd44e0..550c83b80 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -38,6 +38,7 @@ import { usePlatform } from "@/hooks/use-platform"; import { GITHUB_RELEASES_URL, usePrimaryDownload } from "@/lib/desktop-download-utils"; import { APP_VERSION } from "@/lib/env-config"; import { trackDesktopDownloadClicked } from "@/lib/posthog/events"; +import { getUserAvatarColor, getUserInitials } from "@/lib/user-avatar"; import { cn } from "@/lib/utils"; import type { User } from "../../types/layout.types"; @@ -81,46 +82,6 @@ function formatAnnouncementCount(count: number): string { return `${thousands}k+`; } -/** - * Generates a consistent color based on email - */ -function stringToColor(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - const colors = [ - "#6366f1", - "#8b5cf6", - "#a855f7", - "#d946ef", - "#ec4899", - "#f43f5e", - "#ef4444", - "#f97316", - "#eab308", - "#84cc16", - "#22c55e", - "#14b8a6", - "#06b6d4", - "#0ea5e9", - "#3b82f6", - ]; - return colors[Math.abs(hash) % colors.length]; -} - -/** - * Gets initials from email - */ -function getInitials(email: string): string { - const name = email.split("@")[0]; - const parts = name.split(/[._-]/); - if (parts.length >= 2) { - return (parts[0][0] + parts[1][0]).toUpperCase(); - } - return name.slice(0, 2).toUpperCase(); -} - /** * User avatar component - shows image if available, otherwise falls back to initials */ @@ -180,8 +141,8 @@ export function SidebarUserProfile({ const isDesktopViewport = useMediaQuery("(min-width: 768px)"); const { os, primary } = usePrimaryDownload(); const [isLoggingOut, setIsLoggingOut] = useState(false); - const bgColor = stringToColor(user.email); - const initials = getInitials(user.email); + const bgColor = getUserAvatarColor(user.email); + const initials = getUserInitials(user.email); const displayName = user.name || user.email.split("@")[0]; const downloadUrl = primary?.url ?? GITHUB_RELEASES_URL; const downloadLabel = t("download_for_os", { os }); diff --git a/surfsense_web/components/settings/user-settings-panel.tsx b/surfsense_web/components/settings/user-settings-panel.tsx index 28e04a8d4..b4924c16a 100644 --- a/surfsense_web/components/settings/user-settings-panel.tsx +++ b/surfsense_web/components/settings/user-settings-panel.tsx @@ -251,7 +251,7 @@ export function UserSettingsPanel({

{selectedLabel}

- +
{selectedTab === "profile" && } diff --git a/surfsense_web/lib/user-avatar.ts b/surfsense_web/lib/user-avatar.ts new file mode 100644 index 000000000..66a972362 --- /dev/null +++ b/surfsense_web/lib/user-avatar.ts @@ -0,0 +1,34 @@ +const USER_AVATAR_COLORS = [ + "#6366f1", + "#8b5cf6", + "#a855f7", + "#d946ef", + "#ec4899", + "#f43f5e", + "#ef4444", + "#f97316", + "#eab308", + "#84cc16", + "#22c55e", + "#14b8a6", + "#06b6d4", + "#0ea5e9", + "#3b82f6", +]; + +export function getUserAvatarColor(email: string): string { + let hash = 0; + for (let i = 0; i < email.length; i++) { + hash = email.charCodeAt(i) + ((hash << 5) - hash); + } + return USER_AVATAR_COLORS[Math.abs(hash) % USER_AVATAR_COLORS.length]; +} + +export function getUserInitials(email: string): string { + const name = email.split("@")[0]; + const parts = name.split(/[._-]/); + if (parts.length >= 2 && parts[0]?.[0] && parts[1]?.[0]) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return name.slice(0, 2).toUpperCase(); +}