fix: agent app log detail modal not display well (#38014)

This commit is contained in:
非法操作 2026-06-29 17:29:35 +08:00 committed by GitHub
parent 8a6ce28855
commit 23917c7b3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 169 additions and 11 deletions

View File

@ -21,6 +21,7 @@ let mockChatConversationDetail: Record<string, unknown> | undefined
let mockCompletionConversationDetail: Record<string, unknown> | undefined
let mockShowMessageLogModal = false
let mockShowPromptLogModal = false
let mockShowAgentLogModal = false
let mockCurrentLogItem: Record<string, unknown> | undefined
let mockCurrentLogModalActiveTab = 'messages'
@ -81,6 +82,7 @@ vi.mock('@/app/components/app/store', () => ({
setShowAgentLogModal: mockSetShowAgentLogModal,
setShowMessageLogModal: mockSetShowMessageLogModal,
showPromptLogModal: mockShowPromptLogModal,
showAgentLogModal: mockShowAgentLogModal,
currentLogModalActiveTab: mockCurrentLogModalActiveTab,
}),
}))
@ -126,6 +128,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({
onAnnotationEdited,
onAnnotationRemoved,
switchSibling,
hideLogModal,
}: {
chatList: Array<{ id: string }>
onFeedback: (mid: string, value: { rating: string, content?: string }) => Promise<boolean>
@ -133,8 +136,9 @@ vi.mock('@/app/components/base/chat/chat', () => ({
onAnnotationEdited: (query: string, answer: string, index: number) => void
onAnnotationRemoved: (index: number) => Promise<boolean>
switchSibling: (siblingMessageId: string) => void
hideLogModal?: boolean
}) => (
<div data-testid="chat-panel">
<div data-testid="chat-panel" data-hide-log-modal={String(hideLogModal)}>
<div>{chatList.length}</div>
<button onClick={() => void onFeedback('message-1', { rating: 'like', content: 'nice' })}>chat-feedback</button>
<button onClick={() => onAnnotationAdded('annotation-2', 'Admin', 'Edited question', 'Edited answer', 1)}>chat-add-annotation</button>
@ -145,6 +149,14 @@ vi.mock('@/app/components/base/chat/chat', () => ({
),
}))
vi.mock('@/app/components/base/agent-log-modal', () => ({
default: ({ floating, onCancel }: { floating?: boolean, onCancel: () => void }) => (
<div data-testid="agent-log-modal" data-floating={String(floating)}>
<button onClick={onCancel}>close-agent-log-modal</button>
</div>
),
}))
vi.mock('@/app/components/base/message-log-modal', () => ({
default: ({ onCancel }: { onCancel: () => void }) => (
<div data-testid="message-log-modal">
@ -255,6 +267,7 @@ describe('ConversationList', () => {
mockCompletionConversationDetail = undefined
mockShowMessageLogModal = false
mockShowPromptLogModal = false
mockShowAgentLogModal = false
mockCurrentLogItem = undefined
mockCurrentLogModalActiveTab = 'messages'
mockDelAnnotation.mockResolvedValue(undefined)
@ -383,6 +396,7 @@ describe('ConversationList', () => {
expect(screen.getByTestId('var-panel')).toHaveTextContent('query:Latest question')
expect(screen.getByTestId('model-info')).toHaveTextContent('gpt-4o')
expect(screen.getByTestId('chat-panel')).toHaveAttribute('data-hide-log-modal', 'true')
expect(screen.getByTestId('message-log-modal')).toBeInTheDocument()
fireEvent.click(screen.getByText('chat-feedback'))
@ -399,6 +413,61 @@ describe('ConversationList', () => {
})
})
it('should mount agent log modals from the detail panel instead of the nested chat layout', async () => {
mockChatConversationDetail = {
id: 'conversation-1',
created_at: 1710000000,
model_config: {
model: 'gpt-4o',
configs: {
introduction: 'Hello there',
},
user_input_form: [],
},
message: {
inputs: {},
},
}
mockShowAgentLogModal = true
mockCurrentLogItem = {
id: 'message-1',
conversationId: 'conversation-1',
}
mockFetchChatMessages.mockResolvedValue({
data: [
{
id: 'message-1',
answer: 'Assistant reply',
query: 'Latest question',
created_at: 1710000000,
inputs: {},
feedbacks: [],
message: [],
message_files: [],
agent_thoughts: [{ id: 'thought-1' }],
},
],
has_more: false,
})
renderConversationList({
searchParams: '?page=2&conversation_id=conversation-1',
})
await waitFor(() => {
expect(screen.getByTestId('chat-panel')).toBeInTheDocument()
})
expect(screen.getByTestId('chat-panel')).toHaveAttribute('data-hide-log-modal', 'true')
expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument()
expect(screen.getByTestId('agent-log-modal')).toHaveAttribute('data-floating', 'true')
fireEvent.click(screen.getByText('close-agent-log-modal'))
expect(mockSetCurrentLogItem).toHaveBeenCalled()
expect(mockSetShowAgentLogModal).toHaveBeenCalledWith(false)
})
it('should render completion details and refetch after feedback updates', async () => {
mockCompletionConversationDetail = {
id: 'conversation-1',
@ -424,7 +493,7 @@ describe('ConversationList', () => {
},
}
mockShowPromptLogModal = true
mockCurrentLogItem = { id: 'log-2' }
mockCurrentLogItem = { id: 'log-2', log: [{ role: 'user', text: 'Prompt body' }] }
renderConversationList({
appDetail: { id: 'app-1', mode: AppModeEnum.COMPLETION } as any,
@ -626,7 +695,7 @@ describe('ConversationList', () => {
},
}
mockShowPromptLogModal = true
mockCurrentLogItem = { id: 'log-2' }
mockCurrentLogItem = { id: 'log-2', log: [{ role: 'user', text: 'Prompt body' }] }
renderConversationList({
appDetail: { id: 'app-1', mode: AppModeEnum.COMPLETION } as any,

View File

@ -36,6 +36,7 @@ import ModelInfo from '@/app/components/app/log/model-info'
import { useStore as useAppStore } from '@/app/components/app/store'
import TextGeneration from '@/app/components/app/text-generate/item'
import ActionButton from '@/app/components/base/action-button'
import AgentLogModal from '@/app/components/base/agent-log-modal'
import Chat from '@/app/components/base/chat/chat'
import CopyIcon from '@/app/components/base/copy-icon'
import Loading from '@/app/components/base/loading'
@ -165,13 +166,25 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
})
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow((state: AppStoreState) => ({
const {
currentLogItem,
setCurrentLogItem,
showMessageLogModal,
setShowMessageLogModal,
showPromptLogModal,
setShowPromptLogModal,
showAgentLogModal,
setShowAgentLogModal,
currentLogModalActiveTab,
} = useAppStore(useShallow((state: AppStoreState) => ({
currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem,
showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
showPromptLogModal: state.showPromptLogModal,
setShowPromptLogModal: state.setShowPromptLogModal,
showAgentLogModal: state.showAgentLogModal,
setShowAgentLogModal: state.setShowAgentLogModal,
currentLogModalActiveTab: state.currentLogModalActiveTab,
})))
const { t } = useTranslation()
@ -395,6 +408,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const isChatMode = appDetail?.mode !== AppModeEnum.COMPLETION
const isAdvanced = appDetail?.mode === AppModeEnum.ADVANCED_CHAT
const shouldShowPromptLogModal = showPromptLogModal && !!currentLogItem?.log
const varList = getDetailVarList(detail, varValues)
const message_files = getCompletionMessageFiles(detail, isChatMode)
@ -507,6 +521,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
noChatInput
showPromptLog
hideProcessDetail
hideLogModal
chatContainerInnerClassName="px-3"
switchSibling={switchSibling}
/>
@ -546,6 +561,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
noChatInput
showPromptLog
hideProcessDetail
hideLogModal
chatContainerInnerClassName="px-3"
switchSibling={switchSibling}
/>
@ -574,7 +590,18 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
/>
</WorkflowContextProvider>
)}
{!isChatMode && showPromptLogModal && (
{showAgentLogModal && (
<AgentLogModal
floating
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowAgentLogModal(false)
}}
/>
)}
{shouldShowPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}

View File

@ -119,6 +119,17 @@ describe('AgentLogModal', () => {
})
})
it('should render the floating modal through a dialog portal', () => {
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
const { container } = render(<AgentLogModal {...mockProps} floating />)
const modal = screen.getByRole('dialog')
expect(container).not.toContainElement(modal)
expect(document.body).toContainElement(modal)
expect(modal).toHaveClass('fixed', 'z-50', 'w-[480px]!', 'left-[max(8px,calc(100vw-1136px))]!')
})
it('should call onCancel when close button is clicked', () => {
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
@ -158,4 +169,18 @@ describe('AgentLogModal', () => {
expect(mockProps.onCancel).not.toHaveBeenCalled()
})
it('should not use click-away to close the floating dialog', () => {
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
let clickAwayHandler!: (event: Event) => void
vi.mocked(useClickAway).mockImplementation((callback) => {
clickAwayHandler = callback
})
render(<AgentLogModal {...mockProps} floating />)
clickAwayHandler(new Event('click'))
expect(mockProps.onCancel).not.toHaveBeenCalled()
})
})

View File

@ -1,6 +1,7 @@
import type { FC } from 'react'
import type { IChatItem } from '@/app/components/base/chat/chat/type'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react'
import { useClickAway } from 'ahooks'
import { useEffect, useRef, useState } from 'react'
@ -10,11 +11,13 @@ import AgentLogDetail from './detail'
type AgentLogModalProps = Readonly<{
currentLogItem?: IChatItem
width: number
floating?: boolean
onCancel: () => void
}>
const AgentLogModal: FC<AgentLogModalProps> = ({
currentLogItem,
width,
floating,
onCancel,
}) => {
const { t } = useTranslation()
@ -22,7 +25,7 @@ const AgentLogModal: FC<AgentLogModalProps> = ({
const [mounted, setMounted] = useState(false)
useClickAway(() => {
if (mounted)
if (mounted && !floating)
onCancel()
}, ref)
@ -33,6 +36,44 @@ const AgentLogModal: FC<AgentLogModalProps> = ({
if (!currentLogItem || !currentLogItem.conversationId)
return null
const detailContent = (
<>
<AgentLogDetail
conversationID={currentLogItem.conversationId}
messageID={currentLogItem.id}
log={currentLogItem}
/>
</>
)
if (floating) {
return (
<Dialog
open
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<DialogContent
backdropClassName="bg-transparent!"
className="top-16! bottom-4! left-[max(8px,calc(100vw-1136px))]! flex max-h-none! w-[480px]! max-w-[calc(100vw-16px)]! translate-x-0! translate-y-0! flex-col overflow-hidden! rounded-xl! border-[0.5px]! border-components-panel-border! bg-components-panel-bg! p-0! pt-3! pb-3! shadow-xl!"
>
<DialogTitle className="text-md shrink-0 px-4 py-1 font-semibold text-text-primary">{t('runDetail.workflowTitle', { ns: 'appLog' })}</DialogTitle>
<button
type="button"
aria-label={t('operation.close', { ns: 'common' })}
className="absolute top-4 right-3 z-20 cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
onClick={onCancel}
>
<RiCloseLine className="size-4 text-text-tertiary" aria-hidden="true" />
</button>
{detailContent}
</DialogContent>
</Dialog>
)
}
return (
<div
className={cn('relative z-10 flex flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg py-3 shadow-xl')}
@ -54,11 +95,7 @@ const AgentLogModal: FC<AgentLogModalProps> = ({
>
<RiCloseLine className="size-4 text-text-tertiary" aria-hidden="true" />
</button>
<AgentLogDetail
conversationID={currentLogItem.conversationId}
messageID={currentLogItem.id}
log={currentLogItem}
/>
{detailContent}
</div>
)
}