mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
feat(agent): add emoji avatar support and refactor avatar handling
- Rename getAgentAvatar to getAgentDefaultAvatar for clarity - Add EmojiAvatarWithPicker component for emoji selection - Update AgentLabel to support both default and emoji avatars - Add AvatarSetting component for avatar configuration - Modify agent configuration schema to support emoji avatars
This commit is contained in:
parent
b46237296e
commit
2ccfde1ba4
22
src/renderer/src/components/Avatar/EmojiAvatarWithPicker.tsx
Normal file
22
src/renderer/src/components/Avatar/EmojiAvatarWithPicker.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react'
|
||||
import React from 'react'
|
||||
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
|
||||
type Props = {
|
||||
emoji: string
|
||||
onPick: (emoji: string) => void
|
||||
}
|
||||
|
||||
export const EmojiAvatarWithPicker: React.FC<Props> = ({ emoji, onPick }) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button size="sm" startContent={<span className="text-lg">{emoji}</span>} isIconOnly />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<EmojiPicker onEmojiClick={onPick}></EmojiPicker>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@ -11,7 +11,7 @@ export const DEFAULT_CLAUDE_CODE_CONFIG: Omit<AgentBase, 'model'> = {
|
||||
...DEFAULT_AGENT_CONFIG
|
||||
} as const
|
||||
|
||||
export const getAgentAvatar = (type: AgentType): string => {
|
||||
export const getAgentDefaultAvatar = (type: AgentType): string => {
|
||||
switch (type) {
|
||||
case 'claude-code':
|
||||
return ClaudeAvatar
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Avatar, Button, Chip, cn } from '@heroui/react'
|
||||
import { Button, Chip, cn } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { getAgentAvatar } from '@renderer/config/agent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings/AgentSettingsPopup'
|
||||
import { AgentLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import { AgentEntity } from '@renderer/types'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||
import { FC, memo, useCallback } from 'react'
|
||||
import { FC, memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// const logger = loggerService.withContext('AgentItem')
|
||||
@ -21,24 +21,13 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
|
||||
const { t } = useTranslation()
|
||||
const { sessions } = useSessions(agent.id)
|
||||
|
||||
const AgentLabel = useCallback(() => {
|
||||
const displayName = agent.name ?? agent.id
|
||||
const avatar = getAgentAvatar(agent.type)
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6" src={avatar} name={displayName} />
|
||||
<span className="text-sm">{displayName}</span>
|
||||
</div>
|
||||
)
|
||||
}, [agent.id, agent.name, agent.type])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
|
||||
<AssistantNameRow className="name flex w-full justify-between" title={agent.name ?? agent.id}>
|
||||
<AgentLabel />
|
||||
<AgentLabel agent={agent} />
|
||||
{isActive && (
|
||||
<Chip
|
||||
variant="bordered"
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { Avatar } from '@heroui/react'
|
||||
import { getAgentDefaultAvatar } from '@renderer/config/agent'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
||||
import { GetAgentResponse } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { AccessibleDirsSetting } from './AccessibleDirsSetting'
|
||||
import { AvatarSetting } from './AvatarSetting'
|
||||
import { DescriptionSetting } from './DescriptionSetting'
|
||||
import { ModelSetting } from './ModelSetting'
|
||||
import { NameSetting } from './NameSetting'
|
||||
import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
// const logger = loggerService.withContext('AgentEssentialSettings')
|
||||
|
||||
@ -25,8 +29,12 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update
|
||||
<SettingsContainer>
|
||||
<SettingsItem inline>
|
||||
<SettingsTitle>{t('agent.type.label')}</SettingsTitle>
|
||||
<AgentLabel type={agent.type} />
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar src={getAgentDefaultAvatar(agent.type)} className="h-6 w-6 text-lg" />
|
||||
<span>{(agent?.name ?? agent?.type) ? getAgentTypeLabel(agent.type) : ''}</span>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
<AvatarSetting agent={agent} update={update} />
|
||||
<NameSetting base={agent} update={update} />
|
||||
<ModelSetting base={agent} update={update} />
|
||||
<AccessibleDirsSetting base={agent} update={update} />
|
||||
|
||||
@ -104,14 +104,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
afterClose={afterClose}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
title={
|
||||
<AgentLabel
|
||||
type={agent?.type ?? 'claude-code'}
|
||||
name={agent?.name}
|
||||
classNames={{ name: 'text-lg font-extrabold' }}
|
||||
avatarProps={{ size: 'sm' }}
|
||||
/>
|
||||
}
|
||||
title={<AgentLabel agent={agent} classNames={{ name: 'text-lg font-extrabold' }} avatarProps={{ size: 'sm' }} />}
|
||||
transitionName="animation-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import { Avatar, Radio, RadioGroup } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { EmojiAvatarWithPicker } from '@renderer/components/Avatar/EmojiAvatarWithPicker'
|
||||
import { getAgentDefaultAvatar } from '@renderer/config/agent'
|
||||
import { AgentEntity, isAgentType, UpdateAgentForm } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import z from 'zod'
|
||||
|
||||
import { SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
export interface AvatarSettingsProps {
|
||||
agent: AgentEntity
|
||||
update: (form: UpdateAgentForm) => Promise<void>
|
||||
}
|
||||
|
||||
const optionsSchema = z.enum(['default', 'emoji'])
|
||||
|
||||
type AvatarOption = z.infer<typeof optionsSchema>
|
||||
|
||||
const options = {
|
||||
DEFAULT: 'default',
|
||||
EMOJI: 'emoji'
|
||||
} as const satisfies Record<string, AvatarOption>
|
||||
|
||||
const logger = loggerService.withContext('AvatarSetting')
|
||||
|
||||
export const AvatarSetting: React.FC<AvatarSettingsProps> = ({ agent, update }) => {
|
||||
const { t } = useTranslation()
|
||||
const isDefault = isAgentType(agent.configuration?.avatar)
|
||||
const [avatarOption, setAvatarOption] = useState<AvatarOption>(isDefault ? options.DEFAULT : options.EMOJI)
|
||||
const [emoji, setEmoji] = useState(isDefault ? '⭐️' : (agent.configuration?.avatar ?? '⭐️'))
|
||||
|
||||
const updateAvatar = useCallback(
|
||||
(avatar: string) => {
|
||||
const payload = {
|
||||
id: agent.id,
|
||||
// hard-encoded default values. better to implement incremental update for configuration
|
||||
configuration: {
|
||||
...agent.configuration,
|
||||
permission_mode: agent.configuration?.permission_mode ?? 'default',
|
||||
max_turns: agent.configuration?.max_turns ?? 100,
|
||||
avatar
|
||||
}
|
||||
} satisfies UpdateAgentForm
|
||||
update(payload)
|
||||
},
|
||||
[agent, update]
|
||||
)
|
||||
|
||||
const handleOptionChange = useCallback(
|
||||
(value: string) => {
|
||||
const result = optionsSchema.safeParse(value)
|
||||
if (!result.success) {
|
||||
logger.error('Invalid option', { value })
|
||||
return
|
||||
}
|
||||
const option = result.data
|
||||
setAvatarOption(option)
|
||||
if (option === agent?.configuration?.avatar) return
|
||||
|
||||
switch (option) {
|
||||
case options.DEFAULT:
|
||||
updateAvatar(agent.type)
|
||||
break
|
||||
case options.EMOJI:
|
||||
updateAvatar(emoji)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
[agent?.configuration?.avatar, agent.type, emoji, updateAvatar]
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingsItem inline>
|
||||
<SettingsTitle>{t('common.avatar')}</SettingsTitle>
|
||||
<RadioGroup size="sm" orientation="horizontal" value={avatarOption} onValueChange={handleOptionChange}>
|
||||
<Radio value={options.DEFAULT} classNames={{ label: 'flex flex-row' }}>
|
||||
<Avatar className="h-6 w-6" src={getAgentDefaultAvatar(agent.type)} />
|
||||
</Radio>
|
||||
<Radio value={options.EMOJI}>
|
||||
<EmojiAvatarWithPicker
|
||||
emoji={emoji}
|
||||
onPick={(emoji: string) => {
|
||||
setEmoji(emoji)
|
||||
if (emoji === agent?.configuration?.avatar) return
|
||||
updateAvatar(emoji)
|
||||
}}
|
||||
/>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</SettingsItem>
|
||||
)
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import { Avatar, AvatarProps, cn } from '@heroui/react'
|
||||
import { getAgentAvatar } from '@renderer/config/agent'
|
||||
import EmojiIcon from '@renderer/components/EmojiIcon'
|
||||
import { getAgentDefaultAvatar } from '@renderer/config/agent'
|
||||
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
||||
import { AgentSessionEntity, AgentType } from '@renderer/types'
|
||||
import { AgentEntity, AgentSessionEntity, isAgentType } from '@renderer/types'
|
||||
import { Menu, Modal } from 'antd'
|
||||
import React, { ReactNode } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@ -22,8 +23,7 @@ export const SettingsTitle: React.FC<SettingsTitleProps> = ({ children, actions
|
||||
}
|
||||
|
||||
export type AgentLabelProps = {
|
||||
type: AgentType
|
||||
name?: string
|
||||
agent: AgentEntity | undefined | null
|
||||
classNames?: {
|
||||
container?: string
|
||||
avatar?: string
|
||||
@ -32,11 +32,16 @@ export type AgentLabelProps = {
|
||||
avatarProps?: AvatarProps
|
||||
}
|
||||
|
||||
export const AgentLabel: React.FC<AgentLabelProps> = ({ type, name, classNames, avatarProps }) => {
|
||||
export const AgentLabel: React.FC<AgentLabelProps> = ({ agent, classNames, avatarProps }) => {
|
||||
const isDefault = isAgentType(agent?.configuration?.avatar)
|
||||
const src = isDefault ? getAgentDefaultAvatar(agent.type) : undefined
|
||||
const emoji = isDefault ? undefined : agent?.configuration?.avatar
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', classNames?.container)}>
|
||||
<Avatar src={getAgentAvatar(type)} title={type} {...avatarProps} className={cn('h-5 w-5', classNames?.avatar)} />
|
||||
<span className={classNames?.name}>{name ?? getAgentTypeLabel(type)}</span>
|
||||
{isDefault && <Avatar src={src} {...avatarProps} className={cn('h-6 w-6 text-lg', classNames?.avatar)} />}
|
||||
{!isDefault && <EmojiIcon emoji={emoji || '⭐️'} className={classNames?.avatar} />}
|
||||
<span className={classNames?.name}>{(agent?.name ?? agent?.type) ? getAgentTypeLabel(agent.type) : ''}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -83,7 +88,7 @@ export const SettingsItem: React.FC<SettingsItemProps> = ({
|
||||
|
||||
export const SettingsContainer: React.FC<React.ComponentPropsWithRef<'div'>> = ({ children, className, ...props }) => {
|
||||
return (
|
||||
<div className={cn('flex flex-1 flex-col overflow-auto pr-2', className)} {...props}>
|
||||
<div className={cn('flex flex-1 flex-col overflow-y-auto overflow-x-hidden pr-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -52,12 +52,12 @@ export type SlashCommand = z.infer<typeof SlashCommandSchema>
|
||||
// ------------------ Agent configuration & base schema ------------------
|
||||
export const AgentConfigurationSchema = z
|
||||
.object({
|
||||
avatar: z.string().optional(), // URL or path to avatar image
|
||||
avatar: z.string().optional(), // agent type as mark of default avatar; single emoji; URL or path to avatar image.
|
||||
slash_commands: z.array(z.string()).optional(), // Array of slash commands to trigger the agent, this is from agent init response
|
||||
|
||||
// https://docs.claude.com/en/docs/claude-code/sdk/sdk-permissions#mode-specific-behaviors
|
||||
permission_mode: PermissionModeSchema.default('default'), // Permission mode, default to 'default'
|
||||
max_turns: z.number().default(100) // Maximum number of interaction turns, default to 100
|
||||
permission_mode: PermissionModeSchema.optional().default('default'), // Permission mode, default to 'default'
|
||||
max_turns: z.number().optional().default(100) // Maximum number of interaction turns, default to 100
|
||||
})
|
||||
.loose()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user