This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View 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

View 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 = {}

View 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

View 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

View 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

View 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

View 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