diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx index afff226acda8..4f637b4aa6f0 100644 --- a/ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx +++ b/ee/tabby-ui/app/(dashboard)/profile/components/avatar.tsx @@ -1,17 +1,64 @@ -import NiceAvatar, { genConfig } from 'react-nice-avatar' +'use client' +import { ChangeEvent } from 'react' + +import { cn } from '@/lib/utils' import { useMe } from '@/lib/hooks/use-me' +import { graphql } from '@/lib/gql/generates' +import { useMutation } from '@/lib/tabby/gql' +import { toast } from 'sonner' + +import { buttonVariants } from "@/components/ui/button" +import { UserAvatar, mutateAvatar } from '@/components/user-avatar' + +const uploadUserAvatarMutation = graphql(/* GraphQL */ ` + mutation uploadUserAvatarBase64($id: ID!, $avatarBase64: String!) { + uploadUserAvatarBase64(id: $id, avatarBase64: $avatarBase64) + } +`) export const Avatar = () => { const [{ data }] = useMe() - + const uploadUserAvatar = useMutation(uploadUserAvatarMutation) if (!data?.me?.email) return null - const config = genConfig(data?.me?.email) + const onUploadAvatar = (e: ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : null; + if (file) { + const reader = new FileReader(); + + reader.onloadend = async () => { + try { + const imageString = reader.result as string; + const mimeHeaderMatcher = new RegExp("^data:image/.+;base64,") + const avatarBase64 = imageString.replace(mimeHeaderMatcher, "") + + const response = await uploadUserAvatar({ + avatarBase64, + id: data.me.id + }) + if (response?.error) throw response.error + if (response?.data?.uploadUserAvatarBase64 === false) throw new Error('Upload failed') + mutateAvatar(data.me.id) + toast.success('Avatar uploaded successfully.') + } catch (err: any) { + toast.error(err.message || 'Upload failed') + } + }; + + reader.readAsDataURL(file); + } + } return ( -
- +
+ + +
+ + +

Recommended: Square JPG, PNG, at least 1,000 pixels per side.

+
) } diff --git a/ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx b/ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx index 445fa1b1c5f0..5f9567003b50 100644 --- a/ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx +++ b/ee/tabby-ui/app/(dashboard)/profile/components/profile.tsx @@ -20,7 +20,7 @@ export default function Profile() { diff --git a/ee/tabby-ui/components/ui/avatar.tsx b/ee/tabby-ui/components/ui/avatar.tsx new file mode 100644 index 000000000000..51e507ba9d08 --- /dev/null +++ b/ee/tabby-ui/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/ee/tabby-ui/components/user-avatar.tsx b/ee/tabby-ui/components/user-avatar.tsx new file mode 100644 index 000000000000..2f8c1531fd4f --- /dev/null +++ b/ee/tabby-ui/components/user-avatar.tsx @@ -0,0 +1,64 @@ +'use client' + +import NiceAvatar, { genConfig } from 'react-nice-avatar' +import { mutate } from 'swr'; +import useSWRImmutable from 'swr/immutable' + +import { useMe } from '@/lib/hooks/use-me' +import fetcher from '@/lib/tabby/fetcher' +import { cn } from '@/lib/utils'; + +import { + Avatar as AvatarComponent, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { Skeleton } from '@/components/ui/skeleton' + +export function UserAvatar ({ + className +}: { + className?: string; +}) { + const [{ data }] = useMe() + const avatarUrl = !data?.me?.email ? null : `/avatar/${data.me.id}` + const { + data: avatarImageSrc, + isLoading + } = useSWRImmutable(avatarUrl, (url: string) => { + return fetcher(url, { + responseFormatter: async response => { + if (!response.ok) return undefined + const blob = await response.blob() + const buffer = Buffer.from(await blob.arrayBuffer()) + return `data:image/png;base64,${buffer.toString('base64')}` + } + }) + }) + + if (!data?.me?.email) return null + + if (isLoading) { + return ( + + ) + } + + if (!avatarImageSrc) { + const config = genConfig(data.me.email) + return ( + + ) + } + + return ( + + + {data.me?.email.substring(0, 2)} + + ) +} + +export const mutateAvatar = (userId: string) => { + mutate(`/avatar/${userId}`) +} \ No newline at end of file diff --git a/ee/tabby-ui/components/user-panel.tsx b/ee/tabby-ui/components/user-panel.tsx index 3a38cf8af6cf..78400d2f608b 100644 --- a/ee/tabby-ui/components/user-panel.tsx +++ b/ee/tabby-ui/components/user-panel.tsx @@ -1,5 +1,4 @@ import React from 'react' -import NiceAvatar, { genConfig } from 'react-nice-avatar' import { useMe } from '@/lib/hooks/use-me' import { useIsChatEnabled } from '@/lib/hooks/use-server-info' @@ -12,6 +11,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { UserAvatar } from '@/components/user-avatar' import { IconBackpack, @@ -39,14 +39,10 @@ export default function UserPanel() { return } - const config = genConfig(user.email) - return ( - - - + {user.email} diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json index ad594ee0bb5d..66b883dbe776 100644 --- a/ee/tabby-ui/package.json +++ b/ee/tabby-ui/package.json @@ -26,6 +26,7 @@ "@curvenote/ansi-to-react": "^7.0.0", "@hookform/resolvers": "^3.3.2", "@radix-ui/react-alert-dialog": "1.0.4", + "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "1.0.4", diff --git a/ee/tabby-ui/yarn.lock b/ee/tabby-ui/yarn.lock index 0f2e6ac5b026..8a712dda5500 100644 --- a/ee/tabby-ui/yarn.lock +++ b/ee/tabby-ui/yarn.lock @@ -1874,6 +1874,17 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-avatar@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" + integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-checkbox@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"