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
BIN
web-app/public/images/emoji/akarshan.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
web-app/public/images/emoji/alan.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
web-app/public/images/emoji/alex.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
web-app/public/images/emoji/bach.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
web-app/public/images/emoji/daniel.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
web-app/public/images/emoji/doug.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
web-app/public/images/emoji/emre.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
web-app/public/images/emoji/faisal.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
web-app/public/images/emoji/gaunerst.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
web-app/public/images/emoji/hien.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
web-app/public/images/emoji/louis.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
web-app/public/images/emoji/nicole.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
web-app/public/images/emoji/rach.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
web-app/public/images/emoji/rex.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
web-app/public/images/emoji/sang.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
web-app/public/images/emoji/yuuki.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
31
web-app/src/containers/AvatarEmoji.tsx
Normal 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>
|
||||
}
|
||||
@ -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>
|
||||
)}
|
||||
{selectedAssistant?.name || 'Jan'}
|
||||
<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>
|
||||
)}
|
||||
<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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) => {
|
||||
// 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)
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
82
web-app/src/utils/teamEmoji.ts
Normal 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',
|
||||
},
|
||||
]
|
||||