From 7a7dc6011a43d75533e98e43f7b8a937d87f7c53 Mon Sep 17 00:00:00 2001 From: Stefan Yoshovski Date: Sat, 26 Sep 2026 12:36:08 +0000 Subject: [PATCH] fix(agent-v2): use icon_url for image avatars in the configure preview (#42977) Co-authored-by: Claude Opus 5.5 Co-authored-by: yyh --- .../agent-orchestrate-panel-content.tsx | 7 -- .../__tests__/navigation.spec.tsx | 41 +++++++ .../configure/components/composer-session.tsx | 8 -- .../__tests__/chat-mode-routing.spec.tsx | 27 +++- .../preview/__tests__/preview-avatar.spec.tsx | 115 ++++++++++++++++++ .../components/preview/chat-runtime.tsx | 15 +-- .../components/preview/chat-session.tsx | 14 +-- .../components/preview/preview-chat.tsx | 33 +++-- .../agent-v2/agent-detail/navigation.tsx | 11 +- 9 files changed, 211 insertions(+), 60 deletions(-) create mode 100644 web/features/agent-v2/agent-detail/configure/components/preview/__tests__/preview-avatar.spec.tsx diff --git a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx index 053435bb47e..36ad0058818 100644 --- a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx +++ b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-panel-content.tsx @@ -742,13 +742,6 @@ function WorkflowInlineAgentConfigureWorkspaceContent({ [0]['agentIconType'] - } agentName={composerState?.agent?.name} agentSoulConfig={buildDraft.agentSoulConfig} clearChatList={clearPreviewChat} diff --git a/web/features/agent-v2/agent-detail/__tests__/navigation.spec.tsx b/web/features/agent-v2/agent-detail/__tests__/navigation.spec.tsx index 5ebc5365e84..468d94add94 100644 --- a/web/features/agent-v2/agent-detail/__tests__/navigation.spec.tsx +++ b/web/features/agent-v2/agent-detail/__tests__/navigation.spec.tsx @@ -137,6 +137,47 @@ describe('AgentDetailSection', () => { expect(agentAvatar).toHaveClass('h-10', 'w-10', 'rounded-full') }) + it('renders an uploaded sidebar avatar using its signed URL', () => { + mocks.queryData = createAgent({ + icon_type: 'image', + icon: 'uploaded-file-id', + icon_url: 'https://files.example.com/avatar.png?sign=signature', + }) + renderAgentDetailSection() + + expect(screen.getByRole('img', { hidden: true })).toHaveAttribute( + 'src', + 'https://files.example.com/avatar.png?sign=signature', + ) + }) + + it('preserves an external sidebar avatar when icon_url is null', () => { + mocks.queryData = createAgent({ + icon_type: 'link', + icon: 'https://example.com/avatar.png', + icon_url: null, + }) + renderAgentDetailSection() + + expect(screen.getByRole('img', { hidden: true })).toHaveAttribute( + 'src', + 'https://example.com/avatar.png', + ) + }) + + it('shows a fallback without exposing the file ID when the signed URL is missing', () => { + mocks.queryData = createAgent({ + icon_type: 'image', + icon: 'uploaded-file-id', + icon_url: null, + }) + renderAgentDetailSection() + + expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument() + expect(screen.queryByText('uploaded-file-id')).not.toBeInTheDocument() + expect(screen.getByText('🤖')).toBeInTheDocument() + }) + it.each([null, '', ' '])( 'omits an empty role while keeping the agent accessible (%s)', (role) => { diff --git a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx index 119144d799c..a00d5ad3dbb 100644 --- a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx @@ -2,7 +2,6 @@ import type { AgentAppDetailWithSite, - AgentIconType, AgentSoulConfig, } from '@dify/contracts/api/console/agent/types.gen' import type { useAgentConfigureData } from '../hooks' @@ -152,7 +151,6 @@ function AgentConfigurePageComposerSession({ normalAgentSoulConfig: agentSoulConfig, onModeChange: onRightPanelModeChange, }) - const agentIconType = agentQuery.data?.icon_type as AgentIconType | null | undefined const refreshDebugConversationMutation = useMutation( consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions({ onSuccess: ({ @@ -231,7 +229,6 @@ function AgentConfigurePageComposerSession({ > configureData: ReturnType isRefreshingDebugConversation: boolean @@ -554,9 +549,6 @@ function AgentConfigurePageComposerContent({ ( + {children} + ), + }) +} + describe('Agent chat mode request routing', () => { beforeEach(() => { runtimePropsMock.mockClear() @@ -56,12 +77,11 @@ describe('Agent chat mode request routing', () => { }) it('should show the unconfigured notice below the Preview description', () => { - render() + render() const renderEmptyState = runtimePropsMock.mock.calls.at(-1)?.[0].renderEmptyState const emptyStateView = render( renderEmptyState({ - agentName: 'Research Agent', showUnconfiguredNotice: true, }), ) @@ -79,7 +99,6 @@ describe('Agent chat mode request routing', () => { emptyStateView.rerender( renderEmptyState({ - agentName: 'Research Agent', showUnconfiguredNotice: false, }), ) diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/preview-avatar.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/preview-avatar.spec.tsx new file mode 100644 index 00000000000..29316d0e4e2 --- /dev/null +++ b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/preview-avatar.spec.tsx @@ -0,0 +1,115 @@ +import type { AgentChatRuntimeProps } from '../chat-runtime' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { act, screen } from '@testing-library/react' +import { consoleQuery } from '@/service/console' +import { render } from '@/test/console/render' +import { createAgentFixture } from '@/test/fixtures/agent' +import { AgentPreviewChat } from '../preview-chat' + +vi.mock('../chat-runtime', () => ({ + AgentChatRuntime: ({ renderEmptyState }: AgentChatRuntimeProps) => + renderEmptyState({ showUnconfiguredNotice: false }), +})) + +const commonProps = { + agentId: 'agent-1', + clearChatList: false, + onClearChatListChange: vi.fn(), +} + +const agentQueryOptions = consoleQuery.agent.byAgentId.get.queryOptions({ + input: { params: { agent_id: commonProps.agentId } }, +}) + +function renderPreview(agent: ReturnType) { + const queryClient = new QueryClient({ + defaultOptions: { queries: { staleTime: Infinity, retry: false } }, + }) + queryClient.setQueryData(agentQueryOptions.queryKey, agent) + + return { + queryClient, + ...render( + + + , + ), + } +} + +describe('Agent preview avatar', () => { + it('renders an uploaded image from its signed URL instead of its file ID', () => { + renderPreview( + createAgentFixture({ + icon_type: 'image', + icon: 'uploaded-file-id', + icon_url: 'https://files.example.com/avatar.png?sign=signature', + }), + ) + + expect(screen.getByRole('img', { name: 'app icon' })).toHaveAttribute( + 'src', + 'https://files.example.com/avatar.png?sign=signature', + ) + }) + + it('renders an external image from the raw icon when icon_url is null', () => { + renderPreview( + createAgentFixture({ + icon_type: 'link', + icon: 'https://example.com/avatar.png', + icon_url: null, + }), + ) + + expect(screen.getByRole('img', { name: 'app icon' })).toHaveAttribute( + 'src', + 'https://example.com/avatar.png', + ) + }) + + it('shows a fallback without exposing the file ID when its signed URL is unavailable', () => { + renderPreview( + createAgentFixture({ + icon_type: 'image', + icon: 'uploaded-file-id', + icon_url: null, + }), + ) + + expect(screen.queryByRole('img', { name: 'app icon' })).not.toBeInTheDocument() + expect(screen.queryByText('uploaded-file-id')).not.toBeInTheDocument() + expect(screen.getByText('🤖')).toBeInTheDocument() + }) + + it('updates the avatar and name when the shared Agent query changes', async () => { + const { queryClient } = renderPreview( + createAgentFixture({ + name: 'Original Agent', + icon_type: 'image', + icon: 'uploaded-file-id', + icon_url: 'https://files.example.com/avatar.png?sign=signature', + }), + ) + + expect(screen.getByRole('img', { name: 'app icon' })).toBeInTheDocument() + expect(screen.getByText('Original Agent')).toBeInTheDocument() + + await act(async () => { + queryClient.setQueryData( + agentQueryOptions.queryKey, + createAgentFixture({ + name: 'Updated Agent', + icon_type: 'emoji', + icon: '🚀', + icon_url: null, + }), + ) + }) + + expect(await screen.findByText('Updated Agent')).toBeInTheDocument() + expect(screen.queryByText('Original Agent')).not.toBeInTheDocument() + expect(screen.queryByRole('img', { name: 'app icon' })).not.toBeInTheDocument() + expect(screen.getByText('🚀')).toBeInTheDocument() + }) +}) diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx index 731ba2bebeb..b8370a7a8b7 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx @@ -1,6 +1,6 @@ 'use client' -import type { AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen' +import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen' import type { ReactNode, Ref } from 'react' import type { AgentChatMessageSender, AgentPreviewChatController } from './chat-conversation' import type { AnswerActionPosition } from '@/app/components/base/chat/chat/answer/operation' @@ -12,19 +12,12 @@ import { getFormattedAgentDebugChatTree } from './chat-history' import { AgentPreviewChatSession } from './chat-session' export type AgentChatRuntimeEmptyStateProps = { - agentIcon?: string | null - agentIconBackground?: string | null - agentIconType?: AgentIconType | null - agentName?: string showUnconfiguredNotice: boolean } export type AgentChatRuntimeProps = { agentId: string answerActionPosition?: AnswerActionPosition - agentIcon?: string | null - agentIconBackground?: string | null - agentIconType?: AgentIconType | null agentName?: string agentSoulConfig?: AgentSoulConfig clearChatList: boolean @@ -49,9 +42,6 @@ export type AgentChatRuntimeProps = { export function AgentChatRuntime({ agentId, answerActionPosition, - agentIcon, - agentIconBackground, - agentIconType, agentName, agentSoulConfig, clearChatList, @@ -118,9 +108,6 @@ export function AgentChatRuntime({ conversationSessionKey={conversationSessionKey} agentId={agentId} answerActionPosition={answerActionPosition} - agentIcon={agentIcon} - agentIconBackground={agentIconBackground} - agentIconType={agentIconType} agentName={agentName} agentSoulConfig={agentSoulConfig} clearChatList={clearChatList} diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/chat-session.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/chat-session.tsx index de5c66f21f6..18ace33bbfe 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/chat-session.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/chat-session.tsx @@ -1,6 +1,6 @@ 'use client' -import type { AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen' +import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen' import type { ReactNode, Ref } from 'react' import type { AgentChatMessageSender, @@ -29,9 +29,6 @@ export function AgentPreviewChatSession({ conversationSessionKey, agentId, answerActionPosition, - agentIcon, - agentIconBackground, - agentIconType, agentName, agentSoulConfig, clearChatList, @@ -57,10 +54,7 @@ export function AgentPreviewChatSession({ conversationSessionKey: string agentId: string answerActionPosition?: AgentChatRuntimeProps['answerActionPosition'] - agentIcon?: string | null - agentIconBackground?: string | null - agentIconType?: AgentIconType | null - agentName?: string + agentName?: AgentChatRuntimeProps['agentName'] agentSoulConfig?: AgentSoulConfig clearChatList: boolean controllerRef?: Ref @@ -234,10 +228,6 @@ export function AgentPreviewChatSession({ {isEmptyChat && (
{renderEmptyState({ - agentIcon, - agentIconBackground, - agentIconType, - agentName, showUnconfiguredNotice, })}
diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/preview-chat.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/preview-chat.tsx index fbbaae4fcba..7df5be2a7aa 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/preview-chat.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/preview-chat.tsx @@ -1,8 +1,10 @@ 'use client' import type { AgentChatRuntimeEmptyStateProps, AgentChatRuntimeProps } from './chat-runtime' +import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' +import { consoleQuery } from '@/service/console' import { AgentChatRuntime } from './chat-runtime' import { sendPreviewChatMessage } from './preview-chat-request' import { AgentUnconfiguredNotice } from './unconfigured-notice' @@ -13,15 +15,22 @@ type AgentPreviewChatProps = Omit< > function AgentPreviewChatEmptyState({ - agentIcon, - agentIconBackground, - agentIconType, - agentName, + agentId, showUnconfiguredNotice, -}: AgentChatRuntimeEmptyStateProps) { +}: AgentChatRuntimeEmptyStateProps & { agentId: AgentChatRuntimeProps['agentId'] }) { const { t } = useTranslation(['agentV2']) - const imageUrl = agentIconType === 'image' || agentIconType === 'link' ? agentIcon : undefined - const iconType = imageUrl ? 'image' : agentIconType + const { data: agent } = useQuery( + consoleQuery.agent.byAgentId.get.queryOptions({ + input: { params: { agent_id: agentId } }, + }), + ) + const imageUrl = + agent?.icon_type === 'image' + ? agent.icon_url + : agent?.icon_type === 'link' + ? agent.icon + : undefined + const iconType = imageUrl ? 'image' : agent?.icon_type return ( <> @@ -29,13 +38,13 @@ function AgentPreviewChatEmptyState({ size="xxl" rounded iconType={iconType} - icon={agentIcon ?? undefined} - background={agentIconBackground} + icon={agent?.icon_type === 'emoji' ? (agent.icon ?? undefined) : undefined} + background={agent?.icon_background} imageUrl={imageUrl} className="bg-background-default" />
- {agentName || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName'])} + {agent?.name || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName'])}

