mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
fix(agent-v2): use icon_url for image avatars in the configure preview (#42977)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
9 files changed
+211
-60
No files matched your search
-7
@@ -742,13 +742,6 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
<AgentConfigureRightPanelChat
|
||||
agentId={agentId}
|
||||
answerActionPosition="below"
|
||||
agentIcon={composerState?.agent?.icon}
|
||||
agentIconBackground={composerState?.agent?.icon_background}
|
||||
agentIconType={
|
||||
composerState?.agent?.icon_type as Parameters<
|
||||
typeof AgentConfigureRightPanelChat
|
||||
>[0]['agentIconType']
|
||||
}
|
||||
agentName={composerState?.agent?.name}
|
||||
agentSoulConfig={buildDraft.agentSoulConfig}
|
||||
clearChatList={clearPreviewChat}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
<AgentConfigurePageComposerContent
|
||||
agentId={agentId}
|
||||
agentIconType={agentIconType}
|
||||
buildDraft={buildDraft}
|
||||
configureData={configureData}
|
||||
isRefreshingDebugConversation={
|
||||
@@ -254,7 +251,6 @@ function AgentConfigurePageComposerSession({
|
||||
|
||||
function AgentConfigurePageComposerContent({
|
||||
agentId,
|
||||
agentIconType,
|
||||
buildDraft,
|
||||
configureData,
|
||||
isRefreshingDebugConversation,
|
||||
@@ -269,7 +265,6 @@ function AgentConfigurePageComposerContent({
|
||||
onSelectVersion,
|
||||
}: {
|
||||
agentId: string
|
||||
agentIconType: AgentIconType | null | undefined
|
||||
buildDraft: ReturnType<typeof useAgentConfigureBuildDraftData>
|
||||
configureData: ReturnType<typeof useAgentConfigureData>
|
||||
isRefreshingDebugConversation: boolean
|
||||
@@ -554,9 +549,6 @@ function AgentConfigurePageComposerContent({
|
||||
<AgentConfigureRightPanelChat
|
||||
agentId={agentId}
|
||||
answerActionPosition="below"
|
||||
agentIcon={agentQuery.data?.icon}
|
||||
agentIconBackground={agentQuery.data?.icon_background}
|
||||
agentIconType={agentIconType}
|
||||
agentName={agentQuery.data?.name}
|
||||
agentSoulConfig={buildDraft.agentSoulConfig}
|
||||
clearChatList={clearChatByMode[rightPanelChatMode]}
|
||||
|
||||
+23
-4
@@ -1,7 +1,11 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { AgentChatMessageSender } from '../chat-conversation'
|
||||
import type { AgentChatRuntimeProps } from '../chat-runtime'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { render } from '@/test/console/render'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { render as renderConsole } from '@/test/console/render'
|
||||
import { createAgentFixture } from '@/test/fixtures/agent'
|
||||
import { AgentBuildChat } from '../build-chat'
|
||||
import { sendBuildChatMessage } from '../build-chat-request'
|
||||
import { AgentPreviewChat } from '../preview-chat'
|
||||
@@ -28,6 +32,23 @@ const commonProps = {
|
||||
onClearChatListChange: vi.fn(),
|
||||
}
|
||||
|
||||
function render(ui: ReactElement) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: Infinity, retry: false } },
|
||||
})
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.agent.byAgentId.get.queryKey({
|
||||
input: { params: { agent_id: commonProps.agentId } },
|
||||
}),
|
||||
createAgentFixture({ name: 'Research Agent' }),
|
||||
)
|
||||
return renderConsole(ui, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
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(<AgentPreviewChat {...commonProps} agentName="Research Agent" />)
|
||||
render(<AgentPreviewChat {...commonProps} />)
|
||||
|
||||
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,
|
||||
}),
|
||||
)
|
||||
|
||||
+115
@@ -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<typeof createAgentFixture>) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: Infinity, retry: false } },
|
||||
})
|
||||
queryClient.setQueryData(agentQueryOptions.queryKey, agent)
|
||||
|
||||
return {
|
||||
queryClient,
|
||||
...render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AgentPreviewChat {...commonProps} />
|
||||
</QueryClientProvider>,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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}
|
||||
|
||||
@@ -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<AgentPreviewChatController>
|
||||
@@ -234,10 +228,6 @@ export function AgentPreviewChatSession({
|
||||
{isEmptyChat && (
|
||||
<div className="w-full p-3 pb-0">
|
||||
{renderEmptyState({
|
||||
agentIcon,
|
||||
agentIconBackground,
|
||||
agentIconType,
|
||||
agentName,
|
||||
showUnconfiguredNotice,
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<div className="mt-3 max-w-full truncate system-md-medium text-text-secondary">
|
||||
{agentName || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName'])}
|
||||
{agent?.name || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName'])}
|
||||
</div>
|
||||
<p className="mt-1 max-w-full body-md-regular text-text-tertiary">
|
||||
{t(($) => $['agentDetail.configure.preview.empty.description'])}
|
||||
@@ -57,7 +66,9 @@ export function AgentPreviewChat(props: AgentPreviewChatProps) {
|
||||
name: agentName,
|
||||
})}
|
||||
sendMessage={sendPreviewChatMessage}
|
||||
renderEmptyState={(emptyStateProps) => <AgentPreviewChatEmptyState {...emptyStateProps} />}
|
||||
renderEmptyState={(emptyStateProps) => (
|
||||
<AgentPreviewChatEmptyState {...emptyStateProps} agentId={props.agentId} />
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={cn('flex min-h-0 flex-1 flex-col', expand ? 'px-2 pb-2' : 'pb-2')}>
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user