mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
refactor(logs): pass app identity into log inspectors (#43261)
This commit is contained in:
12 files changed
+234
-177
No files matched your search
@@ -593,17 +593,9 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/base/agent-log-modal/detail.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/agent-log-modal/index.stories.tsx": {
|
||||
"no-console": {
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/agent-log-modal/index.tsx": {
|
||||
@@ -1141,17 +1133,9 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/base/message-log-modal/__tests__/index.spec.tsx": {
|
||||
"dify/require-disable-directive-description": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/message-log-modal/index.stories.tsx": {
|
||||
"no-console": {
|
||||
"count": 1
|
||||
},
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/new-audio-button/index.tsx": {
|
||||
|
||||
@@ -638,6 +638,7 @@ const Debug: FC<IDebug> = ({
|
||||
{selectedLogItem &&
|
||||
(selectedLogItem.agent_thoughts?.length ? (
|
||||
<AgentLogModal
|
||||
appId={appId}
|
||||
width={logModalWidth}
|
||||
currentLogItem={selectedLogItem}
|
||||
onCancel={() => setSelectedLogItem(undefined)}
|
||||
|
||||
@@ -588,6 +588,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
{logKind === 'workflow' && (
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
appId={appDetail.id}
|
||||
width={width}
|
||||
currentLogItem={selectedLogItem}
|
||||
onCancel={() => setSelectedLogItem(undefined)}
|
||||
@@ -596,6 +597,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
)}
|
||||
{logKind === 'agent' && (
|
||||
<AgentLogModal
|
||||
appId={appDetail.id}
|
||||
floating
|
||||
width={width}
|
||||
currentLogItem={selectedLogItem}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { ComponentProps, ReactNode } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogDetail from '../detail'
|
||||
|
||||
@@ -27,10 +26,6 @@ vi.mock('@/app/notifications', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn((selector) => selector({ appDetail: { id: 'app-id' } })),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/status', () => ({
|
||||
default: ({
|
||||
status,
|
||||
@@ -116,6 +111,7 @@ const createMockResponse = (
|
||||
describe('AgentLogDetail', () => {
|
||||
const renderComponent = (props: Partial<ComponentProps<typeof AgentLogDetail>> = {}) => {
|
||||
const defaultProps: ComponentProps<typeof AgentLogDetail> = {
|
||||
appId: 'app-id',
|
||||
conversationID: 'conv-id',
|
||||
messageID: 'msg-id',
|
||||
log: createMockLog(),
|
||||
@@ -228,20 +224,6 @@ describe('AgentLogDetail', () => {
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should not fetch data when app detail is unavailable', async () => {
|
||||
vi.mocked(useAppStore).mockImplementationOnce((selector) =>
|
||||
selector({ appDetail: undefined } as never),
|
||||
)
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchAgentLogDetail).not.toHaveBeenCalled()
|
||||
})
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should notify on API error', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockRejectedValue(new Error('API Error'))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogModal from '../index'
|
||||
@@ -25,10 +25,6 @@ vi.mock('@/app/notifications', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn((selector) => selector({ appDetail: { id: 'app-id' } })),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/status', () => ({
|
||||
default: ({
|
||||
status,
|
||||
@@ -122,19 +118,25 @@ describe('AgentLogModal', () => {
|
||||
})
|
||||
|
||||
it('should return null if no currentLogItem', () => {
|
||||
const { container } = render(<AgentLogModal {...mockProps} currentLogItem={undefined} />)
|
||||
const { container } = render(
|
||||
<AgentLogModal appId="app-id" {...mockProps} currentLogItem={undefined} />,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null if no conversationId', () => {
|
||||
const { container } = render(
|
||||
<AgentLogModal {...mockProps} currentLogItem={{ id: '1' } as unknown as IChatItem} />,
|
||||
<AgentLogModal
|
||||
appId="app-id"
|
||||
{...mockProps}
|
||||
currentLogItem={{ id: '1' } as unknown as IChatItem}
|
||||
/>,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render correctly when log item is provided', async () => {
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
render(<AgentLogModal appId="app-id" {...mockProps} />)
|
||||
|
||||
expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument()
|
||||
|
||||
@@ -146,7 +148,7 @@ 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 { container } = render(<AgentLogModal appId="app-id" {...mockProps} floating />)
|
||||
|
||||
const modal = screen.getByRole('dialog')
|
||||
expect(container).not.toContainElement(modal)
|
||||
@@ -157,7 +159,7 @@ describe('AgentLogModal', () => {
|
||||
it('should call onCancel when close button is clicked', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
render(<AgentLogModal appId="app-id" {...mockProps} />)
|
||||
|
||||
const closeBtn = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
fireEvent.click(closeBtn)
|
||||
@@ -173,7 +175,7 @@ describe('AgentLogModal', () => {
|
||||
clickAwayHandler = callback
|
||||
})
|
||||
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
render(<AgentLogModal appId="app-id" {...mockProps} />)
|
||||
clickAwayHandler(new Event('click'))
|
||||
|
||||
expect(mockProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
@@ -189,7 +191,7 @@ describe('AgentLogModal', () => {
|
||||
}
|
||||
})
|
||||
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
render(<AgentLogModal appId="app-id" {...mockProps} />)
|
||||
|
||||
expect(mockProps.onCancel).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -202,9 +204,83 @@ describe('AgentLogModal', () => {
|
||||
clickAwayHandler = callback
|
||||
})
|
||||
|
||||
render(<AgentLogModal {...mockProps} floating />)
|
||||
render(<AgentLogModal appId="app-id" {...mockProps} floating />)
|
||||
clickAwayHandler(new Event('click'))
|
||||
|
||||
expect(mockProps.onCancel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the new app log visible when the previous app response arrives late', async () => {
|
||||
const response = {
|
||||
meta: {
|
||||
status: 'succeeded',
|
||||
executor: 'New executor',
|
||||
start_time: '2023-01-01',
|
||||
elapsed_time: 1,
|
||||
total_tokens: 1,
|
||||
agent_mode: 'function_call',
|
||||
iterations: 0,
|
||||
},
|
||||
iterations: [],
|
||||
files: [],
|
||||
}
|
||||
let resolveOldResponse!: (value: Awaited<ReturnType<typeof fetchAgentLogDetail>>) => void
|
||||
const pending = new Promise<Awaited<ReturnType<typeof fetchAgentLogDetail>>>((resolve) => {
|
||||
resolveOldResponse = resolve
|
||||
})
|
||||
vi.mocked(fetchAgentLogDetail)
|
||||
.mockResolvedValue(response)
|
||||
.mockImplementationOnce(() => pending)
|
||||
const { rerender } = render(<AgentLogModal appId="old-app" {...mockProps} />)
|
||||
rerender(<AgentLogModal appId="new-app" {...mockProps} />)
|
||||
await screen.findByText('New executor')
|
||||
expect(fetchAgentLogDetail).toHaveBeenLastCalledWith({
|
||||
appID: 'new-app',
|
||||
params: { conversation_id: mockLog.conversationId, message_id: mockLog.id },
|
||||
})
|
||||
await act(async () => {
|
||||
resolveOldResponse({ ...response, meta: { ...response.meta, executor: 'Old executor' } })
|
||||
await pending
|
||||
})
|
||||
expect(screen.getByText('New executor')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Old executor')).not.toBeInTheDocument()
|
||||
})
|
||||
it('preserves the selected tab when another message replaces the open record', async () => {
|
||||
const { rerender } = render(<AgentLogModal appId="app-id" {...mockProps} />)
|
||||
await screen.findByText('User')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'runLog.tracing' }))
|
||||
rerender(
|
||||
<AgentLogModal
|
||||
appId="app-id"
|
||||
{...mockProps}
|
||||
currentLogItem={{ ...mockLog, id: 'next-message' }}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'runLog.tracing' })).toHaveAttribute(
|
||||
'data-active',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
||||
await waitFor(() => expect(screen.queryByRole('progressbar')).not.toBeInTheDocument())
|
||||
expect(fetchAgentLogDetail).toHaveBeenLastCalledWith({
|
||||
appID: 'app-id',
|
||||
params: { conversation_id: mockLog.conversationId, message_id: 'next-message' },
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores a failed request after its record has been replaced', async () => {
|
||||
let rejectOldResponse: (error: Error) => void = () => {}
|
||||
const pending = new Promise<Awaited<ReturnType<typeof fetchAgentLogDetail>>>((_, reject) => {
|
||||
rejectOldResponse = reject
|
||||
})
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValueOnce(pending)
|
||||
const { rerender } = render(<AgentLogModal appId="old-app" {...mockProps} />)
|
||||
rerender(<AgentLogModal appId="new-app" {...mockProps} />)
|
||||
await screen.findByText('User')
|
||||
await act(async () => {
|
||||
rejectOldResponse(new Error('Old request failed'))
|
||||
})
|
||||
expect(mockToast.error).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('User')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,12 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentIteration, AgentLogDetailResponse } from '@/models/log'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { uniq } from 'es-toolkit/array'
|
||||
import { flatten } from 'es-toolkit/compat'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
@@ -16,63 +14,21 @@ import ResultPanel from './result'
|
||||
import TracingPanel from './tracing'
|
||||
|
||||
type AgentLogDetailProps = Readonly<{
|
||||
appId: string
|
||||
activeTab?: 'DETAIL' | 'TRACING'
|
||||
conversationID: string
|
||||
log: IChatItem
|
||||
messageID: string
|
||||
}>
|
||||
const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
appId,
|
||||
activeTab = 'DETAIL',
|
||||
conversationID,
|
||||
messageID,
|
||||
log,
|
||||
}) => {
|
||||
const { t } = useTranslation(['runLog'])
|
||||
const [currentTab, setCurrentTab] = useState<string>(activeTab)
|
||||
const appDetail = useAppStore((s) => s.appDetail)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [runDetail, setRunDetail] = useState<AgentLogDetailResponse>()
|
||||
const [list, setList] = useState<AgentIteration[]>([])
|
||||
const tools = useMemo(() => {
|
||||
const res = uniq(
|
||||
flatten(
|
||||
runDetail?.iterations.map((iteration) => {
|
||||
return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean)
|
||||
}),
|
||||
).filter(Boolean),
|
||||
)
|
||||
return res
|
||||
}, [runDetail])
|
||||
const getLogDetail = useCallback(
|
||||
async (appID: string, conversationID: string, messageID: string) => {
|
||||
try {
|
||||
const res = await fetchAgentLogDetail({
|
||||
appID,
|
||||
params: {
|
||||
conversation_id: conversationID,
|
||||
message_id: messageID,
|
||||
},
|
||||
})
|
||||
setRunDetail(res)
|
||||
setList(res.iterations)
|
||||
} catch (err) {
|
||||
toast.error(`${err}`)
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
const getData = async (appID: string, conversationID: string, messageID: string) => {
|
||||
setLoading(true)
|
||||
await getLogDetail(appID, conversationID, messageID)
|
||||
setLoading(false)
|
||||
}
|
||||
const switchTab = async (tab: string) => {
|
||||
setCurrentTab(tab)
|
||||
}
|
||||
useEffect(() => {
|
||||
// fetch data
|
||||
if (appDetail) getData(appDetail.id, conversationID, messageID)
|
||||
}, [appDetail, conversationID, messageID])
|
||||
const [currentTab, setCurrentTab] = useState<'DETAIL' | 'TRACING'>(activeTab)
|
||||
return (
|
||||
<div className="relative flex grow flex-col">
|
||||
{/* tab */}
|
||||
@@ -84,7 +40,7 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
currentTab === 'DETAIL' && 'border-[rgb(21,94,239)]! text-text-secondary',
|
||||
)}
|
||||
data-active={currentTab === 'DETAIL'}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
onClick={() => setCurrentTab('DETAIL')}
|
||||
>
|
||||
{t(($) => $.detail, { ns: 'runLog' })}
|
||||
</button>
|
||||
@@ -95,41 +51,99 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
currentTab === 'TRACING' && 'border-[rgb(21,94,239)]! text-text-secondary',
|
||||
)}
|
||||
data-active={currentTab === 'TRACING'}
|
||||
onClick={() => switchTab('TRACING')}
|
||||
onClick={() => setCurrentTab('TRACING')}
|
||||
>
|
||||
{t(($) => $.tracing, { ns: 'runLog' })}
|
||||
</button>
|
||||
</div>
|
||||
{/* panel detail */}
|
||||
<div
|
||||
className={cn(
|
||||
'h-0 grow overflow-y-auto rounded-b-2xl bg-components-panel-bg',
|
||||
currentTab !== 'DETAIL' && 'bg-background-section!',
|
||||
)}
|
||||
>
|
||||
{loading && (
|
||||
<div className="flex h-full items-center justify-center bg-components-panel-bg">
|
||||
<LoadingPlaceholder />
|
||||
</div>
|
||||
)}
|
||||
{!loading && currentTab === 'DETAIL' && runDetail && (
|
||||
<ResultPanel
|
||||
inputs={log.input}
|
||||
outputs={log.content}
|
||||
status={runDetail.meta.status}
|
||||
error={runDetail.meta.error}
|
||||
elapsed_time={runDetail.meta.elapsed_time}
|
||||
total_tokens={runDetail.meta.total_tokens}
|
||||
created_at={runDetail.meta.start_time}
|
||||
created_by={runDetail.meta.executor}
|
||||
agentMode={runDetail.meta.agent_mode}
|
||||
tools={tools}
|
||||
iterations={runDetail.iterations.length}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'TRACING' && <TracingPanel list={list} />}
|
||||
</div>
|
||||
<AgentLogContent
|
||||
key={JSON.stringify([appId, conversationID, messageID])}
|
||||
appId={appId}
|
||||
conversationID={conversationID}
|
||||
messageID={messageID}
|
||||
log={log}
|
||||
currentTab={currentTab}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
type AgentLogContentProps = Omit<AgentLogDetailProps, 'activeTab'> & {
|
||||
currentTab: 'DETAIL' | 'TRACING'
|
||||
}
|
||||
|
||||
function AgentLogContent({
|
||||
appId,
|
||||
conversationID,
|
||||
messageID,
|
||||
log,
|
||||
currentTab,
|
||||
}: AgentLogContentProps) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [runDetail, setRunDetail] = useState<AgentLogDetailResponse>()
|
||||
const tools = useMemo(
|
||||
() =>
|
||||
uniq(
|
||||
(runDetail?.iterations ?? []).flatMap((iteration) =>
|
||||
iteration.tool_calls
|
||||
.map((tool) => tool.tool_name)
|
||||
.filter((name): name is string => !!name),
|
||||
),
|
||||
),
|
||||
[runDetail],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
fetchAgentLogDetail({
|
||||
appID: appId,
|
||||
params: { conversation_id: conversationID, message_id: messageID },
|
||||
}).then(
|
||||
(data) => {
|
||||
if (!active) return
|
||||
setRunDetail(data)
|
||||
setLoading(false)
|
||||
},
|
||||
(error) => {
|
||||
if (!active) return
|
||||
toast.error(`${error}`)
|
||||
setLoading(false)
|
||||
},
|
||||
)
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [appId, conversationID, messageID])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'h-0 grow overflow-y-auto rounded-b-2xl bg-components-panel-bg',
|
||||
currentTab !== 'DETAIL' && 'bg-background-section!',
|
||||
)}
|
||||
>
|
||||
{loading && (
|
||||
<div className="flex h-full items-center justify-center bg-components-panel-bg">
|
||||
<LoadingPlaceholder />
|
||||
</div>
|
||||
)}
|
||||
{!loading && currentTab === 'DETAIL' && runDetail && (
|
||||
<ResultPanel
|
||||
inputs={log.input}
|
||||
outputs={log.content}
|
||||
status={runDetail.meta.status}
|
||||
error={runDetail.meta.error}
|
||||
elapsed_time={runDetail.meta.elapsed_time}
|
||||
total_tokens={runDetail.meta.total_tokens}
|
||||
created_at={runDetail.meta.start_time}
|
||||
created_by={runDetail.meta.executor}
|
||||
agentMode={runDetail.meta.agent_mode}
|
||||
tools={tools}
|
||||
iterations={runDetail.iterations.length}
|
||||
/>
|
||||
)}
|
||||
{!loading && currentTab === 'TRACING' && <TracingPanel list={runDetail?.iterations ?? []} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogDetail
|
||||
@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppToastHost } from '@/app/notifications/host'
|
||||
import AgentLogModal from '.'
|
||||
|
||||
@@ -66,15 +65,8 @@ const MOCK_CHAT_ITEM: IChatItem = {
|
||||
|
||||
const AgentLogModalDemo = ({ width = 960 }: { width?: number }) => {
|
||||
const originalFetchRef = useRef<typeof globalThis.fetch>(null)
|
||||
const setAppDetail = useAppStore((state) => state.setAppDetail)
|
||||
|
||||
useEffect(() => {
|
||||
setAppDetail({
|
||||
id: 'app-1',
|
||||
name: 'Analytics Agent',
|
||||
mode: 'agent-chat',
|
||||
} as any)
|
||||
|
||||
originalFetchRef.current = globalThis.fetch?.bind(globalThis)
|
||||
|
||||
const handler = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
@@ -98,15 +90,15 @@ const AgentLogModalDemo = ({ width = 960 }: { width?: number }) => {
|
||||
|
||||
return () => {
|
||||
if (originalFetchRef.current) globalThis.fetch = originalFetchRef.current
|
||||
setAppDetail(undefined)
|
||||
}
|
||||
}, [setAppDetail])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppToastHost />
|
||||
<div className="relative min-h-135 w-full bg-background-default-subtle p-6">
|
||||
<AgentLogModal
|
||||
appId="app-1"
|
||||
currentLogItem={MOCK_CHAT_ITEM}
|
||||
width={width}
|
||||
onCancel={() => {
|
||||
|
||||
@@ -9,12 +9,19 @@ import { useTranslation } from 'react-i18next'
|
||||
import AgentLogDetail from './detail'
|
||||
|
||||
type AgentLogModalProps = Readonly<{
|
||||
appId: string
|
||||
currentLogItem?: IChatItem
|
||||
width: number
|
||||
floating?: boolean
|
||||
onCancel: () => void
|
||||
}>
|
||||
const AgentLogModal: FC<AgentLogModalProps> = ({ currentLogItem, width, floating, onCancel }) => {
|
||||
const AgentLogModal: FC<AgentLogModalProps> = ({
|
||||
appId,
|
||||
currentLogItem,
|
||||
width,
|
||||
floating,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation(['appLog', 'common'])
|
||||
const ref = useRef(null)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
@@ -32,6 +39,7 @@ const AgentLogModal: FC<AgentLogModalProps> = ({ currentLogItem, width, floating
|
||||
const detailContent = (
|
||||
<>
|
||||
<AgentLogDetail
|
||||
appId={appId}
|
||||
conversationID={currentLogItem.conversationId}
|
||||
messageID={currentLogItem.id}
|
||||
log={currentLogItem}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import MessageLogModal from '../index'
|
||||
|
||||
let clickAwayHandler: (() => void) | null = null
|
||||
@@ -10,10 +9,6 @@ vi.mock('ahooks', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run', () => ({
|
||||
default: ({
|
||||
activeTab,
|
||||
@@ -46,23 +41,20 @@ describe('MessageLogModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
clickAwayHandler = null
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
vi.mocked(useStore).mockImplementation((selector: any) =>
|
||||
selector({
|
||||
appDetail: { id: 'app-1' },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('Render', () => {
|
||||
it('renders nothing if currentLogItem is missing', () => {
|
||||
const { container } = render(<MessageLogModal width={800} onCancel={onCancel} />)
|
||||
const { container } = render(
|
||||
<MessageLogModal appId="app-1" width={800} onCancel={onCancel} />,
|
||||
)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('renders nothing if currentLogItem.workflow_run_id is missing', () => {
|
||||
const { container } = render(
|
||||
<MessageLogModal
|
||||
appId="app-1"
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
currentLogItem={{ id: '1' } as IChatItem}
|
||||
@@ -72,7 +64,9 @@ describe('MessageLogModal', () => {
|
||||
})
|
||||
|
||||
it('renders modal with correct title and Run component', () => {
|
||||
render(<MessageLogModal width={800} onCancel={onCancel} currentLogItem={mockLog} />)
|
||||
render(
|
||||
<MessageLogModal appId="app-1" width={800} onCancel={onCancel} currentLogItem={mockLog} />,
|
||||
)
|
||||
expect(screen.getByText(/title/i))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-run'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -82,6 +76,7 @@ describe('MessageLogModal', () => {
|
||||
it('passes correct props to Run component', () => {
|
||||
render(
|
||||
<MessageLogModal
|
||||
appId="inspected-app"
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
currentLogItem={mockLog}
|
||||
@@ -91,16 +86,17 @@ describe('MessageLogModal', () => {
|
||||
const runComponent = screen.getByTestId('workflow-run')
|
||||
expect(runComponent.getAttribute('data-active-tab')).toBe('TRACING')
|
||||
expect(runComponent.getAttribute('data-run-detail-url')).toBe(
|
||||
'/apps/app-1/workflow-runs/run-1',
|
||||
'/apps/inspected-app/workflow-runs/run-1',
|
||||
)
|
||||
expect(runComponent.getAttribute('data-tracing-list-url')).toBe(
|
||||
'/apps/app-1/workflow-runs/run-1/node-executions',
|
||||
'/apps/inspected-app/workflow-runs/run-1/node-executions',
|
||||
)
|
||||
})
|
||||
|
||||
it('sets fixed style when fixedWidth is false (floating)', () => {
|
||||
const { container } = render(
|
||||
<MessageLogModal
|
||||
appId="app-1"
|
||||
width={1000}
|
||||
onCancel={onCancel}
|
||||
currentLogItem={mockLog}
|
||||
@@ -115,6 +111,7 @@ describe('MessageLogModal', () => {
|
||||
it('sets fixed width when fixedWidth is true', () => {
|
||||
const { container } = render(
|
||||
<MessageLogModal
|
||||
appId="app-1"
|
||||
width={1000}
|
||||
onCancel={onCancel}
|
||||
currentLogItem={mockLog}
|
||||
@@ -129,7 +126,9 @@ describe('MessageLogModal', () => {
|
||||
|
||||
describe('Interaction', () => {
|
||||
it('calls onCancel when close icon is clicked', () => {
|
||||
render(<MessageLogModal width={800} onCancel={onCancel} currentLogItem={mockLog} />)
|
||||
render(
|
||||
<MessageLogModal appId="app-1" width={800} onCancel={onCancel} currentLogItem={mockLog} />,
|
||||
)
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
expect(closeButton)!.toBeInTheDocument()
|
||||
fireEvent.click(closeButton)
|
||||
@@ -138,7 +137,13 @@ describe('MessageLogModal', () => {
|
||||
|
||||
it('calls onCancel when clicked away', () => {
|
||||
render(
|
||||
<MessageLogModal width={800} onCancel={onCancel} currentLogItem={mockLog} fixedWidth />,
|
||||
<MessageLogModal
|
||||
appId="app-1"
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
currentLogItem={mockLog}
|
||||
fixedWidth
|
||||
/>,
|
||||
)
|
||||
expect(clickAwayHandler).toBeTruthy()
|
||||
clickAwayHandler!()
|
||||
@@ -146,7 +151,9 @@ describe('MessageLogModal', () => {
|
||||
})
|
||||
|
||||
it('does not use click away to close the floating dialog', () => {
|
||||
render(<MessageLogModal width={800} onCancel={onCancel} currentLogItem={mockLog} />)
|
||||
render(
|
||||
<MessageLogModal appId="app-1" width={800} onCancel={onCancel} currentLogItem={mockLog} />,
|
||||
)
|
||||
expect(clickAwayHandler).toBeTruthy()
|
||||
clickAwayHandler!()
|
||||
expect(onCancel).not.toHaveBeenCalled()
|
||||
|
||||
@@ -3,17 +3,10 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { WorkflowRunDetailResponse } from '@/models/log'
|
||||
import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow'
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import MessageLogModal from '.'
|
||||
|
||||
const SAMPLE_APP_DETAIL = {
|
||||
id: 'app-demo-1',
|
||||
name: 'Support Assistant',
|
||||
mode: 'chat',
|
||||
} as any
|
||||
|
||||
const mockRunDetail: WorkflowRunDetailResponse = {
|
||||
id: 'run-demo-1',
|
||||
version: 'v1.0.0',
|
||||
@@ -96,9 +89,6 @@ const mockCurrentLogItem: IChatItem = {
|
||||
|
||||
const useMessageLogMocks = () => {
|
||||
useEffect(() => {
|
||||
const store = useStore.getState()
|
||||
store.setAppDetail(SAMPLE_APP_DETAIL)
|
||||
|
||||
const originalFetch = globalThis.fetch?.bind(globalThis) ?? null
|
||||
|
||||
const handle = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
@@ -128,7 +118,6 @@ const useMessageLogMocks = () => {
|
||||
|
||||
return () => {
|
||||
globalThis.fetch = originalFetch || globalThis.fetch
|
||||
useStore.getState().setAppDetail(undefined)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
@@ -160,6 +149,7 @@ const meta = {
|
||||
},
|
||||
},
|
||||
args: {
|
||||
appId: 'app-demo-1',
|
||||
defaultTab: 'DETAIL',
|
||||
width: 960,
|
||||
fixedWidth: true,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import Run from '@/app/components/workflow/run'
|
||||
|
||||
type RunActiveTab = 'RESULT' | 'DETAIL' | 'TRACING'
|
||||
@@ -14,6 +13,7 @@ const isRunActiveTab = (tab: string): tab is RunActiveTab =>
|
||||
tab === 'RESULT' || tab === 'DETAIL' || tab === 'TRACING'
|
||||
|
||||
type MessageLogModalProps = {
|
||||
appId: string
|
||||
currentLogItem?: IChatItem
|
||||
defaultTab?: string
|
||||
width: number
|
||||
@@ -21,6 +21,7 @@ type MessageLogModalProps = {
|
||||
onCancel: () => void
|
||||
}
|
||||
const MessageLogModal: FC<MessageLogModalProps> = ({
|
||||
appId,
|
||||
currentLogItem,
|
||||
defaultTab = 'DETAIL',
|
||||
width,
|
||||
@@ -29,7 +30,6 @@ const MessageLogModal: FC<MessageLogModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation(['appLog', 'common'])
|
||||
const ref = useRef(null)
|
||||
const appDetail = useStore((state) => state.appDetail)
|
||||
|
||||
useClickAway(() => {
|
||||
if (fixedWidth) onCancel()
|
||||
@@ -54,8 +54,8 @@ const MessageLogModal: FC<MessageLogModalProps> = ({
|
||||
<Run
|
||||
hideResult
|
||||
activeTab={activeTab}
|
||||
runDetailUrl={`/apps/${appDetail?.id}/workflow-runs/${currentLogItem.workflow_run_id}`}
|
||||
tracingListUrl={`/apps/${appDetail?.id}/workflow-runs/${currentLogItem.workflow_run_id}/node-executions`}
|
||||
runDetailUrl={`/apps/${appId}/workflow-runs/${currentLogItem.workflow_run_id}`}
|
||||
tracingListUrl={`/apps/${appId}/workflow-runs/${currentLogItem.workflow_run_id}/node-executions`}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -104,8 +104,8 @@ const MessageLogModal: FC<MessageLogModalProps> = ({
|
||||
<Run
|
||||
hideResult
|
||||
activeTab={activeTab}
|
||||
runDetailUrl={`/apps/${appDetail?.id}/workflow-runs/${currentLogItem.workflow_run_id}`}
|
||||
tracingListUrl={`/apps/${appDetail?.id}/workflow-runs/${currentLogItem.workflow_run_id}/node-executions`}
|
||||
runDetailUrl={`/apps/${appId}/workflow-runs/${currentLogItem.workflow_run_id}`}
|
||||
tracingListUrl={`/apps/${appId}/workflow-runs/${currentLogItem.workflow_run_id}/node-executions`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -35,13 +35,14 @@ const GlobalVariablePanel = dynamic(
|
||||
},
|
||||
)
|
||||
|
||||
const WorkflowPanelOnLeft = () => {
|
||||
const WorkflowPanelOnLeft = ({ appId }: { appId: string }) => {
|
||||
const messageLogItem = useStore((state) => state.messageLogItem)
|
||||
const setMessageLogItem = useStore((state) => state.setMessageLogItem)
|
||||
return (
|
||||
<>
|
||||
{messageLogItem && (
|
||||
<MessageLogModal
|
||||
appId={appId}
|
||||
fixedWidth
|
||||
width={400}
|
||||
currentLogItem={messageLogItem}
|
||||
@@ -88,12 +89,12 @@ const WorkflowPanel = () => {
|
||||
const panelProps: PanelProps = useMemo(() => {
|
||||
return {
|
||||
components: {
|
||||
left: <WorkflowPanelOnLeft />,
|
||||
left: appDetail && <WorkflowPanelOnLeft appId={appDetail.id} />,
|
||||
right: <WorkflowPanelOnRight />,
|
||||
},
|
||||
versionHistoryPanelProps,
|
||||
}
|
||||
}, [versionHistoryPanelProps])
|
||||
}, [appDetail, versionHistoryPanelProps])
|
||||
|
||||
return <Panel {...panelProps} />
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user