enhancement: built-in custom emoji and show metadata message (#5085)

* enhancement: built-in custom emoji and show metadata message

* chore: seperate render avatar as component

* fix: avatar on assistant screen
This commit is contained in:
Faisal Amir 2025-05-23 15:37:47 +07:00 committed by GitHub
parent 063292db6d
commit dfe15fac32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 239 additions and 24 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,31 @@
import React from 'react'
/**
* Checks if an avatar is a custom image (starts with '/images/')
*/
const isCustomImageAvatar = (avatar: React.ReactNode): avatar is string => {
return typeof avatar === 'string' && avatar.startsWith('/images/')
}
/**
* Component for rendering assistant avatars with consistent styling
*/
interface AvatarEmojiProps {
avatar?: React.ReactNode
fallback?: React.ReactNode
imageClassName?: string
textClassName?: string
}
export const AvatarEmoji: React.FC<AvatarEmojiProps> = ({
avatar,
fallback = '👋',
imageClassName = 'w-5 h-5 object-contain',
textClassName = 'text-base',
}) => {
if (isCustomImageAvatar(avatar)) {
return <img src={avatar} alt="Custom avatar" className={imageClassName} />
}
return <span className={textClassName}>{avatar || fallback}</span>
}

View File

@ -10,6 +10,7 @@ import { useAssistant } from '@/hooks/useAssistant'
import AddEditAssistant from './dialogs/AddEditAssistant'
import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
import { useThreads } from '@/hooks/useThreads'
import { AvatarEmoji } from '@/containers/AvatarEmoji'
const DropdownAssistant = () => {
const {
@ -34,13 +35,21 @@ const DropdownAssistant = () => {
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<div className="flex items-center justify-between gap-1">
<DropdownMenuTrigger asChild>
<button className="bg-main-view-fg/5 py-0.5 hover:bg-main-view-fg/8 px-2 rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40">
<span className="text-main-view-fg/80 truncate">
<button className="bg-main-view-fg/5 py-1 hover:bg-main-view-fg/8 px-2 rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40">
<div className="text-main-view-fg/80 flex items-center gap-1">
{selectedAssistant?.avatar && (
<span className="mr-1">{selectedAssistant?.avatar}</span>
<span className="shrink-0 w-4 h-4 relative flex items-center justify-center">
<AvatarEmoji
avatar={selectedAssistant.avatar}
imageClassName="object-cover"
textClassName="text-sm"
/>
</span>
)}
{selectedAssistant?.name || 'Jan'}
</span>
<div className="truncate max-w-30">
<span>{selectedAssistant?.name || 'Jan'}</span>
</div>
</div>
</button>
</DropdownMenuTrigger>
<div
@ -67,17 +76,25 @@ const DropdownAssistant = () => {
>
{assistants.map((assistant) => (
<div className="relative pr-6" key={assistant.id}>
<DropdownMenuItem className="flex justify-between items-center">
<span
className="truncate text-main-view-fg/70 flex-1 cursor-pointer"
<DropdownMenuItem asChild>
<div
className="text-main-view-fg/70 cursor-pointer"
onClick={() => {
setCurrentAssistant(assistant)
updateCurrentThreadAssistant(assistant)
}}
>
<span className="mr-1">{assistant?.avatar}</span>
{assistant.name}
</span>
<div className="shrink-0 relative w-4 h-4">
<AvatarEmoji
avatar={assistant?.avatar}
imageClassName="object-cover"
textClassName=""
/>
</div>
<div className="truncate text-left">
<span>{assistant.name}</span>
</div>
</div>
</DropdownMenuItem>
<div className="absolute top-1/2 -translate-y-1/2 right-1">
<div className="size-5 text-main-view-fg/50 cursor-pointer relative z-10 flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out">

View File

@ -7,6 +7,7 @@ import {
IconRefresh,
IconTrash,
IconPencil,
IconInfoCircle,
} from '@tabler/icons-react'
import { useAppState } from '@/hooks/useAppState'
import { cn } from '@/lib/utils'
@ -32,6 +33,9 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip'
import { formatDate } from '@/utils/formatDate'
import { AvatarEmoji } from '@/containers/AvatarEmoji'
import CodeEditor from '@uiw/react-textarea-code-editor'
import '@uiw/react-textarea-code-editor/dist.css'
const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false)
@ -154,7 +158,7 @@ export const ThreadContent = memo(
{item.content?.[0]?.text && item.role === 'user' && (
<div>
<div className="flex justify-end w-full">
<div className="bg-accent text-accent-fg p-2 rounded-md inline-block">
<div className="bg-main-view-fg/4 text-main-view-fg p-2 rounded-md inline-block">
<p className="select-text">{item.content?.[0].text.value}</p>
</div>
</div>
@ -237,8 +241,13 @@ export const ThreadContent = memo(
<>
{item.showAssistant && (
<div className="flex items-center gap-2 mb-3 text-main-view-fg/60">
<div className="flex items-center gap-2 size-8 rounded-md justify-center border border-main-view-fg/10 bg-main-view-fg/5 p-2">
<span className="text-base">{assistant?.avatar || '👋'}</span>
<div className="flex items-center gap-2 size-8 rounded-md justify-center border border-main-view-fg/10 bg-main-view-fg/5 p-1">
<AvatarEmoji
avatar={assistant?.avatar}
fallback="👋"
imageClassName="w-6 h-6 object-contain"
textClassName="text-base"
/>
</div>
<div className="flex flex-col">
@ -303,6 +312,55 @@ export const ThreadContent = memo(
<p>Delete</p>
</TooltipContent>
</Tooltip>
<Dialog>
<DialogTrigger>
<Tooltip>
<TooltipTrigger asChild>
<button className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
<IconInfoCircle size={16} />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Metadata</p>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Message Metadata</DialogTitle>
<div className="space-y-2">
<div className="border border-main-view-fg/10 rounded-md overflow-hidden">
<CodeEditor
value={JSON.stringify(
item.metadata || {},
null,
2
)}
language="json"
readOnly
style={{
fontFamily: 'ui-monospace',
backgroundColor: 'transparent',
height: '100%',
}}
className="w-full h-full"
/>
</div>
</div>
<DialogFooter className="mt-2 flex items-center">
<DialogClose asChild>
<Button
variant="link"
size="sm"
className="hover:no-underline"
>
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogHeader>
</DialogContent>
</Dialog>
{item.isLastMessage && (
<Tooltip>

View File

@ -20,6 +20,8 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTheme } from '@/hooks/useTheme'
import { teamEmoji } from '@/utils/teamEmoji'
import { AvatarEmoji } from '@/containers/AvatarEmoji'
interface AddEditAssistantProps {
open: boolean
@ -218,10 +220,15 @@ export default function AddEditAssistant({
<div className="relative">
<label className="text-sm mb-2 inline-block">Emoji</label>
<div
className="border rounded-sm p-2 w-9 h-9 flex items-center justify-center border-main-view-fg/10 cursor-pointer"
className="border rounded-sm p-1 w-9 h-9 flex items-center justify-center border-main-view-fg/10 cursor-pointer"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
>
{avatar || '😊'}
<AvatarEmoji
avatar={avatar}
fallback="😊"
imageClassName="w-5 h-5 object-contain"
textClassName=""
/>
</div>
<div className="relative" ref={emojiPickerRef}>
<EmojiPicker
@ -229,10 +236,16 @@ export default function AddEditAssistant({
theme={isDark ? ('dark' as Theme) : ('light' as Theme)}
className="!absolute !z-40 !overflow-y-auto top-2"
height={350}
customEmojis={teamEmoji}
lazyLoadEmojis
previewConfig={{ showPreview: false }}
onEmojiClick={(emojiData: EmojiClickData) => {
setAvatar(emojiData.emoji)
// For custom emojis, use the imageUrl instead of the emoji name
if (emojiData.isCustom && emojiData.imageUrl) {
setAvatar(emojiData.imageUrl)
} else {
setAvatar(emojiData.emoji)
}
setShowEmojiPicker(false)
}}
/>

View File

@ -28,12 +28,19 @@ export const useAssistant = create<AssistantState>()(
currentAssistant: defaultAssistant,
addAssistant: (assistant) =>
set({ assistants: [...get().assistants, assistant] }),
updateAssistant: (assistant) =>
updateAssistant: (assistant) => {
const state = get()
set({
assistants: get().assistants.map((a) =>
assistants: state.assistants.map((a) =>
a.id === assistant.id ? assistant : a
),
}),
// Update currentAssistant if it's the same assistant being updated
currentAssistant:
state.currentAssistant.id === assistant.id
? assistant
: state.currentAssistant,
})
},
deleteAssistant: (id) =>
set({ assistants: get().assistants.filter((a) => a.id !== id) }),
setCurrentAssistant: (assistant) => {

View File

@ -16,6 +16,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { AvatarEmoji } from '@/containers/AvatarEmoji'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.assistant as any)({
@ -67,10 +68,16 @@ function Assistant() {
>
<div className="flex items-center justify-between gap-2">
<h3 className="text-base font-medium text-main-view-fg/80">
{assistant.avatar && (
<span className="mr-1">{assistant.avatar}</span>
)}
{assistant.name}
<div className="flex items-center gap-1">
<span className="shrink-0 w-4 h-4 relative flex items-center justify-center">
<AvatarEmoji
avatar={assistant?.avatar}
imageClassName="object-cover"
textClassName="text-sm"
/>
</span>
<span>{assistant.name}</span>
</div>
</h3>
<div className="flex items-center gap-0.5">
{/* <div

View File

@ -0,0 +1,82 @@
export const teamEmoji = [
{
names: ['louis'],
imgUrl: '/images/emoji/louis.png',
id: 'louis',
},
{
names: ['emre'],
imgUrl: '/images/emoji/emre.png',
id: 'emre',
},
{
names: ['akarshan'],
imgUrl: '/images/emoji/akarshan.png',
id: 'akarshan',
},
{
names: ['daniel'],
imgUrl: '/images/emoji/daniel.png',
id: 'daniel',
},
{
names: ['gaunerst'],
imgUrl: '/images/emoji/gaunerst.png',
id: 'gaunerst',
},
{
names: ['yuuki'],
imgUrl: '/images/emoji/yuuki.png',
id: 'yuuki',
},
{
names: ['rach'],
imgUrl: '/images/emoji/rach.png',
id: 'rach',
},
{
names: ['faisal'],
imgUrl: '/images/emoji/faisal.png',
id: 'faisal',
},
{
names: ['nicole'],
imgUrl: '/images/emoji/nicole.png',
id: 'nicole',
},
{
names: ['doug'],
imgUrl: '/images/emoji/doug.png',
id: 'doug',
},
{
names: ['sang'],
imgUrl: '/images/emoji/sang.png',
id: 'sang',
},
{
names: ['alex'],
imgUrl: '/images/emoji/alex.png',
id: 'alex',
},
{
names: ['rex'],
imgUrl: '/images/emoji/rex.png',
id: 'rex',
},
{
names: ['hien'],
imgUrl: '/images/emoji/hien.png',
id: 'hien',
},
{
names: ['bach'],
imgUrl: '/images/emoji/bach.png',
id: 'bach',
},
{
names: ['alan'],
imgUrl: '/images/emoji/alan.png',
id: 'alan',
},
]