dify
This commit is contained in:
132
dify/web/app/components/base/agent-log-modal/detail.tsx
Normal file
132
dify/web/app/components/base/agent-log-modal/detail.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { flatten, uniq } from 'lodash-es'
|
||||
import ResultPanel from './result'
|
||||
import TracingPanel from './tracing'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import type { AgentIteration, AgentLogDetailResponse } from '@/models/log'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
|
||||
export type AgentLogDetailProps = {
|
||||
activeTab?: 'DETAIL' | 'TRACING'
|
||||
conversationID: string
|
||||
log: IChatItem
|
||||
messageID: string
|
||||
}
|
||||
|
||||
const AgentLogDetail: FC<AgentLogDetailProps> = ({
|
||||
activeTab = 'DETAIL',
|
||||
conversationID,
|
||||
messageID,
|
||||
log,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
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) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: `${err}`,
|
||||
})
|
||||
}
|
||||
}, [notify])
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<div className='relative flex grow flex-col'>
|
||||
{/* tab */}
|
||||
<div className='flex shrink-0 items-center border-b-[0.5px] border-divider-regular px-4'>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
|
||||
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary',
|
||||
)}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>{t('runLog.detail')}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
|
||||
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary',
|
||||
)}
|
||||
onClick={() => switchTab('TRACING')}
|
||||
>{t('runLog.tracing')}</div>
|
||||
</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'>
|
||||
<Loading />
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogDetail
|
||||
146
dify/web/app/components/base/agent-log-modal/index.stories.tsx
Normal file
146
dify/web/app/components/base/agent-log-modal/index.stories.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import AgentLogModal from '.'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
|
||||
const MOCK_RESPONSE: AgentLogDetailResponse = {
|
||||
meta: {
|
||||
status: 'finished',
|
||||
executor: 'Agent Runner',
|
||||
start_time: '2024-03-12T10:00:00Z',
|
||||
elapsed_time: 12.45,
|
||||
total_tokens: 2589,
|
||||
agent_mode: 'ReACT',
|
||||
iterations: 2,
|
||||
error: undefined,
|
||||
},
|
||||
iterations: [
|
||||
{
|
||||
created_at: '2024-03-12T10:00:05Z',
|
||||
files: [],
|
||||
thought: JSON.stringify({ reasoning: 'Summarise conversation' }, null, 2),
|
||||
tokens: 934,
|
||||
tool_calls: [
|
||||
{
|
||||
status: 'success',
|
||||
tool_icon: null,
|
||||
tool_input: { query: 'Latest revenue numbers' },
|
||||
tool_output: { answer: 'Revenue up 12% QoQ' },
|
||||
tool_name: 'search',
|
||||
tool_label: {
|
||||
'en-US': 'Revenue Search',
|
||||
},
|
||||
time_cost: 1.8,
|
||||
},
|
||||
],
|
||||
tool_raw: {
|
||||
inputs: JSON.stringify({ context: 'Summaries' }, null, 2),
|
||||
outputs: JSON.stringify({ observation: 'Revenue up 12% QoQ' }, null, 2),
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: '2024-03-12T10:00:09Z',
|
||||
files: [],
|
||||
thought: JSON.stringify({ final: 'Revenue increased 12% quarter-over-quarter.' }, null, 2),
|
||||
tokens: 642,
|
||||
tool_calls: [],
|
||||
tool_raw: {
|
||||
inputs: JSON.stringify({ context: 'Compose summary' }, null, 2),
|
||||
outputs: JSON.stringify({ observation: 'Final answer ready' }, null, 2),
|
||||
},
|
||||
},
|
||||
],
|
||||
files: [],
|
||||
}
|
||||
|
||||
const MOCK_CHAT_ITEM: IChatItem = {
|
||||
id: 'message-1',
|
||||
content: JSON.stringify({ answer: 'Revenue grew 12% QoQ.' }, null, 2),
|
||||
input: JSON.stringify({ question: 'Summarise revenue trends.' }, null, 2),
|
||||
isAnswer: true,
|
||||
conversationId: 'conv-123',
|
||||
}
|
||||
|
||||
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) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
const url = request.url
|
||||
const parsed = new URL(url, window.location.origin)
|
||||
|
||||
if (parsed.pathname.endsWith('/apps/app-1/agent/logs')) {
|
||||
return new Response(JSON.stringify(MOCK_RESPONSE), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
if (originalFetchRef.current)
|
||||
return originalFetchRef.current(request)
|
||||
|
||||
throw new Error(`Unhandled request: ${url}`)
|
||||
}
|
||||
|
||||
globalThis.fetch = handler as typeof globalThis.fetch
|
||||
|
||||
return () => {
|
||||
if (originalFetchRef.current)
|
||||
globalThis.fetch = originalFetchRef.current
|
||||
setAppDetail(undefined)
|
||||
}
|
||||
}, [setAppDetail])
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<div className="relative min-h-[540px] w-full bg-background-default-subtle p-6">
|
||||
<AgentLogModal
|
||||
currentLogItem={MOCK_CHAT_ITEM}
|
||||
width={width}
|
||||
onCancel={() => {
|
||||
console.log('Agent log modal closed')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Other/AgentLogModal',
|
||||
component: AgentLogModalDemo,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Agent execution viewer showing iterations, tool calls, and metadata. Fetch responses are mocked for Storybook.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
width: 960,
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof AgentLogModalDemo>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Playground: Story = {}
|
||||
61
dify/web/app/components/base/agent-log-modal/index.tsx
Normal file
61
dify/web/app/components/base/agent-log-modal/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import AgentLogDetail from './detail'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
|
||||
type AgentLogModalProps = {
|
||||
currentLogItem?: IChatItem
|
||||
width: number
|
||||
onCancel: () => void
|
||||
}
|
||||
const AgentLogModal: FC<AgentLogModalProps> = ({
|
||||
currentLogItem,
|
||||
width,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const ref = useRef(null)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useClickAway(() => {
|
||||
if (mounted)
|
||||
onCancel()
|
||||
}, ref)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!currentLogItem || !currentLogItem.conversationId)
|
||||
return null
|
||||
|
||||
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')}
|
||||
style={{
|
||||
width: 480,
|
||||
position: 'fixed',
|
||||
top: 56 + 8,
|
||||
left: 8 + (width - 480),
|
||||
bottom: 16,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<h1 className='text-md shrink-0 px-4 py-1 font-semibold text-text-primary'>{t('appLog.runDetail.workflowTitle')}</h1>
|
||||
<span className='absolute right-3 top-4 z-20 cursor-pointer p-1' onClick={onCancel}>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</span>
|
||||
<AgentLogDetail
|
||||
conversationID={currentLogItem.conversationId}
|
||||
messageID={currentLogItem.id}
|
||||
log={currentLogItem}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentLogModal
|
||||
51
dify/web/app/components/base/agent-log-modal/iteration.tsx
Normal file
51
dify/web/app/components/base/agent-log-modal/iteration.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FC } from 'react'
|
||||
import ToolCall from './tool-call'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import type { AgentIteration } from '@/models/log'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
isFinal: boolean
|
||||
index: number
|
||||
iterationInfo: AgentIteration
|
||||
}
|
||||
|
||||
const Iteration: FC<Props> = ({ iterationInfo, isFinal, index }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn('px-4 py-2')}>
|
||||
<div className='flex items-center'>
|
||||
{isFinal && (
|
||||
<div className='mr-3 shrink-0 text-xs font-semibold leading-[18px] text-text-tertiary'>{t('appLog.agentLogDetail.finalProcessing')}</div>
|
||||
)}
|
||||
{!isFinal && (
|
||||
<div className='mr-3 shrink-0 text-xs font-semibold leading-[18px] text-text-tertiary'>{`${t('appLog.agentLogDetail.iteration').toUpperCase()} ${index}`}</div>
|
||||
)}
|
||||
<Divider bgStyle='gradient' className='mx-0 h-px grow'/>
|
||||
</div>
|
||||
<ToolCall
|
||||
isLLM
|
||||
isFinal={isFinal}
|
||||
tokens={iterationInfo.tokens}
|
||||
observation={iterationInfo.tool_raw.outputs}
|
||||
finalAnswer={iterationInfo.thought}
|
||||
toolCall={{
|
||||
status: 'success',
|
||||
tool_icon: null,
|
||||
}}
|
||||
/>
|
||||
{iterationInfo.tool_calls.map((toolCall, index) => (
|
||||
<ToolCall
|
||||
isLLM={false}
|
||||
key={index}
|
||||
toolCall={toolCall}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Iteration
|
||||
126
dify/web/app/components/base/agent-log-modal/result.tsx
Normal file
126
dify/web/app/components/base/agent-log-modal/result.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StatusPanel from '@/app/components/workflow/run/status'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
|
||||
type ResultPanelProps = {
|
||||
status: string
|
||||
elapsed_time?: number
|
||||
total_tokens?: number
|
||||
error?: string
|
||||
inputs?: any
|
||||
outputs?: any
|
||||
created_by?: string
|
||||
created_at: string
|
||||
agentMode?: string
|
||||
tools?: string[]
|
||||
iterations?: number
|
||||
}
|
||||
|
||||
const ResultPanel: FC<ResultPanelProps> = ({
|
||||
elapsed_time,
|
||||
total_tokens,
|
||||
error,
|
||||
inputs,
|
||||
outputs,
|
||||
created_by,
|
||||
created_at,
|
||||
agentMode,
|
||||
tools,
|
||||
iterations,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTime } = useTimestamp()
|
||||
|
||||
return (
|
||||
<div className='bg-components-panel-bg py-2'>
|
||||
<div className='px-4 py-2'>
|
||||
<StatusPanel
|
||||
status='succeeded'
|
||||
time={elapsed_time}
|
||||
tokens={total_tokens}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2 px-4 py-2'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>INPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={inputs}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>OUTPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={outputs}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<div className='h-[0.5px] bg-divider-regular opacity-5' />
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<div className='relative'>
|
||||
<div className='h-6 text-xs font-medium leading-6 text-text-tertiary'>{t('runLog.meta.title')}</div>
|
||||
<div className='py-1'>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('runLog.meta.status')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>SUCCESS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('runLog.meta.executor')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>{created_by || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('runLog.meta.startTime')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>{formatTime(Date.parse(created_at) / 1000, t('appLog.dateTimeFormat') as string)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('runLog.meta.time')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>{`${elapsed_time?.toFixed(3)}s`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('runLog.meta.tokens')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>{`${total_tokens || 0} Tokens`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('appLog.agentLogDetail.agentMode')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>{agentMode === 'function_call' ? t('appDebug.agent.agentModeType.functionCall') : t('appDebug.agent.agentModeType.ReACT')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('appLog.agentLogDetail.toolUsed')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>{tools?.length ? tools?.join(', ') : 'Null'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='w-[104px] shrink-0 truncate px-2 py-[5px] text-xs leading-[18px] text-text-tertiary'>{t('appLog.agentLogDetail.iterations')}</div>
|
||||
<div className='grow px-2 py-[5px] text-xs leading-[18px] text-text-primary'>
|
||||
<span>{iterations}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResultPanel
|
||||
142
dify/web/app/components/base/agent-log-modal/tool-call.tsx
Normal file
142
dify/web/app/components/base/agent-log-modal/tool-call.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
RiCheckboxCircleLine,
|
||||
RiErrorWarningLine,
|
||||
} from '@remixicon/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from '@/utils/classnames'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import type { ToolCall } from '@/models/log'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
type Props = {
|
||||
toolCall: ToolCall
|
||||
isLLM: boolean
|
||||
isFinal?: boolean
|
||||
tokens?: number
|
||||
observation?: any
|
||||
finalAnswer?: any
|
||||
}
|
||||
|
||||
const ToolCallItem: FC<Props> = ({ toolCall, isLLM = false, isFinal, tokens, observation, finalAnswer }) => {
|
||||
const [collapseState, setCollapseState] = useState<boolean>(true)
|
||||
const { locale } = useContext(I18n)
|
||||
const toolName = isLLM ? 'LLM' : (toolCall.tool_label[locale] || toolCall.tool_label[locale.replaceAll('-', '_')])
|
||||
|
||||
const getTime = (time: number) => {
|
||||
if (time < 1)
|
||||
return `${(time * 1000).toFixed(3)} ms`
|
||||
if (time > 60)
|
||||
return `${Math.floor(time / 60)} m ${(time % 60).toFixed(3)} s`
|
||||
return `${time.toFixed(3)} s`
|
||||
}
|
||||
|
||||
const getTokenCount = (tokens: number) => {
|
||||
if (tokens < 1000)
|
||||
return tokens
|
||||
if (tokens >= 1000 && tokens < 1000000)
|
||||
return `${Number.parseFloat((tokens / 1000).toFixed(3))}K`
|
||||
if (tokens >= 1000000)
|
||||
return `${Number.parseFloat((tokens / 1000000).toFixed(3))}M`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('py-1')}>
|
||||
<div className={cn('group rounded-2xl border border-components-panel-border bg-background-default shadow-xs transition-all hover:shadow-md')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center py-3 pl-[6px] pr-3',
|
||||
!collapseState && '!pb-2',
|
||||
)}
|
||||
onClick={() => setCollapseState(!collapseState)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'mr-1 h-3 w-3 shrink-0 text-text-quaternary transition-all group-hover:text-text-tertiary',
|
||||
!collapseState && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
<BlockIcon className={cn('mr-2 shrink-0')} type={isLLM ? BlockEnum.LLM : BlockEnum.Tool} toolIcon={toolCall.tool_icon} />
|
||||
<div className={cn(
|
||||
'grow truncate text-[13px] font-semibold leading-[16px] text-text-secondary',
|
||||
)} title={toolName}>{toolName}</div>
|
||||
<div className='shrink-0 text-xs leading-[18px] text-text-tertiary'>
|
||||
{toolCall.time_cost && (
|
||||
<span>{getTime(toolCall.time_cost || 0)}</span>
|
||||
)}
|
||||
{isLLM && (
|
||||
<span>{`${getTokenCount(tokens || 0)} tokens`}</span>
|
||||
)}
|
||||
</div>
|
||||
{toolCall.status === 'success' && (
|
||||
<RiCheckboxCircleLine className='ml-2 h-3.5 w-3.5 shrink-0 text-[#12B76A]' />
|
||||
)}
|
||||
{toolCall.status === 'error' && (
|
||||
<RiErrorWarningLine className='ml-2 h-3.5 w-3.5 shrink-0 text-[#F04438]' />
|
||||
)}
|
||||
</div>
|
||||
{!collapseState && (
|
||||
<div className='pb-2'>
|
||||
<div className={cn('px-[10px] py-1')}>
|
||||
{toolCall.status === 'error' && (
|
||||
<div className='rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] bg-[#fef3f2] px-3 py-[10px] text-xs leading-[18px] text-[#d92d20] shadow-xs'>{toolCall.error}</div>
|
||||
)}
|
||||
</div>
|
||||
{toolCall.tool_input && (
|
||||
<div className={cn('px-[10px] py-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>INPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={toolCall.tool_input}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{toolCall.tool_output && (
|
||||
<div className={cn('px-[10px] py-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>OUTPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={toolCall.tool_output}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isLLM && (
|
||||
<div className={cn('px-[10px] py-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>OBSERVATION</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={observation}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isLLM && (
|
||||
<div className={cn('px-[10px] py-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{isFinal ? 'FINAL ANSWER' : 'THOUGHT'}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={finalAnswer}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolCallItem
|
||||
25
dify/web/app/components/base/agent-log-modal/tracing.tsx
Normal file
25
dify/web/app/components/base/agent-log-modal/tracing.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Iteration from './iteration'
|
||||
import type { AgentIteration } from '@/models/log'
|
||||
|
||||
type TracingPanelProps = {
|
||||
list: AgentIteration[]
|
||||
}
|
||||
|
||||
const TracingPanel: FC<TracingPanelProps> = ({ list }) => {
|
||||
return (
|
||||
<div className='bg-background-section'>
|
||||
{list.map((iteration, index) => (
|
||||
<Iteration
|
||||
key={index}
|
||||
index={index + 1}
|
||||
isFinal={index + 1 === list.length}
|
||||
iterationInfo={iteration}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TracingPanel
|
||||
Reference in New Issue
Block a user