{t(($) => $['agentDetail.configure.preview.empty.description'])} @@ -57,7 +66,9 @@ export function AgentPreviewChat(props: AgentPreviewChatProps) { name: agentName, })} sendMessage={sendPreviewChatMessage} - renderEmptyState={(emptyStateProps) => } + renderEmptyState={(emptyStateProps) => ( + + )} /> ) } diff --git a/web/features/agent-v2/agent-detail/navigation.tsx b/web/features/agent-v2/agent-detail/navigation.tsx index 1ba403c15b7..5baa17acf28 100644 --- a/web/features/agent-v2/agent-detail/navigation.tsx +++ b/web/features/agent-v2/agent-detail/navigation.tsx @@ -1,6 +1,5 @@ 'use client' -import type { AgentIconType } from '@dify/contracts/api/console/agent/types.gen' import type { AgentDetailSectionKey } from './section' import { Breadcrumb, @@ -179,8 +178,12 @@ export function AgentDetailSection({ expand = true }: AgentDetailSectionProps) { sectionAccess[item.labelKey.slice('agentDetail.sections.'.length) as AgentDetailSectionKey], ) const imageUrl = - agent?.icon_type === 'image' || agent?.icon_type === 'link' ? agent.icon : undefined - const iconType = (imageUrl ? 'image' : agent?.icon_type) as AgentIconType | null | undefined + agent?.icon_type === 'image' + ? agent.icon_url + : agent?.icon_type === 'link' + ? agent.icon + : undefined + const iconType = imageUrl ? 'image' : agent?.icon_type return (

@@ -207,7 +210,7 @@ export function AgentDetailSection({ expand = true }: AgentDetailSectionProps) { size="large" rounded iconType={iconType} - icon={agent?.icon ?? undefined} + icon={agent?.icon_type === 'emoji' ? (agent.icon ?? undefined) : undefined} background={agent?.icon_background} imageUrl={imageUrl} />