{chatList.length}
+ {onOpenLog && (
+ <>
+
+
+
+
+ >
+ )}
@@ -276,11 +318,6 @@ describe('ConversationList', () => {
vi.clearAllMocks()
mockChatConversationDetail = undefined
mockCompletionConversationDetail = undefined
- mockShowMessageLogModal = false
- mockShowPromptLogModal = false
- mockShowAgentLogModal = false
- mockCurrentLogItem = undefined
- mockCurrentLogModalActiveTab = 'messages'
mockDelAnnotation.mockResolvedValue(undefined)
mockFetchChatMessages.mockResolvedValue({
data: [],
@@ -352,7 +389,7 @@ describe('ConversationList', () => {
},
)
- it('should close the drawer, refresh, and clear modal flags', async () => {
+ it('should close the drawer and refresh', async () => {
mockChatConversationDetail = {
id: 'conversation-1',
created_at: 1710000000,
@@ -378,9 +415,6 @@ describe('ConversationList', () => {
fireEvent.click(await screen.findByRole('button', { name: /(?:^|\.)operation\.close(?=$|:)/ }))
expect(mockOnRefresh).toHaveBeenCalledTimes(1)
- expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(false)
- expect(mockSetShowAgentLogModal).toHaveBeenCalledWith(false)
- expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(false)
await waitFor(() => {
expect(onUrlUpdate).toHaveBeenCalled()
@@ -432,8 +466,6 @@ describe('ConversationList', () => {
],
has_more: false,
})
- mockShowMessageLogModal = true
- mockCurrentLogItem = { id: 'log-1' }
renderConversationList({
searchParams: '?page=2&conversation_id=conversation-1',
@@ -455,8 +487,6 @@ 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'))
@@ -501,65 +531,7 @@ describe('ConversationList', () => {
searchParams: '?conversation_id=conversation-1',
})
- expect(await screen.findByTestId('chat-panel')).toHaveAttribute(
- 'data-show-prompt-log',
- expected,
- )
- })
-
- 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)
+ expect(await screen.findByTestId('chat-panel')).toHaveAttribute('data-log-enabled', expected)
})
it('should render completion details and refetch after feedback updates', async () => {
@@ -586,8 +558,6 @@ describe('ConversationList', () => {
message_files: [{ url: 'https://example.com/file.txt' }],
},
}
- mockShowPromptLogModal = true
- mockCurrentLogItem = { id: 'log-2', log: [{ role: 'user', text: 'Prompt body' }] }
renderConversationList({
appDetail: { id: 'app-1', mode: AppModeEnum.COMPLETION } as any,
@@ -600,8 +570,8 @@ describe('ConversationList', () => {
})
expect(screen.getByTestId('var-panel')).toHaveTextContent('query:Question')
- expect(screen.getByTestId('text-generation')).toHaveAttribute('data-hide-log-action', 'true')
- expect(screen.getByTestId('prompt-log-modal')).toBeInTheDocument()
+ expect(screen.getByTestId('text-generation')).toHaveAttribute('data-log-enabled', 'false')
+ expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
fireEvent.click(screen.getByText('completion-feedback'))
@@ -683,7 +653,7 @@ describe('ConversationList', () => {
expect(screen.getAllByText('1').length).toBeGreaterThan(0)
})
- it('should support annotation changes, modal closing, and paginated scroll loading in the detail drawer', async () => {
+ it('should support annotation changes and paginated scroll loading in the detail drawer', async () => {
mockChatConversationDetail = {
id: 'conversation-1',
created_at: 1710000000,
@@ -706,8 +676,6 @@ describe('ConversationList', () => {
},
},
}
- mockShowMessageLogModal = true
- mockCurrentLogItem = { id: 'log-1' }
mockFetchChatMessages
.mockResolvedValueOnce({
data: [
@@ -747,10 +715,6 @@ describe('ConversationList', () => {
expect(mockDelAnnotation).toHaveBeenCalledWith('app-1', 'annotation-2')
})
- fireEvent.click(screen.getByText('close-message-log-modal'))
- expect(mockSetCurrentLogItem).toHaveBeenCalled()
- expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(false)
-
const scrollableDiv = document.getElementById('scrollableDiv') as HTMLDivElement
Object.defineProperty(scrollableDiv, 'clientHeight', { configurable: true, value: 100 })
Object.defineProperty(scrollableDiv, 'scrollHeight', { configurable: true, value: 500 })
@@ -765,44 +729,82 @@ describe('ConversationList', () => {
})
})
- it('should close the prompt log modal from completion detail drawers', async () => {
- mockCompletionConversationDetail = {
+ it.each(['workflow', 'agent', 'prompt'])(
+ 'should open and close the selected %s log within the conversation',
+ async (kind) => {
+ mockChatConversationDetail = {
+ id: 'conversation-1',
+ model_config: { user_input_form: [] },
+ message: { inputs: {} },
+ }
+ renderConversationList({
+ appDetail: { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT },
+ searchParams: '?conversation_id=conversation-1',
+ })
+ const user = userEvent.setup()
+ await user.click(await screen.findByRole('button', { name: `open-${kind}-log` }))
+ const modalKind = kind === 'workflow' ? 'message' : kind
+ expect(screen.getByTestId(`${modalKind}-log-modal`)).toBeInTheDocument()
+ for (const other of ['message', 'agent', 'prompt'].filter((value) => value !== modalKind))
+ expect(screen.queryByTestId(`${other}-log-modal`)).not.toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: `close-${modalKind}-log-modal` }))
+ expect(screen.queryByTestId(`${modalKind}-log-modal`)).not.toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: 'open-no-log' }))
+ expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
+ },
+ )
+
+ it('should discard the selected log when the conversation drawer closes and reopens', async () => {
+ mockChatConversationDetail = {
id: 'conversation-1',
- created_at: 1710000000,
- model_config: {
- model: 'gpt-4o-mini',
- user_input_form: [
- {
- query: {
- variable: 'query',
- },
- },
- ],
- },
- message: {
- id: 'message-1',
- answer: 'Generated output',
- inputs: {
- query: 'Question',
- },
- feedbacks: [],
- message_files: [{ url: 'https://example.com/file.txt' }],
- },
+ model_config: { user_input_form: [] },
+ message: { inputs: {} },
}
- mockShowPromptLogModal = true
- mockCurrentLogItem = { id: 'log-2', log: [{ role: 'user', text: 'Prompt body' }] }
-
renderConversationList({
- appDetail: { id: 'app-1', mode: AppModeEnum.COMPLETION } as any,
- logs: createCompletionLogs() as any,
- searchParams: '?page=2&conversation_id=conversation-1',
+ appDetail: { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT },
+ searchParams: '?conversation_id=conversation-1',
})
-
- expect(await screen.findByTestId('prompt-log-modal')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('close-prompt-log-modal'))
-
- expect(mockSetCurrentLogItem).toHaveBeenCalled()
- expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(false)
+ const user = userEvent.setup()
+ await user.click(await screen.findByRole('button', { name: 'open-workflow-log' }))
+ expect(screen.getByTestId('message-log-modal')).toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: /(?:^|\.)operation\.close(?=$|:)/ }))
+ await waitFor(() => expect(screen.queryByTestId('message-log-modal')).not.toBeInTheDocument())
+ await user.click(screen.getByText('hello world'))
+ await screen.findByTestId('chat-panel')
+ expect(screen.queryByTestId('message-log-modal')).not.toBeInTheDocument()
+ })
+ it('should clear a selected log when URL navigation changes or closes the conversation', async () => {
+ const user = userEvent.setup()
+ const queryClient = createAccountProfileQueryClient({ timezone: 'Asia/Shanghai' })
+ const appDetail = createAppDetailFixture({ mode: AppModeEnum.ADVANCED_CHAT })
+ const view = (searchParams: string) => (
+
+
+
+
+
+ )
+ mockChatConversationDetail = {
+ id: 'conversation-1',
+ model_config: { user_input_form: [] },
+ message: { inputs: {} },
+ }
+ const { rerender } = render(view('?conversation_id=conversation-1'))
+ await user.click(await screen.findByRole('button', { name: 'open-workflow-log' }))
+ expect(screen.getByTestId('message-log-modal')).toBeInTheDocument()
+ mockChatConversationDetail = { ...mockChatConversationDetail, id: 'conversation-2' }
+ rerender(view('?conversation_id=conversation-2'))
+ await waitFor(() => expect(screen.queryByTestId('message-log-modal')).not.toBeInTheDocument())
+ await user.click(await screen.findByRole('button', { name: 'open-agent-log' }))
+ expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument()
+ rerender(view(''))
+ await waitFor(() => expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument())
+ rerender(view('?conversation_id=conversation-2'))
+ await screen.findByTestId('chat-panel')
+ expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument()
})
})
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx
index 61f146d845b..df1f4486d2f 100644
--- a/web/app/components/app/log/list.tsx
+++ b/web/app/components/app/log/list.tsx
@@ -42,9 +42,7 @@ import { parseAsString, useQueryState } from 'nuqs'
import * as React from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useShallow } from 'zustand/react/shallow'
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 AgentLogModal from '@/app/components/base/agent-log-modal'
import Chat from '@/app/components/base/chat/chat'
@@ -81,7 +79,6 @@ import {
} from './list-utils'
import VarPanel from './var-panel'
-type AppStoreState = ReturnType
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
type ConversationSelection = ConversationListItem | { id: string; isPlaceholder?: true }
@@ -174,29 +171,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
select: (data) => data.profile.timezone ?? undefined,
})
const { formatTime } = useTimestamp()
- 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 [selectedLogItem, setSelectedLogItem] = useState()
const { t } = useTranslation(['appLog', 'common'])
const [hasMore, setHasMore] = useState(true)
const [varValues, setVarValues] = useState>({})
@@ -427,7 +402,13 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
const isChatMode = appDetail.mode !== AppModeEnum.COMPLETION
const isAdvanced = appDetail.mode === AppModeEnum.ADVANCED_CHAT
- const shouldShowPromptLogModal = showPromptLogModal && !!currentLogItem?.log
+ const logKind = selectedLogItem?.workflow_run_id
+ ? 'workflow'
+ : selectedLogItem?.agent_thoughts?.length
+ ? 'agent'
+ : selectedLogItem?.log
+ ? 'prompt'
+ : undefined
const varList = getDetailVarList(detail, varValues)
const message_files = getCompletionMessageFiles(detail, isChatMode)
@@ -518,7 +499,6 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
onRetry={noop}
supportFeedback
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
- hideLogAction
onFeedback={(feedback) => onFeedback(detail.message.id, feedback)}
isShowTextToSpeech
siteInfo={null}
@@ -547,9 +527,8 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
onAnnotationRemoved={handleAnnotationRemoved}
onFeedback={onFeedback}
noChatInput
- showPromptLog={isAdvanced}
+ onOpenLog={isAdvanced ? setSelectedLogItem : undefined}
hideProcessDetail
- hideLogModal
chatContainerInnerClassName="px-3"
switchSibling={switchSibling}
/>
@@ -589,9 +568,8 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
onAnnotationRemoved={handleAnnotationRemoved}
onFeedback={onFeedback}
noChatInput
- showPromptLog={isAdvanced}
+ onOpenLog={isAdvanced ? setSelectedLogItem : undefined}
hideProcessDetail
- hideLogModal
chatContainerInnerClassName="px-3"
switchSibling={switchSibling}
/>
@@ -607,38 +585,28 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {