diff --git a/apps/api/src/app/agents/agents.controller.ts b/apps/api/src/app/agents/agents.controller.ts index c7d22a271b0..0610b6a66e0 100644 --- a/apps/api/src/app/agents/agents.controller.ts +++ b/apps/api/src/app/agents/agents.controller.ts @@ -49,6 +49,7 @@ import { DeleteAgentCommand } from './usecases/delete-agent/delete-agent.command import { DeleteAgent } from './usecases/delete-agent/delete-agent.usecase'; import { GetAgentCommand } from './usecases/get-agent/get-agent.command'; import { GetAgent } from './usecases/get-agent/get-agent.usecase'; +import { type AgentEmojiEntry, ListAgentEmoji } from './usecases/list-agent-emoji/list-agent-emoji.usecase'; import { ListAgentIntegrationsCommand } from './usecases/list-agent-integrations/list-agent-integrations.command'; import { ListAgentIntegrations } from './usecases/list-agent-integrations/list-agent-integrations.usecase'; import { ListAgentsCommand } from './usecases/list-agents/list-agents.command'; @@ -77,9 +78,22 @@ export class AgentsController { private readonly addAgentIntegrationUsecase: AddAgentIntegration, private readonly listAgentIntegrationsUsecase: ListAgentIntegrations, private readonly updateAgentIntegrationUsecase: UpdateAgentIntegration, - private readonly removeAgentIntegrationUsecase: RemoveAgentIntegration + private readonly removeAgentIntegrationUsecase: RemoveAgentIntegration, + private readonly listAgentEmojiUsecase: ListAgentEmoji ) {} + @Get('/emoji') + @ApiOperation({ + summary: 'List available emoji', + description: + 'Returns the set of well-known cross-platform emoji names supported for agent reactions. ' + + 'Each entry includes the normalized name and a unicode representation for display.', + }) + @RequirePermissions(PermissionsEnum.AGENT_READ) + listAgentEmoji(): Promise { + return this.listAgentEmojiUsecase.execute(); + } + @Post('/') @ApiResponse(AgentResponseDto, 201) @ApiOperation({ diff --git a/apps/api/src/app/agents/dtos/agent-behavior.dto.ts b/apps/api/src/app/agents/dtos/agent-behavior.dto.ts index b9e1444a1c9..1a13e3ff3e5 100644 --- a/apps/api/src/app/agents/dtos/agent-behavior.dto.ts +++ b/apps/api/src/app/agents/dtos/agent-behavior.dto.ts @@ -1,24 +1,29 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsBoolean, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator'; +import { IsBoolean, IsOptional, ValidateIf, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; +import { IsWellKnownEmoji } from '../validators/is-well-known-emoji.validator'; export class AgentReactionSettingsDto { @ApiPropertyOptional({ - description: 'Emoji reaction for incoming messages. Emoji name string to customize, null to disable. Default: "eyes" (👀)', + description: + 'Cross-platform emoji name for incoming messages (e.g. "eyes", "thumbs_up"). ' + + 'Set to null to disable. Default: "eyes"', default: 'eyes', }) @IsOptional() @ValidateIf((_, value) => value !== null) - @IsString() + @IsWellKnownEmoji() onMessageReceived?: string | null; @ApiPropertyOptional({ - description: 'Emoji reaction when a conversation is resolved. Emoji name string to customize, null to disable. Default: "check" (✅)', + description: + 'Cross-platform emoji name for resolved conversations (e.g. "check", "star"). ' + + 'Set to null to disable. Default: "check"', default: 'check', }) @IsOptional() @ValidateIf((_, value) => value !== null) - @IsString() + @IsWellKnownEmoji() onResolved?: string | null; } diff --git a/apps/api/src/app/agents/e2e/agent-webhook.e2e.ts b/apps/api/src/app/agents/e2e/agent-webhook.e2e.ts index f1efc829084..f374ae80eea 100644 --- a/apps/api/src/app/agents/e2e/agent-webhook.e2e.ts +++ b/apps/api/src/app/agents/e2e/agent-webhook.e2e.ts @@ -7,6 +7,7 @@ import { import { testServer } from '@novu/testing'; import { expect } from 'chai'; import sinon from 'sinon'; +import type { EmojiValue } from 'chat'; import { AgentEventEnum } from '../dtos/agent-event.enum'; import { AgentConfigResolver } from '../services/agent-config-resolver.service'; import { AgentInboundHandler, InboundReactionEvent } from '../services/agent-inbound-handler.service'; @@ -20,6 +21,10 @@ import { } from './helpers/agent-test-setup'; import { buildSlackChallenge, signSlackRequest } from './helpers/providers/slack'; +function mockEmoji(name: string): EmojiValue { + return { name, toJSON: () => `{{emoji:${name}}}`, toString: () => `{{emoji:${name}}}` }; +} + function mockSentMessage() { return { addReaction: async () => {}, @@ -359,7 +364,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => { bridgeCalls = []; const reactionEvent: InboundReactionEvent = { - emoji: { name: 'thumbs_up' }, + emoji: mockEmoji('thumbs_up'), added: true, messageId: msg.id, message: msg as any, @@ -379,7 +384,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => { it('should skip reaction when no conversation exists for the thread', async () => { const reactionEvent: InboundReactionEvent = { - emoji: { name: 'wave' }, + emoji: mockEmoji('wave'), added: true, messageId: 'msg-orphan', thread: mockThread(`T_NOCONV_${Date.now()}`) as any, @@ -392,7 +397,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => { it('should skip reaction when thread context is missing', async () => { const reactionEvent: InboundReactionEvent = { - emoji: { name: 'fire' }, + emoji: mockEmoji('fire'), added: false, messageId: 'msg-no-thread', }; @@ -410,7 +415,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => { bridgeCalls = []; const reactionEvent: InboundReactionEvent = { - emoji: { name: 'tada' }, + emoji: mockEmoji('tada'), added: true, messageId: msg.id, message: msg as any, @@ -443,7 +448,7 @@ describe('Agent Webhook - inbound flow #novu-v2', () => { ); const reactionEvent: InboundReactionEvent = { - emoji: { name: 'heart' }, + emoji: mockEmoji('heart'), added: true, messageId: msg.id, message: msg as any, diff --git a/apps/api/src/app/agents/services/agent-config-resolver.service.ts b/apps/api/src/app/agents/services/agent-config-resolver.service.ts index 639d63c495b..ecb6f219e04 100644 --- a/apps/api/src/app/agents/services/agent-config-resolver.service.ts +++ b/apps/api/src/app/agents/services/agent-config-resolver.service.ts @@ -8,9 +8,22 @@ import { IntegrationRepository, } from '@novu/dal'; import { FeatureFlagsKeysEnum } from '@novu/shared'; +import type { WellKnownEmoji } from 'chat'; import { AgentPlatformEnum } from '../dtos/agent-platform.enum'; +import { esmImport } from '../utils/esm-import'; import { resolveAgentPlatform } from '../utils/provider-to-platform'; +let cachedEmojiNames: Set | null = null; + +async function loadEmojiNames(): Promise> { + if (cachedEmojiNames) return cachedEmojiNames; + + const { DEFAULT_EMOJI_MAP } = await esmImport('chat'); + cachedEmojiNames = new Set(Object.keys(DEFAULT_EMOJI_MAP)); + + return cachedEmojiNames; +} + export interface ResolvedAgentConfig { platform: AgentPlatformEnum; credentials: ICredentialsEntity; @@ -21,25 +34,36 @@ export interface ResolvedAgentConfig { integrationIdentifier: string; integrationId: string; thinkingIndicatorEnabled: boolean; - reactionOnMessageReceived: string | null; - reactionOnResolved: string | null; + reactionOnMessageReceived: WellKnownEmoji | null; + reactionOnResolved: WellKnownEmoji | null; bridgeUrl?: string; devBridgeUrl?: string; devBridgeActive?: boolean; } -const DEFAULT_REACTION_ON_MESSAGE = 'eyes'; -const DEFAULT_REACTION_ON_RESOLVED = 'check'; +const DEFAULT_REACTION_ON_MESSAGE: WellKnownEmoji = 'eyes'; +const DEFAULT_REACTION_ON_RESOLVED: WellKnownEmoji = 'check'; function resolveThinkingIndicator(agent: { behavior?: { thinkingIndicatorEnabled?: boolean } }): boolean { return agent.behavior?.thinkingIndicatorEnabled !== false; } -function resolveReaction(value: string | null | undefined, defaultEmoji: string): string | null { +async function resolveReaction( + value: string | null | undefined, + defaultEmoji: WellKnownEmoji, + log: PinoLogger +): Promise { if (value === null) return null; if (value === undefined) return defaultEmoji; - return value; + const known = await loadEmojiNames(); + if (!known.has(value)) { + log.warn(`Unknown emoji "${value}" in agent config, falling back to default "${defaultEmoji}"`); + + return defaultEmoji; + } + + return value as WellKnownEmoji; } @Injectable() @@ -133,11 +157,16 @@ export class AgentConfigResolver { integrationIdentifier, integrationId: integration._id, thinkingIndicatorEnabled: resolveThinkingIndicator(agent), - reactionOnMessageReceived: resolveReaction( + reactionOnMessageReceived: await resolveReaction( agent.behavior?.reactions?.onMessageReceived, - DEFAULT_REACTION_ON_MESSAGE + DEFAULT_REACTION_ON_MESSAGE, + this.logger + ), + reactionOnResolved: await resolveReaction( + agent.behavior?.reactions?.onResolved, + DEFAULT_REACTION_ON_RESOLVED, + this.logger ), - reactionOnResolved: resolveReaction(agent.behavior?.reactions?.onResolved, DEFAULT_REACTION_ON_RESOLVED), bridgeUrl: agent.bridgeUrl, devBridgeUrl: agent.devBridgeUrl, devBridgeActive: agent.devBridgeActive, diff --git a/apps/api/src/app/agents/services/agent-inbound-handler.service.ts b/apps/api/src/app/agents/services/agent-inbound-handler.service.ts index 561aed44963..d97b9e2b49d 100644 --- a/apps/api/src/app/agents/services/agent-inbound-handler.service.ts +++ b/apps/api/src/app/agents/services/agent-inbound-handler.service.ts @@ -6,7 +6,7 @@ import { ConversationRepository, SubscriberRepository, } from '@novu/dal'; -import type { Message, Thread } from 'chat'; +import type { EmojiValue, Message, Thread } from 'chat'; import { AgentEventEnum } from '../dtos/agent-event.enum'; import { PLATFORMS_WITHOUT_TYPING_INDICATOR } from '../dtos/agent-platform.enum'; import { HandleAgentReplyCommand } from '../usecases/handle-agent-reply/handle-agent-reply.command'; @@ -22,7 +22,7 @@ const ONBOARDING_NO_BRIDGE_REPLY_MARKDOWN = `*You're connected to Novu* Your bot is linked successfully. Go back to the *Novu dashboard* to complete onboarding.`; export interface InboundReactionEvent { - emoji: { name: string }; + emoji: EmojiValue; added: boolean; messageId: string; message?: Message; diff --git a/apps/api/src/app/agents/services/chat-sdk.service.ts b/apps/api/src/app/agents/services/chat-sdk.service.ts index 524481c416c..6c1ab7020e7 100644 --- a/apps/api/src/app/agents/services/chat-sdk.service.ts +++ b/apps/api/src/app/agents/services/chat-sdk.service.ts @@ -1,11 +1,12 @@ import { BadRequestException, forwardRef, Inject, Injectable, OnModuleDestroy } from '@nestjs/common'; import { PinoLogger } from '@novu/application-generic'; -import type { Chat, Message, Thread } from 'chat'; +import type { Chat, EmojiValue, Message, ReactionEvent, Thread } from 'chat'; import { Request as ExpressRequest, Response as ExpressResponse } from 'express'; import { LRUCache } from 'lru-cache'; import { AgentEventEnum } from '../dtos/agent-event.enum'; import { AgentPlatformEnum } from '../dtos/agent-platform.enum'; import type { ReplyContentDto } from '../dtos/agent-reply-payload.dto'; +import { esmImport } from '../utils/esm-import'; import { sendWebResponse, toWebRequest } from '../utils/express-to-web-request'; import { AgentConfigResolver, ResolvedAgentConfig } from './agent-config-resolver.service'; import { AgentInboundHandler } from './agent-inbound-handler.service'; @@ -26,11 +27,6 @@ import { AgentInboundHandler } from './agent-inbound-handler.service'; * credentials.phoneNumberIdentification → phoneNumberId */ -// Chat SDK packages are ESM-only; SWC rewrites import() → require() for CJS output. -// Wrapping in new Function prevents SWC from seeing the import() keyword. -// eslint-disable-next-line @typescript-eslint/no-implied-eval -const esmImport = new Function('specifier', 'return import(specifier)') as (s: string) => Promise; - const MAX_CACHED_INSTANCES = 200; const INSTANCE_TTL_MS = 1000 * 60 * 30; @@ -133,14 +129,15 @@ export class ChatSdkService implements OnModuleDestroy { platform: string, platformThreadId: string, platformMessageId: string, - emoji: string + emojiName: string ): Promise { const config = await this.agentConfigResolver.resolve(agentId, integrationIdentifier); const instanceKey = `${agentId}:${integrationIdentifier}`; const chat = await this.getOrCreate(instanceKey, agentId, config.platform, config); const adapter = chat.getAdapter(platform); - await adapter.removeReaction(platformThreadId, platformMessageId, emoji); + const resolved = await this.resolveEmoji(emojiName); + await adapter.removeReaction(platformThreadId, platformMessageId, resolved); } async reactToMessage( @@ -149,14 +146,25 @@ export class ChatSdkService implements OnModuleDestroy { platform: string, platformThreadId: string, platformMessageId: string, - emoji: string + emojiName: string ): Promise { const config = await this.agentConfigResolver.resolve(agentId, integrationIdentifier); const instanceKey = `${agentId}:${integrationIdentifier}`; const chat = await this.getOrCreate(instanceKey, agentId, config.platform, config); const adapter = chat.getAdapter(platform); - await adapter.addReaction(platformThreadId, platformMessageId, emoji); + const resolved = await this.resolveEmoji(emojiName); + await adapter.addReaction(platformThreadId, platformMessageId, resolved); + } + + private async resolveEmoji(name: string): Promise { + const { getEmoji } = await esmImport('chat'); + const resolved = getEmoji(name); + if (!resolved) { + throw new Error(`Unknown emoji name: "${name}". Use GET /agents/emoji to list supported options.`); + } + + return resolved; } private async getOrCreate( @@ -379,14 +387,14 @@ export class ChatSdkService implements OnModuleDestroy { } }); - cached.chat.onReaction(async (event: any) => { + cached.chat.onReaction(async (event: ReactionEvent) => { try { await this.inboundHandler.handleReaction(agentId, cached.config, { emoji: event.emoji, added: event.added, messageId: event.messageId, message: event.message, - thread: event.thread, + thread: event.thread as Thread | undefined, user: event.user, }); } catch (err) { diff --git a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts index 6ba3eee43e8..b47b65962ed 100644 --- a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts +++ b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.command.ts @@ -1,6 +1,6 @@ +import type { Signal } from '@novu/framework'; import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; -import type { Signal } from '@novu/framework'; import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; import { ReplyContentDto } from '../../dtos/agent-reply-payload.dto'; diff --git a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts index 2cf1fd4b3d6..1e873751f2a 100644 --- a/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts +++ b/apps/api/src/app/agents/usecases/handle-agent-reply/handle-agent-reply.usecase.ts @@ -104,7 +104,7 @@ export class HandleAgentReply { } if (command.resolve) { - await this.executeResolveSignal(command, config!, conversation, channel, command.resolve); + await this.resolveConversation(command, config!, conversation, channel, command.resolve); } return { status: 'ok' }; @@ -255,12 +255,12 @@ export class HandleAgentReply { ]); } - private async executeResolveSignal( + private async resolveConversation( command: HandleAgentReplyCommand, config: ResolvedAgentConfig, conversation: ConversationEntity, channel: ConversationChannel, - signal: { summary?: string } + options: { summary?: string } ): Promise { await Promise.all([ this.conversationRepository.updateStatus( @@ -276,8 +276,8 @@ export class HandleAgentReply { integrationId: channel._integrationId, platformThreadId: channel.platformThreadId, agentId: command.agentIdentifier, - content: signal.summary ?? 'Conversation resolved', - signalData: { type: 'resolve', payload: signal.summary ? { summary: signal.summary } : undefined }, + content: options.summary ?? 'Conversation resolved', + signalData: { type: 'resolve', payload: options.summary ? { summary: options.summary } : undefined }, environmentId: command.environmentId, organizationId: command.organizationId, }), diff --git a/apps/api/src/app/agents/usecases/index.ts b/apps/api/src/app/agents/usecases/index.ts index 8807ddbce8c..746ba75dd57 100644 --- a/apps/api/src/app/agents/usecases/index.ts +++ b/apps/api/src/app/agents/usecases/index.ts @@ -3,6 +3,7 @@ import { CreateAgent } from './create-agent/create-agent.usecase'; import { DeleteAgent } from './delete-agent/delete-agent.usecase'; import { GetAgent } from './get-agent/get-agent.usecase'; import { HandleAgentReply } from './handle-agent-reply/handle-agent-reply.usecase'; +import { ListAgentEmoji } from './list-agent-emoji/list-agent-emoji.usecase'; import { ListAgentIntegrations } from './list-agent-integrations/list-agent-integrations.usecase'; import { ListAgents } from './list-agents/list-agents.usecase'; import { RemoveAgentIntegration } from './remove-agent-integration/remove-agent-integration.usecase'; @@ -16,6 +17,7 @@ export const USE_CASES = [ UpdateAgent, DeleteAgent, AddAgentIntegration, + ListAgentEmoji, ListAgentIntegrations, UpdateAgentIntegration, RemoveAgentIntegration, diff --git a/apps/api/src/app/agents/usecases/list-agent-emoji/list-agent-emoji.usecase.ts b/apps/api/src/app/agents/usecases/list-agent-emoji/list-agent-emoji.usecase.ts new file mode 100644 index 00000000000..6691f44182c --- /dev/null +++ b/apps/api/src/app/agents/usecases/list-agent-emoji/list-agent-emoji.usecase.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import type { EmojiFormats } from 'chat'; +import { esmImport } from '../../utils/esm-import'; + +export interface AgentEmojiEntry { + name: string; + unicode: string; +} + +@Injectable() +export class ListAgentEmoji { + private cached: AgentEmojiEntry[] | null = null; + + async execute(): Promise { + if (this.cached) return this.cached; + + const { DEFAULT_EMOJI_MAP } = await esmImport('chat'); + const map = DEFAULT_EMOJI_MAP as Record; + + this.cached = Object.entries(map) + .map(([name, formats]) => { + const raw = formats.gchat ?? formats.slack; + const unicode = Array.isArray(raw) ? raw[0] : raw; + + return unicode ? { name, unicode } : null; + }) + .filter((e): e is AgentEmojiEntry => e !== null); + + return this.cached; + } +} diff --git a/apps/api/src/app/agents/utils/esm-import.ts b/apps/api/src/app/agents/utils/esm-import.ts new file mode 100644 index 00000000000..5373f441488 --- /dev/null +++ b/apps/api/src/app/agents/utils/esm-import.ts @@ -0,0 +1,4 @@ +// Chat SDK packages are ESM-only; SWC rewrites import() → require() for CJS output. +// Wrapping in new Function prevents SWC from seeing the import() keyword. +// eslint-disable-next-line @typescript-eslint/no-implied-eval +export const esmImport = new Function('specifier', 'return import(specifier)') as (s: string) => Promise; diff --git a/apps/api/src/app/agents/validators/is-well-known-emoji.validator.ts b/apps/api/src/app/agents/validators/is-well-known-emoji.validator.ts new file mode 100644 index 00000000000..a2a6b6eff3a --- /dev/null +++ b/apps/api/src/app/agents/validators/is-well-known-emoji.validator.ts @@ -0,0 +1,46 @@ +import { + registerDecorator, + type ValidationArguments, + type ValidationOptions, + ValidatorConstraint, + type ValidatorConstraintInterface, +} from 'class-validator'; +import { esmImport } from '../utils/esm-import'; + +let cachedNames: Set | null = null; + +async function loadEmojiNames(): Promise> { + if (cachedNames) return cachedNames; + + const { DEFAULT_EMOJI_MAP } = await esmImport('chat'); + cachedNames = new Set(Object.keys(DEFAULT_EMOJI_MAP)); + + return cachedNames; +} + +@ValidatorConstraint({ async: true, name: 'isWellKnownEmoji' }) +export class IsWellKnownEmojiConstraint implements ValidatorConstraintInterface { + async validate(value: unknown): Promise { + if (typeof value !== 'string') return false; + + const names = await loadEmojiNames(); + + return names.has(value); + } + + defaultMessage(args: ValidationArguments): string { + return `${JSON.stringify(args.value)} is not a supported emoji name. Use GET /agents/emoji to list available options.`; + } +} + +export function IsWellKnownEmoji(validationOptions?: ValidationOptions) { + return (object: object, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [], + validator: IsWellKnownEmojiConstraint, + }); + }; +} diff --git a/apps/dashboard/src/api/agents.ts b/apps/dashboard/src/api/agents.ts index 1ef7856d7a5..6b716d364d1 100644 --- a/apps/dashboard/src/api/agents.ts +++ b/apps/dashboard/src/api/agents.ts @@ -8,6 +8,8 @@ const AGENT_DETAIL_QUERY_KEY = 'fetchAgent' as const; const AGENT_INTEGRATIONS_QUERY_KEY = 'fetchAgentIntegrations' as const; +const AGENT_EMOJI_QUERY_KEY = 'fetchAgentEmoji' as const; + export function getAgentDetailQueryKey(environmentId: string | undefined, identifier: string | undefined) { return [AGENT_DETAIL_QUERY_KEY, environmentId, identifier] as const; @@ -251,3 +253,18 @@ export function removeAgentIntegration( { environment } ); } + +export type AgentEmojiEntry = { + name: string; + unicode: string; +}; + +export function getAgentEmojiQueryKey() { + return [AGENT_EMOJI_QUERY_KEY] as const; +} + +export async function listAgentEmoji(environment: IEnvironment, signal?: AbortSignal): Promise { + const response = await get<{ data: AgentEmojiEntry[] }>('/agents/emoji', { environment, signal }); + + return response.data; +} diff --git a/apps/dashboard/src/components/agents/agent-behavior-section.tsx b/apps/dashboard/src/components/agents/agent-behavior-section.tsx index d7da479bf9d..ce1a85a7571 100644 --- a/apps/dashboard/src/components/agents/agent-behavior-section.tsx +++ b/apps/dashboard/src/components/agents/agent-behavior-section.tsx @@ -1,6 +1,31 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; import { RiExpandUpDownLine } from 'react-icons/ri'; +import { type AgentEmojiEntry, getAgentEmojiQueryKey, listAgentEmoji } from '@/api/agents'; import { HelpTooltipIndicator } from '@/components/primitives/help-tooltip-indicator'; import { Switch } from '@/components/primitives/switch'; +import { useEnvironment } from '@/context/environment/hooks'; + +const DEFAULT_REACTION_ON_MESSAGE = 'eyes'; +const DEFAULT_REACTION_ON_RESOLVED = 'check'; + +function useAgentEmoji() { + const { currentEnvironment } = useEnvironment(); + + const { data: emojiList = [] } = useQuery({ + queryKey: getAgentEmojiQueryKey(), + queryFn: ({ signal }) => listAgentEmoji(currentEnvironment!, signal), + enabled: !!currentEnvironment, + staleTime: Infinity, + }); + + const unicodeMap = useMemo( + () => new Map(emojiList.map((e: AgentEmojiEntry) => [e.name, e.unicode])), + [emojiList] + ); + + return { emojiList, unicodeMap }; +} function SectionHeader({ children }: { children: React.ReactNode }) { return ( @@ -37,6 +62,10 @@ function EmojiPickerButton({ emoji }: { emoji: string }) { } export function AgentBehaviorSection() { + const { unicodeMap } = useAgentEmoji(); + const messageEmoji = unicodeMap.get(DEFAULT_REACTION_ON_MESSAGE) ?? ''; + const resolvedEmoji = unicodeMap.get(DEFAULT_REACTION_ON_RESOLVED) ?? ''; + return (
Agent behavior @@ -79,11 +108,11 @@ export function AgentBehaviorSection() { - + - +