mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
fix(web): align agent activity disclosure (#38999)
This commit is contained in:
parent
71d5c35e8f
commit
b721e7a32d
@ -1870,6 +1870,9 @@ describe('useChatWithHistory', () => {
|
||||
id: 'msg-files',
|
||||
query: 'Question with files',
|
||||
answer: 'Answer with files',
|
||||
answer_tokens: 10,
|
||||
message_tokens: 5,
|
||||
provider_response_latency: 66,
|
||||
message_files: [
|
||||
{
|
||||
id: 'file-user-1',
|
||||
@ -1967,6 +1970,12 @@ describe('useChatWithHistory', () => {
|
||||
expect(messageWithFiles?.children?.[0]?.agent_thoughts?.[0]?.message_files).toHaveLength(1)
|
||||
|
||||
const normalAnswerNode = messageWithFiles?.children?.[0]
|
||||
expect(normalAnswerNode?.more).toEqual({
|
||||
time: '',
|
||||
tokens: 15,
|
||||
latency: '66.00',
|
||||
tokens_per_second: '0.15',
|
||||
})
|
||||
const pausedAnswerNode = result!.current.appPrevChatTree.find(
|
||||
(item) => item.id === 'question-msg-paused-branch',
|
||||
)?.children?.[0]
|
||||
|
||||
@ -61,6 +61,18 @@ function getFormattedChatList(messages: any[]) {
|
||||
})
|
||||
const answerFiles =
|
||||
item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
const answerTokens = item.answer_tokens ?? 0
|
||||
const messageTokens = item.message_tokens ?? 0
|
||||
const latency = Number(item.provider_response_latency)
|
||||
const more =
|
||||
item.provider_response_latency == null || !Number.isFinite(latency)
|
||||
? undefined
|
||||
: {
|
||||
time: '',
|
||||
tokens: answerTokens + messageTokens,
|
||||
latency: latency.toFixed(2),
|
||||
tokens_per_second: latency > 0 ? (answerTokens / latency).toFixed(2) : undefined,
|
||||
}
|
||||
const humanInputFormDataList: HumanInputFormData[] = []
|
||||
const humanInputFilledFormDataList: HumanInputFilledFormData[] = []
|
||||
let workflowRunId = ''
|
||||
@ -111,6 +123,7 @@ function getFormattedChatList(messages: any[]) {
|
||||
humanInputFormDataList,
|
||||
humanInputFilledFormDataList,
|
||||
workflow_run_id: workflowRunId,
|
||||
more,
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ChatItem } from '../../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vitest'
|
||||
import { AgentRosterResponseContent } from '../agent-roster-response-content'
|
||||
|
||||
@ -9,13 +10,59 @@ vi.mock('react-i18next', async () => {
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock({
|
||||
'common.chat.thought': 'Thought',
|
||||
'agentV2.agentDetail.configure.answer.thinking': 'Thinking',
|
||||
'agentV2.agentDetail.configure.answer.duration.minute': '{{count}}m',
|
||||
'agentV2.agentDetail.configure.answer.duration.second': '{{count}}s',
|
||||
'agentV2.agentDetail.configure.answer.activity.ranCommands': 'Ran commands',
|
||||
'agentV2.agentDetail.configure.answer.activity.runningCommands': 'Running commands',
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const thinkingOnlyItem = {
|
||||
id: 'answer-thinking-only',
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
agent_response_parts: [
|
||||
{
|
||||
type: 'thought',
|
||||
thought: {
|
||||
id: 'thought-thinking-only',
|
||||
thought: 'internal thought should not render',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
observation: '',
|
||||
message_id: 'answer-thinking-only',
|
||||
conversation_id: 'conversation-thinking-only',
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
describe('AgentRosterResponseContent', () => {
|
||||
it('should keep the live thinking status before visible activity arrives', () => {
|
||||
render(<AgentRosterResponseContent item={thinkingOnlyItem} responding />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Thinking/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
)
|
||||
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep a stopped thinking status without exposing internal thought', () => {
|
||||
render(<AgentRosterResponseContent item={thinkingOnlyItem} responding={false} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Thinking' })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false',
|
||||
)
|
||||
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render historical agent thought answer as markdown instead of thought process', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-history',
|
||||
content: '',
|
||||
@ -37,55 +84,73 @@ describe('AgentRosterResponseContent', () => {
|
||||
|
||||
render(<AgentRosterResponseContent item={item} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /workFinished/i })).toBeInTheDocument()
|
||||
const processToggle = screen.getByRole('button', { name: 'Thinking' })
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText('history answer')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workFinished/i }))
|
||||
await user.click(processToggle)
|
||||
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'true')
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('agent-roster-response-content')).toHaveTextContent(
|
||||
'history answer',
|
||||
)
|
||||
expect(screen.getByText('history answer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render new agent response parts in event order when thoughts and messages interleave', async () => {
|
||||
it('should preserve historical answer whitespace when rendering markdown', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-history-code',
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-history-code',
|
||||
thought: 'internal thought should not render',
|
||||
answer: ' const answer = 42',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
observation: '',
|
||||
message_id: 'answer-history-code',
|
||||
conversation_id: 'conversation-history-code',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} />)
|
||||
await user.click(screen.getByRole('button', { name: 'Thinking' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('const answer = 42').tagName).toBe('CODE')
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep one collapsible thinking timeline while response parts interleave', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-1',
|
||||
content: 'first answer second answer',
|
||||
isAnswer: true,
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-1',
|
||||
thought: 'first thought',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
observation: '',
|
||||
message_id: 'answer-1',
|
||||
conversation_id: 'conversation-1',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'thought-2',
|
||||
thought: 'second thought',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
observation: '',
|
||||
message_id: 'answer-1',
|
||||
conversation_id: 'conversation-1',
|
||||
position: 2,
|
||||
},
|
||||
],
|
||||
agent_response_parts: [
|
||||
{
|
||||
type: 'thought',
|
||||
thought: {
|
||||
id: 'thought-1',
|
||||
thought: 'first thought',
|
||||
tool: '',
|
||||
thought: 'raw first thought',
|
||||
tool: 'load_tools',
|
||||
tool_input: '',
|
||||
tool_labels: {
|
||||
load_tools: {
|
||||
en_US: 'Loaded tools',
|
||||
zh_Hans: '已加载工具',
|
||||
},
|
||||
shell_run: {
|
||||
en_US: 'Ran commands',
|
||||
zh_Hans: '运行了命令',
|
||||
},
|
||||
},
|
||||
observation: '',
|
||||
message_id: 'answer-1',
|
||||
conversation_id: 'conversation-1',
|
||||
@ -100,9 +165,19 @@ describe('AgentRosterResponseContent', () => {
|
||||
type: 'thought',
|
||||
thought: {
|
||||
id: 'thought-2',
|
||||
thought: 'second thought',
|
||||
tool: '',
|
||||
thought: 'raw second thought',
|
||||
tool: 'shell_run',
|
||||
tool_input: '',
|
||||
tool_labels: {
|
||||
load_tools: {
|
||||
en_US: 'Loaded tools',
|
||||
zh_Hans: '已加载工具',
|
||||
},
|
||||
shell_run: {
|
||||
en_US: 'Ran commands',
|
||||
zh_Hans: '运行了命令',
|
||||
},
|
||||
},
|
||||
observation: '',
|
||||
message_id: 'answer-1',
|
||||
conversation_id: 'conversation-1',
|
||||
@ -118,17 +193,28 @@ describe('AgentRosterResponseContent', () => {
|
||||
|
||||
render(<AgentRosterResponseContent item={item} responding />)
|
||||
|
||||
const processToggle = screen.getByRole('button', { name: /Thinking/ })
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'true')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('agent-roster-response-content')).toHaveTextContent('second answer')
|
||||
})
|
||||
|
||||
const content = screen.getByTestId('agent-roster-response-content').textContent ?? ''
|
||||
expect(content.indexOf('first thought')).toBeLessThan(content.indexOf('first answer'))
|
||||
expect(content.indexOf('first answer')).toBeLessThan(content.indexOf('second thought'))
|
||||
expect(content.indexOf('second thought')).toBeLessThan(content.indexOf('second answer'))
|
||||
expect(content.indexOf('Loaded tools')).toBeLessThan(content.indexOf('first answer'))
|
||||
expect(content.indexOf('first answer')).toBeLessThan(content.indexOf('Ran commands'))
|
||||
expect(content.indexOf('Ran commands')).toBeLessThan(content.indexOf('second answer'))
|
||||
expect(screen.queryByText('raw first thought')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('raw second thought')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(processToggle)
|
||||
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText('Loaded tools')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show THOUGHT as the header when a thought process is expanded', () => {
|
||||
it('should keep activity labels stable when their details are expanded', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-expanded-thought',
|
||||
content: '',
|
||||
@ -136,10 +222,16 @@ describe('AgentRosterResponseContent', () => {
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-expanded',
|
||||
thought: 'visible thought summary',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
observation: '',
|
||||
thought: '',
|
||||
tool: 'shell_run',
|
||||
tool_input: '{"command":"ls"}',
|
||||
tool_labels: {
|
||||
shell_run: {
|
||||
en_US: 'Ran commands',
|
||||
zh_Hans: '运行了命令',
|
||||
},
|
||||
},
|
||||
observation: 'README.md',
|
||||
message_id: 'answer-expanded-thought',
|
||||
conversation_id: 'conversation-expanded-thought',
|
||||
position: 1,
|
||||
@ -147,8 +239,326 @@ describe('AgentRosterResponseContent', () => {
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Thinking' }))
|
||||
|
||||
const activityToggle = screen.getByRole('button', { name: 'Ran commands' })
|
||||
expect(activityToggle).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
await user.click(activityToggle)
|
||||
|
||||
expect(activityToggle).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByRole('button', { name: 'Ran commands' })).toBe(activityToggle)
|
||||
expect(screen.getByText('{"command":"ls"}')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use the shell activity fallback only when no descriptive label is available', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-shell-labels',
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-shell-fallback',
|
||||
thought: '',
|
||||
tool: 'shell_run',
|
||||
tool_input: 'pwd',
|
||||
observation: '/workspace',
|
||||
message_id: 'answer-shell-labels',
|
||||
conversation_id: 'conversation-shell-labels',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'thought-shell-described',
|
||||
thought: '',
|
||||
tool: 'shell_run',
|
||||
tool_input: 'mkdir skill',
|
||||
tool_labels: {
|
||||
shell_run: {
|
||||
en_US: 'Scaffold skill directory',
|
||||
zh_Hans: '创建技能目录',
|
||||
},
|
||||
},
|
||||
observation: '',
|
||||
message_id: 'answer-shell-labels',
|
||||
conversation_id: 'conversation-shell-labels',
|
||||
position: 2,
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Thinking' }))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Ran commands' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Scaffold skill directory' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should describe an unfinished shell run as running commands', () => {
|
||||
const item = {
|
||||
id: 'answer-running-shell',
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
agent_response_parts: [
|
||||
{
|
||||
type: 'thought',
|
||||
thought: {
|
||||
id: 'thought-running-shell',
|
||||
thought: '',
|
||||
tool: 'shell_run',
|
||||
tool_input: 'pnpm test',
|
||||
observation: '',
|
||||
message_id: 'answer-running-shell',
|
||||
conversation_id: 'conversation-running-shell',
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} responding />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'THOUGHT' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Running commands' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep the live activity disclosure open when public messages arrive and completion starts', () => {
|
||||
const thought = {
|
||||
id: 'thought-transition',
|
||||
thought: 'raw thinking',
|
||||
tool: 'shell_run',
|
||||
tool_input: '{"command":"ls"}',
|
||||
tool_labels: {
|
||||
shell_run: {
|
||||
en_US: 'Ran commands',
|
||||
zh_Hans: '运行了命令',
|
||||
},
|
||||
},
|
||||
observation: 'README.md',
|
||||
message_id: 'answer-transition',
|
||||
conversation_id: 'conversation-transition',
|
||||
position: 1,
|
||||
}
|
||||
const item = {
|
||||
id: 'answer-transition',
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
agent_response_parts: [
|
||||
{
|
||||
type: 'thought',
|
||||
thought,
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
const { rerender } = render(<AgentRosterResponseContent item={item} responding />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Thinking/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
)
|
||||
|
||||
const itemWithMessage = {
|
||||
...item,
|
||||
agent_response_parts: [
|
||||
...item.agent_response_parts,
|
||||
{
|
||||
type: 'message',
|
||||
content: 'public answer',
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
rerender(<AgentRosterResponseContent item={itemWithMessage} responding />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Thinking/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByText('public answer')).toBeInTheDocument()
|
||||
|
||||
rerender(<AgentRosterResponseContent item={itemWithMessage} responding={false} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Thinking/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'true',
|
||||
)
|
||||
|
||||
rerender(
|
||||
<AgentRosterResponseContent
|
||||
item={{
|
||||
...item,
|
||||
content: 'final answer',
|
||||
agent_response_parts: undefined,
|
||||
agent_thoughts: [thought],
|
||||
}}
|
||||
content="final answer"
|
||||
responding={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Thinking' })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false',
|
||||
)
|
||||
expect(screen.getByText('final answer')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Ran commands' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should preserve a manual live activity collapse as new events arrive', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-manual-collapse',
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
agent_response_parts: [
|
||||
{
|
||||
type: 'thought',
|
||||
thought: {
|
||||
id: 'thought-manual-collapse',
|
||||
thought: '',
|
||||
tool: 'shell_run',
|
||||
tool_input: 'pwd',
|
||||
observation: '/workspace',
|
||||
message_id: 'answer-manual-collapse',
|
||||
conversation_id: 'conversation-manual-collapse',
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
const { rerender } = render(<AgentRosterResponseContent item={item} responding />)
|
||||
const processToggle = screen.getByRole('button', { name: /Thinking/ })
|
||||
|
||||
await user.click(processToggle)
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
rerender(
|
||||
<AgentRosterResponseContent
|
||||
item={{
|
||||
...item,
|
||||
agent_response_parts: [
|
||||
...item.agent_response_parts,
|
||||
{ type: 'message', content: 'new progress update' },
|
||||
],
|
||||
}}
|
||||
responding
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /Thinking/ })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
'false',
|
||||
)
|
||||
expect(screen.queryByText('new progress update')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep a historical answer and its activity in the same process entry', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-history-with-activity',
|
||||
content: 'final answer',
|
||||
isAnswer: true,
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-history-with-activity',
|
||||
thought: 'internal thought should not render',
|
||||
answer: 'public progress update',
|
||||
tool: 'shell_run',
|
||||
tool_input: 'pwd',
|
||||
observation: '/workspace',
|
||||
message_id: 'answer-history-with-activity',
|
||||
conversation_id: 'conversation-history-with-activity',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} content={item.content} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Thinking' }))
|
||||
|
||||
expect(screen.getByText('public progress update')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Ran commands' })).toBeInTheDocument()
|
||||
expect(screen.getByText('final answer')).toBeInTheDocument()
|
||||
expect(screen.queryByText('internal thought should not render')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a live message directly when there is no activity', () => {
|
||||
const item = {
|
||||
id: 'answer-live-message-only',
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
agent_response_parts: [{ type: 'message', content: 'direct answer' }],
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} responding />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /Thinking/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('direct answer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should omit the activity disclosure for a completed response without activity', () => {
|
||||
const item = {
|
||||
id: 'answer-without-activity',
|
||||
content: 'final answer',
|
||||
isAnswer: true,
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} content={item.content} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /Thinking/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('final answer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep the completed thinking label and render the final answer outside it', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = {
|
||||
id: 'answer-complete',
|
||||
content: 'final answer',
|
||||
isAnswer: true,
|
||||
more: {
|
||||
time: '',
|
||||
tokens: 0,
|
||||
latency: 66,
|
||||
},
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-complete',
|
||||
thought: 'raw process detail',
|
||||
tool: 'shell_run',
|
||||
tool_input: '{"command":"ls"}',
|
||||
tool_labels: {
|
||||
shell_run: {
|
||||
en_US: 'Ran commands',
|
||||
zh_Hans: '运行了命令',
|
||||
},
|
||||
},
|
||||
observation: 'README.md',
|
||||
message_id: 'answer-complete',
|
||||
conversation_id: 'conversation-complete',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
} satisfies ChatItem
|
||||
|
||||
render(<AgentRosterResponseContent item={item} content={item.content} />)
|
||||
|
||||
const processToggle = screen.getByRole('button', {
|
||||
name: 'Thinking · {{count}}m{{count}}s',
|
||||
})
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText('raw process detail')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Ran commands' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('final answer')).toBeInTheDocument()
|
||||
|
||||
await user.click(processToggle)
|
||||
|
||||
expect(processToggle).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByRole('button', { name: 'Ran commands' })).toBeInTheDocument()
|
||||
expect(screen.getByText('final answer')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -41,6 +41,13 @@ describe('More', () => {
|
||||
expect(screen.queryByTestId('more-tps')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should omit the timestamp separator when time is empty', () => {
|
||||
render(<More more={{ ...mockMoreData, time: '' }} />)
|
||||
|
||||
expect(screen.queryByTestId('more-time')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('·')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render nothing inside container if more prop is missing', () => {
|
||||
render(<More more={undefined} />)
|
||||
const containerDiv = screen.getByTestId('more-container')
|
||||
|
||||
@ -26,7 +26,7 @@ const { mockSetShowAnnotationFullModal, mockProviderContext, mockT, mockAddAnnot
|
||||
vi.mock('copy-to-clipboard', () => ({ default: vi.fn() }))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
toast: { success: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
@ -125,7 +125,11 @@ vi.mock(
|
||||
)
|
||||
|
||||
vi.mock('@/app/components/base/new-audio-button', () => ({
|
||||
default: () => <button data-testid="audio-btn">Play</button>,
|
||||
default: ({ value }: { value: string }) => (
|
||||
<button data-testid="audio-btn" data-value={value}>
|
||||
Play
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat/log', () => ({
|
||||
@ -209,6 +213,29 @@ const baseItem: ChatItem = {
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
const createInterruptedItem = (messages: string[]): ChatItem => {
|
||||
const thought = {
|
||||
id: '1',
|
||||
thought: 'internal thought should not be used',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
observation: '',
|
||||
message_id: '',
|
||||
conversation_id: '',
|
||||
position: 0,
|
||||
}
|
||||
|
||||
return {
|
||||
...baseItem,
|
||||
content: '',
|
||||
agent_response_parts: [
|
||||
{ type: 'thought', thought },
|
||||
...messages.map((content) => ({ type: 'message' as const, content })),
|
||||
],
|
||||
agent_thoughts: [thought],
|
||||
}
|
||||
}
|
||||
|
||||
const baseProps: OperationProps = {
|
||||
item: baseItem,
|
||||
question: 'What is this?',
|
||||
@ -279,6 +306,26 @@ describe('Operation', () => {
|
||||
expect(screen.getByTestId('annotation-ctrl'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide content-dependent actions for an interrupted response without public content', () => {
|
||||
mockContextValue.config = makeChatConfig({
|
||||
text_to_speech: { enabled: true },
|
||||
supportAnnotation: true,
|
||||
annotation_reply: {
|
||||
id: 'ar-1',
|
||||
score_threshold: 0.5,
|
||||
embedding_model: { embedding_provider_name: '', embedding_model_name: '' },
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
renderOperation({ ...baseProps, item: createInterruptedItem([]) })
|
||||
|
||||
expect(screen.queryByTestId('audio-btn')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'operation.copy' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('annotation-ctrl')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'operation.regenerate' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide annotation button when chat is readonly', () => {
|
||||
mockContextValue.readonly = true
|
||||
mockContextValue.config = makeChatConfig({
|
||||
@ -344,11 +391,11 @@ describe('Operation', () => {
|
||||
expect(copy).toHaveBeenCalledWith('Hello world')
|
||||
})
|
||||
|
||||
it('should aggregate agent_thoughts for copy content', async () => {
|
||||
it('should copy the visible answer instead of agent thought summaries', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item: ChatItem = {
|
||||
...baseItem,
|
||||
content: 'ignored',
|
||||
content: 'Final answer',
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: '1',
|
||||
@ -374,7 +421,39 @@ describe('Operation', () => {
|
||||
}
|
||||
renderOperation({ ...baseProps, item })
|
||||
await user.click(screen.getByRole('button', { name: 'operation.copy' }))
|
||||
expect(copy).toHaveBeenCalledWith('Hello World')
|
||||
expect(copy).toHaveBeenCalledWith('Final answer')
|
||||
})
|
||||
|
||||
it('should copy public response parts after an interrupted response', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = createInterruptedItem(['First public update', 'Second public update'])
|
||||
renderOperation({ ...baseProps, item })
|
||||
await user.click(screen.getByRole('button', { name: 'operation.copy' }))
|
||||
expect(copy).toHaveBeenCalledWith('First public update\n\nSecond public update')
|
||||
})
|
||||
|
||||
it('should copy public thought answers for legacy messages without content', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item: ChatItem = {
|
||||
...baseItem,
|
||||
content: '',
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: '1',
|
||||
thought: 'internal thought should not be copied',
|
||||
answer: 'Public legacy answer',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
observation: '',
|
||||
message_id: '',
|
||||
conversation_id: '',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
renderOperation({ ...baseProps, item })
|
||||
await user.click(screen.getByRole('button', { name: 'operation.copy' }))
|
||||
expect(copy).toHaveBeenCalledWith('Public legacy answer')
|
||||
})
|
||||
})
|
||||
|
||||
@ -911,6 +990,22 @@ describe('Operation', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should annotate public response parts instead of internal thoughts', async () => {
|
||||
const user = userEvent.setup()
|
||||
const item = createInterruptedItem(['Public interrupted answer'])
|
||||
|
||||
renderOperation({ ...baseProps, item })
|
||||
await user.click(screen.getByTestId('annotation-add-btn'))
|
||||
|
||||
expect(mockContextValue.onAnnotationAdded).toHaveBeenCalledWith(
|
||||
'ann-new',
|
||||
'Test User',
|
||||
'What is this?',
|
||||
'Public interrupted answer',
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
it('should show annotation full modal when limit reached', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockProviderContext.enableBilling = true
|
||||
@ -1000,6 +1095,17 @@ describe('Operation', () => {
|
||||
expect(screen.getByTestId('audio-btn'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should send public response parts to TTS instead of internal thoughts', () => {
|
||||
const item = createInterruptedItem(['Public interrupted answer'])
|
||||
|
||||
renderOperation({ ...baseProps, item })
|
||||
|
||||
expect(screen.getByTestId('audio-btn')).toHaveAttribute(
|
||||
'data-value',
|
||||
'Public interrupted answer',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not show audio button for humanInputFormDataList', () => {
|
||||
const item = { ...baseItem, humanInputFormDataList: [{}] } as ChatItem
|
||||
renderOperation({ ...baseProps, item })
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ThoughtItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItem } from '@/app/components/base/chat/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
|
||||
type AgentRosterResponseContentProps = {
|
||||
item: ChatItem
|
||||
@ -13,7 +15,11 @@ type AgentRosterResponseContentProps = {
|
||||
content?: string
|
||||
}
|
||||
|
||||
type ToolProcess = {
|
||||
const SHELL_TOOL_NAMES = new Set(['shell_run', 'shell_wait', 'shell_input', 'shell_interrupt'])
|
||||
|
||||
type ToolActivity = {
|
||||
kind: 'shell' | 'tool'
|
||||
key: string
|
||||
name: string
|
||||
label: string
|
||||
input: string
|
||||
@ -21,6 +27,18 @@ type ToolProcess = {
|
||||
isFinished: boolean
|
||||
}
|
||||
|
||||
type AgentActivityEntry =
|
||||
| {
|
||||
type: 'message'
|
||||
content: string
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'thought'
|
||||
thought: ThoughtItem
|
||||
key: string
|
||||
}
|
||||
|
||||
function readIndexedValue(value: string, isArray: boolean, index: number) {
|
||||
if (!isArray) return value
|
||||
|
||||
@ -32,7 +50,11 @@ function readIndexedValue(value: string, isArray: boolean, index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function getToolProcesses(thought: ThoughtItem, responding?: boolean): ToolProcess[] {
|
||||
function getToolActivities(
|
||||
thought: ThoughtItem,
|
||||
language: string,
|
||||
responding?: boolean,
|
||||
): ToolActivity[] {
|
||||
let toolNames = [thought.tool]
|
||||
let isArray = false
|
||||
|
||||
@ -44,9 +66,13 @@ function getToolProcesses(thought: ThoughtItem, responding?: boolean): ToolProce
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const labelLanguage = getLanguage(language as Locale)
|
||||
|
||||
return toolNames.filter(Boolean).map((name, index) => ({
|
||||
kind: SHELL_TOOL_NAMES.has(name) ? 'shell' : 'tool',
|
||||
key: `${name}-${index}`,
|
||||
name,
|
||||
label: thought.tool_labels?.toolName?.en_US ?? name,
|
||||
label: renderI18nObject(thought.tool_labels?.[name] ?? {}, labelLanguage) || name,
|
||||
input: readIndexedValue(thought.tool_input, isArray, index),
|
||||
output: readIndexedValue(thought.observation, isArray, index),
|
||||
isFinished: !!thought.observation || !responding,
|
||||
@ -55,10 +81,11 @@ function getToolProcesses(thought: ThoughtItem, responding?: boolean): ToolProce
|
||||
|
||||
function formatDuration(seconds: number, t: ReturnType<typeof useTranslation<'agentV2'>>['t']) {
|
||||
const safeSeconds = Math.max(0, seconds)
|
||||
if (safeSeconds < 60)
|
||||
if (safeSeconds < 60) {
|
||||
return t(($) => $['agentDetail.configure.answer.duration.second'], {
|
||||
count: Number(safeSeconds.toFixed(2)),
|
||||
})
|
||||
}
|
||||
|
||||
const minutes = Math.floor(safeSeconds / 60)
|
||||
const remainingSeconds = Math.floor(safeSeconds % 60)
|
||||
@ -66,31 +93,11 @@ function formatDuration(seconds: number, t: ReturnType<typeof useTranslation<'ag
|
||||
return [
|
||||
t(($) => $['agentDetail.configure.answer.duration.minute'], { count: minutes }),
|
||||
t(($) => $['agentDetail.configure.answer.duration.second'], { count: remainingSeconds }),
|
||||
].join(' ')
|
||||
].join('')
|
||||
}
|
||||
|
||||
function formatLatencyDuration(
|
||||
latency: NonNullable<ChatItem['more']>['latency'],
|
||||
t: ReturnType<typeof useTranslation<'agentV2'>>['t'],
|
||||
) {
|
||||
const numericLatency = Number(latency)
|
||||
if (!Number.isNaN(numericLatency)) return formatDuration(numericLatency, t)
|
||||
|
||||
return String(latency)
|
||||
}
|
||||
|
||||
function getCompletedTitle(
|
||||
latency: NonNullable<ChatItem['more']>['latency'] | undefined,
|
||||
t: ReturnType<typeof useTranslation<'agentV2'>>['t'],
|
||||
) {
|
||||
const numericLatency = Number(latency)
|
||||
if (latency != null && !Number.isNaN(numericLatency) && numericLatency > 0) {
|
||||
return t(($) => $['agentDetail.configure.answer.workedFor'], {
|
||||
duration: formatLatencyDuration(latency, t),
|
||||
})
|
||||
}
|
||||
|
||||
return t(($) => $['agentDetail.configure.answer.workFinished'])
|
||||
function getThoughtKey(thought: ThoughtItem) {
|
||||
return thought.id || `${thought.message_id}-${thought.position}`
|
||||
}
|
||||
|
||||
function hashString(value: string) {
|
||||
@ -100,10 +107,45 @@ function hashString(value: string) {
|
||||
return (hash >>> 0).toString(36)
|
||||
}
|
||||
|
||||
function getAgentResponsePartBaseKey(part: NonNullable<ChatItem['agent_response_parts']>[number]) {
|
||||
if (part.type === 'message') return `message-${part.content.length}-${hashString(part.content)}`
|
||||
function hasVisibleActivity(thought: ThoughtItem) {
|
||||
return !!thought.tool || !!thought.message_files?.length
|
||||
}
|
||||
|
||||
return `thought-${part.thought.id || `${part.thought.message_id}-${part.thought.position}`}`
|
||||
function getAgentActivityEntries(item: ChatItem): AgentActivityEntry[] {
|
||||
if (item.agent_response_parts?.length) {
|
||||
const keyOccurrences = new Map<string, number>()
|
||||
|
||||
return item.agent_response_parts.flatMap<AgentActivityEntry>((part) => {
|
||||
const baseKey =
|
||||
part.type === 'message'
|
||||
? `message-${part.content.length}-${hashString(part.content)}`
|
||||
: `thought-${getThoughtKey(part.thought)}`
|
||||
const occurrence = keyOccurrences.get(baseKey) ?? 0
|
||||
keyOccurrences.set(baseKey, occurrence + 1)
|
||||
const key = occurrence ? `${baseKey}-${occurrence}` : baseKey
|
||||
|
||||
if (part.type === 'message')
|
||||
return part.content ? [{ type: 'message', content: part.content, key }] : []
|
||||
|
||||
return hasVisibleActivity(part.thought)
|
||||
? [{ type: 'thought', thought: part.thought, key }]
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
return [...(item.agent_thoughts ?? [])]
|
||||
.sort((left, right) => left.position - right.position)
|
||||
.flatMap((thought) => {
|
||||
const parts: AgentActivityEntry[] = []
|
||||
const key = getThoughtKey(thought)
|
||||
const answer = thought.answer
|
||||
if (answer?.trim()) parts.push({ type: 'message', content: answer, key: `message-${key}` })
|
||||
|
||||
if (hasVisibleActivity(thought))
|
||||
parts.push({ type: 'thought', thought, key: `thought-${key}` })
|
||||
|
||||
return parts
|
||||
})
|
||||
}
|
||||
|
||||
function useWorkingDuration(enabled?: boolean) {
|
||||
@ -113,7 +155,7 @@ function useWorkingDuration(enabled?: boolean) {
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
|
||||
startedAtRef.current = Date.now()
|
||||
startedAtRef.current ??= Date.now()
|
||||
const timer = window.setInterval(() => {
|
||||
setNow(Date.now())
|
||||
}, 1000)
|
||||
@ -121,197 +163,115 @@ function useWorkingDuration(enabled?: boolean) {
|
||||
return () => window.clearInterval(timer)
|
||||
}, [enabled])
|
||||
|
||||
const elapsedSeconds =
|
||||
enabled && startedAtRef.current !== null
|
||||
? Math.max(0, Math.floor((now - startedAtRef.current) / 1000))
|
||||
: 0
|
||||
|
||||
return elapsedSeconds
|
||||
if (!enabled || startedAtRef.current === null) return 0
|
||||
return Math.max(0, Math.floor((now - startedAtRef.current) / 1000))
|
||||
}
|
||||
|
||||
function ProcessShell({
|
||||
children,
|
||||
collapsed,
|
||||
expandedTitle,
|
||||
icon,
|
||||
title,
|
||||
defaultOpen = false,
|
||||
}: {
|
||||
children?: ReactNode
|
||||
collapsed?: boolean
|
||||
expandedTitle?: ReactNode
|
||||
icon: ReactNode
|
||||
title: ReactNode
|
||||
defaultOpen?: boolean
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
const canExpand = !!children
|
||||
const expanded = canExpand && open && !collapsed
|
||||
|
||||
function ResponseMessage({ content }: { content: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-xl p-2',
|
||||
expanded ? 'bg-background-default' : 'bg-background-default-subtle',
|
||||
)}
|
||||
className="max-w-full min-w-0 overflow-hidden px-1 body-md-regular text-text-primary"
|
||||
data-testid="agent-content-markdown"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-1 text-left"
|
||||
onClick={() => canExpand && setOpen((value) => !value)}
|
||||
>
|
||||
<span className="flex size-5 shrink-0 items-center justify-center text-text-secondary">
|
||||
{icon}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate text-text-secondary',
|
||||
expanded ? 'system-xs-medium-uppercase' : 'system-sm-regular',
|
||||
)}
|
||||
>
|
||||
{expanded ? (expandedTitle ?? title) : title}
|
||||
</span>
|
||||
{canExpand &&
|
||||
(expanded ? (
|
||||
<span
|
||||
className="i-ri-arrow-down-s-line size-4 shrink-0 text-text-quaternary"
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="i-ri-arrow-right-s-line size-4 shrink-0 text-text-quaternary"
|
||||
aria-hidden
|
||||
/>
|
||||
))}
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mt-1 flex gap-1">
|
||||
<div className="w-5 shrink-0 border-l border-divider-subtle" />
|
||||
<div className="min-w-0 flex-1 py-1 body-sm-regular text-text-tertiary">{children}</div>
|
||||
</div>
|
||||
)}
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ThoughtProcess({ thought, defaultOpen }: { thought: ThoughtItem; defaultOpen?: boolean }) {
|
||||
function ToolActivityItem({ tool }: { tool: ToolActivity }) {
|
||||
const { t } = useTranslation()
|
||||
const thoughtTitle = t(($) => $['chat.thought'], { ns: 'common' })
|
||||
const summary = thought.thought.trim() || thoughtTitle
|
||||
const hasDetails = !!tool.input || !!tool.output
|
||||
const label =
|
||||
tool.name === 'shell_run' && tool.label === tool.name
|
||||
? tool.isFinished
|
||||
? t(($) => $['agentDetail.configure.answer.activity.ranCommands'], { ns: 'agentV2' })
|
||||
: t(($) => $['agentDetail.configure.answer.activity.runningCommands'], {
|
||||
ns: 'agentV2',
|
||||
})
|
||||
: tool.label
|
||||
|
||||
return (
|
||||
<ProcessShell
|
||||
icon={<span className="i-custom-public-thought-imagine size-3.5" aria-hidden />}
|
||||
title={summary}
|
||||
expandedTitle={thoughtTitle.toUpperCase()}
|
||||
defaultOpen={defaultOpen}
|
||||
>
|
||||
<Markdown content={summary} />
|
||||
</ProcessShell>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolProcessCard({ tool }: { tool: ToolProcess }) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-background-default-subtle p-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-1 text-left"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<span className="flex size-5 shrink-0 items-center justify-center rounded-md border border-divider-subtle bg-components-icon-bg-midnight-solid p-[3px] text-text-primary-on-surface shadow-xs">
|
||||
{tool.isFinished ? (
|
||||
const content = (
|
||||
<>
|
||||
<span className="inline-flex min-w-0 items-center gap-1">
|
||||
<span className="flex size-3.5 shrink-0 items-center justify-center text-text-tertiary">
|
||||
{!tool.isFinished ? (
|
||||
<span className="i-ri-loader-2-line size-3.5 animate-spin" aria-hidden />
|
||||
) : tool.kind === 'shell' ? (
|
||||
<span className="i-ri-terminal-box-line size-3.5" aria-hidden />
|
||||
) : (
|
||||
<span className="i-ri-loader-2-line size-3.5 animate-spin" aria-hidden />
|
||||
<span className="i-ri-hammer-line size-3.5" aria-hidden />
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-secondary">
|
||||
{tool.label}
|
||||
</span>
|
||||
{open ? (
|
||||
<span
|
||||
className="i-ri-arrow-down-s-line size-4 shrink-0 text-text-quaternary"
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="i-ri-arrow-right-s-line size-4 shrink-0 text-text-quaternary"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
{open && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="rounded-[10px] bg-components-panel-on-panel-item-bg px-3 py-2">
|
||||
<div className="system-xs-semibold-uppercase text-text-tertiary">
|
||||
{t(($) => $['thought.requestTitle'], { ns: 'tools' })}
|
||||
<span className="min-w-0 truncate text-text-secondary">{label}</span>
|
||||
</span>
|
||||
{hasDetails && (
|
||||
<span
|
||||
className="i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary transition-transform duration-100 ease-out group-data-panel-open/tool:rotate-90 motion-reduce:transition-none"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-full min-w-0 flex-col items-start">
|
||||
{hasDetails ? (
|
||||
<Collapsible className="w-full max-w-full items-start">
|
||||
<CollapsibleTrigger className="group/tool h-6 min-h-0 w-auto max-w-full justify-start gap-0 rounded-md p-1 text-left system-xs-medium text-text-tertiary hover:not-data-disabled:bg-state-base-hover focus-visible:bg-state-base-hover">
|
||||
{content}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsiblePanel className="w-full max-w-full">
|
||||
<div className="w-full max-w-full min-w-0 space-y-1 pb-1">
|
||||
{!!tool.input && (
|
||||
<div className="w-full max-w-full min-w-0 overflow-hidden rounded-[10px] bg-components-input-bg-normal px-3 py-2">
|
||||
<div className="system-xs-semibold-uppercase text-text-tertiary">
|
||||
{t(($) => $['thought.requestTitle'], { ns: 'tools' })}
|
||||
</div>
|
||||
<div className="mt-1 max-w-full min-w-0 code-xs-regular wrap-break-word whitespace-pre-wrap text-text-secondary">
|
||||
{tool.input}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!tool.output && (
|
||||
<div className="w-full max-w-full min-w-0 overflow-hidden rounded-[10px] bg-components-input-bg-normal px-3 py-2">
|
||||
<div className="system-xs-semibold-uppercase text-text-tertiary">
|
||||
{t(($) => $['thought.responseTitle'], { ns: 'tools' })}
|
||||
</div>
|
||||
<div className="mt-1 max-w-full min-w-0 code-xs-regular wrap-break-word whitespace-pre-wrap text-text-secondary">
|
||||
{tool.output}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 code-xs-regular wrap-break-word text-text-secondary">
|
||||
{tool.input}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[10px] bg-components-panel-on-panel-item-bg px-3 py-2">
|
||||
<div className="system-xs-semibold-uppercase text-text-tertiary">
|
||||
{t(($) => $['thought.responseTitle'], { ns: 'tools' })}
|
||||
</div>
|
||||
<div className="mt-1 code-xs-regular wrap-break-word text-text-secondary">
|
||||
{tool.output}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsiblePanel>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<div className="inline-flex h-6 w-auto max-w-full items-center rounded-md p-1 system-xs-medium">
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentThoughtsProcessList({ item, responding }: { item: ChatItem; responding?: boolean }) {
|
||||
return (
|
||||
<div className="mt-2 flex flex-col gap-1">
|
||||
{item.agent_thoughts?.map((thought, index) => (
|
||||
<AgentThoughtProcessItem
|
||||
key={thought.id || `${thought.message_id}-${thought.position}`}
|
||||
thought={thought}
|
||||
responding={responding}
|
||||
defaultOpen={index === 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentThoughtProcessItem({
|
||||
function AgentActivityItem({
|
||||
thought,
|
||||
responding,
|
||||
defaultOpen,
|
||||
}: {
|
||||
thought: ThoughtItem
|
||||
responding?: boolean
|
||||
defaultOpen?: boolean
|
||||
}) {
|
||||
const tools = getToolProcesses(thought, responding)
|
||||
const answer = thought.answer?.trim()
|
||||
const { i18n } = useTranslation()
|
||||
const tools = getToolActivities(thought, i18n.language, responding)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{answer && (
|
||||
<div
|
||||
className="px-2 py-2 body-md-regular text-text-primary"
|
||||
data-testid="agent-content-markdown"
|
||||
>
|
||||
<Markdown content={thought.answer || ''} />
|
||||
</div>
|
||||
)}
|
||||
{!answer && thought.thought && <ThoughtProcess thought={thought} defaultOpen={defaultOpen} />}
|
||||
<div className="flex w-full max-w-full min-w-0 flex-col py-0.5">
|
||||
{tools.map((tool) => (
|
||||
<ToolProcessCard key={`${thought.id}-${tool.name}`} tool={tool} />
|
||||
<ToolActivityItem key={`${getThoughtKey(thought)}-${tool.key}`} tool={tool} />
|
||||
))}
|
||||
{!!thought.message_files?.length && (
|
||||
<FileList
|
||||
className="px-2 py-1"
|
||||
className="py-1"
|
||||
files={thought.message_files}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction
|
||||
@ -322,80 +282,58 @@ function AgentThoughtProcessItem({
|
||||
)
|
||||
}
|
||||
|
||||
function AgentResponsePartList({ item, responding }: { item: ChatItem; responding?: boolean }) {
|
||||
const keyOccurrences = new Map<string, number>()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{item.agent_response_parts?.map((part, index) => {
|
||||
const baseKey = getAgentResponsePartBaseKey(part)
|
||||
const occurrence = keyOccurrences.get(baseKey) ?? 0
|
||||
keyOccurrences.set(baseKey, occurrence + 1)
|
||||
const partKey = occurrence ? `${baseKey}-${occurrence}` : baseKey
|
||||
|
||||
if (part.type === 'message') {
|
||||
if (!part.content) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={partKey}
|
||||
className="px-2 py-2 body-md-regular text-text-primary"
|
||||
data-testid="agent-content-markdown"
|
||||
>
|
||||
<Markdown content={part.content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AgentThoughtProcessItem
|
||||
key={partKey}
|
||||
thought={part.thought}
|
||||
responding={responding}
|
||||
defaultOpen={index === 0}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentThoughtsProcessGroup({ item, responding }: { item: ChatItem; responding?: boolean }) {
|
||||
function AgentActivityDisclosure({
|
||||
item,
|
||||
entries,
|
||||
responding,
|
||||
defaultOpen,
|
||||
}: {
|
||||
item: ChatItem
|
||||
entries: AgentActivityEntry[]
|
||||
responding?: boolean
|
||||
defaultOpen?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const [open, setOpen] = useState(false)
|
||||
const workingDuration = formatDuration(useWorkingDuration(responding), t)
|
||||
const completedTitle = getCompletedTitle(item.more?.latency, t)
|
||||
|
||||
if (responding) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-9 w-full items-center border-b border-divider-subtle text-left">
|
||||
<span className="system-md-regular text-text-tertiary">
|
||||
{t(($) => $['agentDetail.configure.answer.workingFor'], { duration: workingDuration })}
|
||||
</span>
|
||||
</div>
|
||||
<AgentThoughtsProcessList item={item} responding={responding} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const workingDuration = useWorkingDuration(responding)
|
||||
const latency = Number(item.more?.latency)
|
||||
const duration = responding
|
||||
? formatDuration(workingDuration, t)
|
||||
: Number.isFinite(latency) && latency > 0
|
||||
? formatDuration(latency, t)
|
||||
: undefined
|
||||
const thinking = t(($) => $['agentDetail.configure.answer.thinking'])
|
||||
const title = duration ? `${thinking} · ${duration}` : thinking
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-9 w-full items-center gap-1 border-b border-divider-subtle text-left"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
<Collapsible className="w-full max-w-full items-start gap-1" defaultOpen={defaultOpen}>
|
||||
<CollapsibleTrigger
|
||||
aria-label={title}
|
||||
className="group/thinking h-6 min-h-0 w-auto max-w-full justify-start gap-1 rounded-md p-1 text-left system-xs-medium text-text-tertiary hover:not-data-disabled:bg-state-base-hover focus-visible:bg-state-base-hover"
|
||||
>
|
||||
<span className="system-md-regular text-text-tertiary">{completedTitle}</span>
|
||||
{open ? (
|
||||
<span className="i-ri-arrow-down-s-line size-4 text-text-tertiary" aria-hidden />
|
||||
) : (
|
||||
<span className="i-ri-arrow-right-s-line size-4 text-text-tertiary" aria-hidden />
|
||||
<span>{thinking}</span>
|
||||
{duration && (
|
||||
<>
|
||||
<span className="system-xs-regular text-text-quaternary">·</span>
|
||||
<span>{duration}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{open && <AgentThoughtsProcessList item={item} responding={responding} />}
|
||||
</div>
|
||||
<span
|
||||
className="i-ri-arrow-right-s-line size-4 transition-transform duration-100 ease-out group-data-panel-open/thinking:rotate-90 motion-reduce:transition-none"
|
||||
aria-hidden
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsiblePanel className="w-full max-w-full">
|
||||
<div className="flex w-full max-w-full min-w-0 flex-col gap-1 overflow-hidden">
|
||||
{entries.map((entry) =>
|
||||
entry.type === 'message' ? (
|
||||
<ResponseMessage key={entry.key} content={entry.content} />
|
||||
) : (
|
||||
<AgentActivityItem key={entry.key} thought={entry.thought} responding={responding} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</CollapsiblePanel>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
@ -404,37 +342,47 @@ export function AgentRosterResponseContent({
|
||||
responding,
|
||||
content,
|
||||
}: AgentRosterResponseContentProps) {
|
||||
const { annotation, agent_thoughts } = item
|
||||
|
||||
if (annotation?.logAnnotation) {
|
||||
if (item.annotation?.logAnnotation) {
|
||||
return (
|
||||
<Markdown
|
||||
content={annotation.logAnnotation.content || ''}
|
||||
content={item.annotation.logAnnotation.content || ''}
|
||||
data-testid="agent-content-markdown"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const entries = getAgentActivityEntries(item)
|
||||
const hasLiveResponseParts = !!item.agent_response_parts?.length
|
||||
const hasThinkingStatus =
|
||||
entries.length === 0 && !!item.agent_response_parts?.some((part) => part.type === 'thought')
|
||||
const hasActivity =
|
||||
hasThinkingStatus ||
|
||||
(hasLiveResponseParts ? entries.some((entry) => entry.type === 'thought') : entries.length > 0)
|
||||
const standaloneMessages = hasLiveResponseParts
|
||||
? hasActivity
|
||||
? []
|
||||
: entries.flatMap((entry) => (entry.type === 'message' ? [entry] : []))
|
||||
: content
|
||||
? [{ type: 'message' as const, content, key: 'final-answer' }]
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1" data-testid="agent-roster-response-content">
|
||||
{!!item.agent_response_parts?.length && (
|
||||
<AgentResponsePartList item={item} responding={responding} />
|
||||
)}
|
||||
{!item.agent_response_parts?.length && (
|
||||
<>
|
||||
{!!agent_thoughts?.length && (
|
||||
<AgentThoughtsProcessGroup item={item} responding={responding} />
|
||||
)}
|
||||
{content && (
|
||||
<div
|
||||
className="px-2 py-2 body-md-regular text-text-primary"
|
||||
data-testid="agent-content-markdown"
|
||||
>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div
|
||||
className="flex w-full max-w-full min-w-0 flex-col gap-1 overflow-hidden"
|
||||
data-testid="agent-roster-response-content"
|
||||
>
|
||||
{hasActivity && (
|
||||
<AgentActivityDisclosure
|
||||
key={hasLiveResponseParts ? 'live' : 'history'}
|
||||
item={item}
|
||||
entries={entries}
|
||||
responding={responding}
|
||||
defaultOpen={hasLiveResponseParts && (!!responding || entries.length > 0)}
|
||||
/>
|
||||
)}
|
||||
{standaloneMessages.map((message) => (
|
||||
<ResponseMessage key={message.key} content={message.content} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -40,10 +40,18 @@ const More: FC<MoreProps> = ({ more }) => {
|
||||
{`${more.tokens_per_second} tokens/s`}
|
||||
</div>
|
||||
)}
|
||||
<div className="mx-2 shrink-0">·</div>
|
||||
<div className="max-w-[25%] shrink-0 truncate" title={more.time} data-testid="more-time">
|
||||
{more.time}
|
||||
</div>
|
||||
{!!more.time && (
|
||||
<>
|
||||
<div className="mx-2 shrink-0">·</div>
|
||||
<div
|
||||
className="max-w-[25%] shrink-0 truncate"
|
||||
title={more.time}
|
||||
data-testid="more-time"
|
||||
>
|
||||
{more.time}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -42,6 +42,23 @@ const feedbackTooltipClassName = 'max-w-[260px]'
|
||||
const answerActiveFlexClassName = 'group-hover:flex group-has-[[data-popup-open]]:flex'
|
||||
const answerActiveBlockClassName = 'group-hover:block group-has-[[data-popup-open]]:block'
|
||||
|
||||
function joinPublicContent(blocks: Array<string | undefined>) {
|
||||
return blocks.filter((block): block is string => !!block?.trim()).join('\n\n')
|
||||
}
|
||||
|
||||
function getPublicResponseContent(item: ChatItem) {
|
||||
if (item.content.trim()) return item.content
|
||||
|
||||
const responseContent = joinPublicContent(
|
||||
item.agent_response_parts?.map((part) =>
|
||||
part.type === 'message' ? part.content : undefined,
|
||||
) ?? [],
|
||||
)
|
||||
if (responseContent) return responseContent
|
||||
|
||||
return joinPublicContent(item.agent_thoughts?.map((thought) => thought.answer) ?? [])
|
||||
}
|
||||
|
||||
const FeedbackTooltip = ({ content, children }: FeedbackTooltipProps) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
@ -74,16 +91,8 @@ function Operation({
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const [isShowFeedbackModal, setIsShowFeedbackModal] = useState(false)
|
||||
const [feedbackContent, setFeedbackContent] = useState('')
|
||||
const {
|
||||
id,
|
||||
isOpeningStatement,
|
||||
content: messageContent,
|
||||
annotation,
|
||||
feedback,
|
||||
adminFeedback,
|
||||
agent_thoughts,
|
||||
humanInputFormDataList,
|
||||
} = item
|
||||
const { id, isOpeningStatement, annotation, feedback, adminFeedback, humanInputFormDataList } =
|
||||
item
|
||||
const [userLocalFeedback, setUserLocalFeedback] = useState(feedback)
|
||||
const [adminLocalFeedback, setAdminLocalFeedback] = useState(adminFeedback)
|
||||
const [feedbackTarget, setFeedbackTarget] = useState<'user' | 'admin'>('user')
|
||||
@ -91,11 +100,8 @@ function Operation({
|
||||
|
||||
const userFeedback = feedback
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (agent_thoughts?.length) return agent_thoughts.reduce((acc, cur) => acc + cur.thought, '')
|
||||
|
||||
return messageContent
|
||||
}, [agent_thoughts, messageContent])
|
||||
const content = getPublicResponseContent(item)
|
||||
const hasPublicContent = !!content.trim()
|
||||
|
||||
const displayUserFeedback = userLocalFeedback ?? userFeedback
|
||||
|
||||
@ -110,6 +116,7 @@ function Operation({
|
||||
!readonly && !!onAnnotationAdded && !!onAnnotationEdited && !!onAnnotationRemoved
|
||||
const shouldShowAnnotationAction =
|
||||
canManageAnnotation &&
|
||||
hasPublicContent &&
|
||||
!!config?.supportAnnotation &&
|
||||
!!config.annotation_reply?.enabled &&
|
||||
!humanInputFormDataList?.length
|
||||
@ -177,7 +184,7 @@ function Operation({
|
||||
let width = 0
|
||||
if (!isOpeningStatement) width += 26
|
||||
if (!isOpeningStatement && showPromptLog) width += 28 + 8
|
||||
if (!isOpeningStatement && config?.text_to_speech?.enabled) width += 26
|
||||
if (!isOpeningStatement && config?.text_to_speech?.enabled && hasPublicContent) width += 26
|
||||
if (!isOpeningStatement && shouldShowAnnotationAction) width += 26
|
||||
if (shouldShowUserFeedbackBar) width += hasUserFeedback ? 28 + 8 : 60 + 8
|
||||
if (shouldShowAdminFeedbackBar)
|
||||
@ -187,6 +194,7 @@ function Operation({
|
||||
}, [
|
||||
config?.text_to_speech?.enabled,
|
||||
hasAdminFeedback,
|
||||
hasPublicContent,
|
||||
hasUserFeedback,
|
||||
isOpeningStatement,
|
||||
shouldShowAdminFeedbackBar,
|
||||
@ -365,10 +373,12 @@ function Operation({
|
||||
)}
|
||||
data-testid="operation-actions"
|
||||
>
|
||||
{config?.text_to_speech?.enabled && !humanInputFormDataList?.length && (
|
||||
<NewAudioButton id={id} value={content} voice={config?.text_to_speech?.voice} />
|
||||
)}
|
||||
{!humanInputFormDataList?.length && (
|
||||
{config?.text_to_speech?.enabled &&
|
||||
hasPublicContent &&
|
||||
!humanInputFormDataList?.length && (
|
||||
<NewAudioButton id={id} value={content} voice={config?.text_to_speech?.voice} />
|
||||
)}
|
||||
{hasPublicContent && !humanInputFormDataList?.length && (
|
||||
<ActionButton
|
||||
aria-label={copyLabel}
|
||||
onClick={() => {
|
||||
|
||||
@ -775,6 +775,59 @@ describe('AgentPreviewChat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should preserve tool labels when formatting chat history', async () => {
|
||||
chatMessagesGetMock.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'message-with-tool-label',
|
||||
conversation_id: 'conversation-1',
|
||||
query: 'run pwd',
|
||||
answer: '',
|
||||
inputs: {},
|
||||
message: [],
|
||||
message_files: [],
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-with-tool-label',
|
||||
message_id: 'message-with-tool-label',
|
||||
thought: '',
|
||||
answer: '',
|
||||
tool: 'shell_run',
|
||||
tool_input: 'pwd',
|
||||
tool_labels: {
|
||||
shell_run: {
|
||||
en_US: 'Ran commands',
|
||||
zh_Hans: '运行了命令',
|
||||
},
|
||||
},
|
||||
observation: '/workspace',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
feedbacks: [],
|
||||
status: 'success',
|
||||
from_source: 'console',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
renderPreviewChat({ conversationId: 'conversation-1' })
|
||||
|
||||
await waitFor(() => {
|
||||
const formattedTree = useChatMock.mock.calls.find((call) => {
|
||||
const chatTree = call[2]
|
||||
return JSON.stringify(chatTree).includes('thought-with-tool-label')
|
||||
})?.[2]
|
||||
|
||||
expect(formattedTree?.[0]?.children?.[0]?.agent_thoughts?.[0]?.tool_labels).toEqual({
|
||||
shell_run: {
|
||||
en_US: 'Ran commands',
|
||||
zh_Hans: '运行了命令',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify the owner when a send settles with an error', async () => {
|
||||
const onSendInterrupted = vi.fn()
|
||||
renderPreviewChat({
|
||||
|
||||
@ -271,12 +271,33 @@ const toLogMessages = (
|
||||
]
|
||||
}
|
||||
|
||||
function toToolLabels(value: AgentThought['tool_labels']): ThoughtItem['tool_labels'] {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
||||
|
||||
const toolLabels: NonNullable<ThoughtItem['tool_labels']> = {}
|
||||
for (const [name, label] of Object.entries(value)) {
|
||||
if (!label || typeof label !== 'object' || Array.isArray(label)) continue
|
||||
|
||||
const enUS = 'en_US' in label ? label.en_US : undefined
|
||||
const zhHans = 'zh_Hans' in label ? label.zh_Hans : undefined
|
||||
if (typeof enUS !== 'string' || typeof zhHans !== 'string') continue
|
||||
|
||||
toolLabels[name] = { en_US: enUS, zh_Hans: zhHans }
|
||||
for (const [locale, localizedLabel] of Object.entries(label)) {
|
||||
if (typeof localizedLabel === 'string') toolLabels[name][locale] = localizedLabel
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(toolLabels).length ? toolLabels : undefined
|
||||
}
|
||||
|
||||
const toAgentThoughtItem = (thought: AgentThought, conversationId: string): ThoughtItem => ({
|
||||
id: thought.id,
|
||||
tool: thought.tool ?? '',
|
||||
thought: thought.thought ?? '',
|
||||
answer: thought.answer ?? '',
|
||||
tool_input: thought.tool_input ?? '',
|
||||
tool_labels: toToolLabels(thought.tool_labels),
|
||||
message_id: thought.message_id,
|
||||
conversation_id: conversationId,
|
||||
observation: thought.observation ?? '',
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "الإعدادات المتقدمة",
|
||||
"agentDetail.configure.advancedSettings.toggle": "تبديل الإعدادات المتقدمة",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "تم تشغيل الأوامر",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "جارٍ تشغيل الأوامر",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} د",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} ث",
|
||||
"agentDetail.configure.answer.thinking": "جارٍ التفكير",
|
||||
"agentDetail.configure.answer.workFinished": "اكتمل العمل",
|
||||
"agentDetail.configure.answer.workedFor": "عمل لمدة {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "يعمل منذ {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Erweiterte Einstellungen",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Erweiterte Einstellungen umschalten",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Befehle ausgeführt",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Befehle werden ausgeführt",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} Min.",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Denkt nach",
|
||||
"agentDetail.configure.answer.workFinished": "Arbeit abgeschlossen",
|
||||
"agentDetail.configure.answer.workedFor": "Gearbeitet für {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Arbeitet seit {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Advanced Settings",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Toggle advanced settings",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Ran commands",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Running commands",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}}m",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}}s",
|
||||
"agentDetail.configure.answer.thinking": "Thinking",
|
||||
"agentDetail.configure.answer.workFinished": "Work finished",
|
||||
"agentDetail.configure.answer.workedFor": "Worked for {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Working for {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Configuración avanzada",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Alternar configuración avanzada",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Comandos ejecutados",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Ejecutando comandos",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Pensando",
|
||||
"agentDetail.configure.answer.workFinished": "Trabajo finalizado",
|
||||
"agentDetail.configure.answer.workedFor": "Trabajó durante {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Trabajando durante {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "تنظیمات پیشرفته",
|
||||
"agentDetail.configure.advancedSettings.toggle": "تغییر تنظیمات پیشرفته",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "دستورات اجرا شدند",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "در حال اجرای دستورات",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} دقیقه",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} ثانیه",
|
||||
"agentDetail.configure.answer.thinking": "در حال فکر کردن",
|
||||
"agentDetail.configure.answer.workFinished": "کار تمام شد",
|
||||
"agentDetail.configure.answer.workedFor": "به مدت {{duration}} کار کرد",
|
||||
"agentDetail.configure.answer.workingFor": "در حال کار به مدت {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Paramètres avancés",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Basculer les paramètres avancés",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Commandes exécutées",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Exécution des commandes",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Réflexion en cours",
|
||||
"agentDetail.configure.answer.workFinished": "Travail terminé",
|
||||
"agentDetail.configure.answer.workedFor": "A travaillé pendant {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Travaille depuis {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "उन्नत सेटिंग्स",
|
||||
"agentDetail.configure.advancedSettings.toggle": "उन्नत सेटिंग्स टॉगल करें",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "कमांड चलाए गए",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "कमांड चलाए जा रहे हैं",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} मिनट",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} सेकंड",
|
||||
"agentDetail.configure.answer.thinking": "सोच रहा है",
|
||||
"agentDetail.configure.answer.workFinished": "काम पूरा हुआ",
|
||||
"agentDetail.configure.answer.workedFor": "{{duration}} तक काम किया",
|
||||
"agentDetail.configure.answer.workingFor": "{{duration}} से काम कर रहा है",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Pengaturan Lanjutan",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Alihkan pengaturan lanjutan",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Perintah dijalankan",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Menjalankan perintah",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} mnt",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} dtk",
|
||||
"agentDetail.configure.answer.thinking": "Sedang berpikir",
|
||||
"agentDetail.configure.answer.workFinished": "Pekerjaan selesai",
|
||||
"agentDetail.configure.answer.workedFor": "Bekerja selama {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Bekerja selama {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Impostazioni avanzate",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Attiva/disattiva impostazioni avanzate",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Comandi eseguiti",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Esecuzione dei comandi",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Sto pensando",
|
||||
"agentDetail.configure.answer.workFinished": "Lavoro terminato",
|
||||
"agentDetail.configure.answer.workedFor": "Ha lavorato per {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Sta lavorando da {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "詳細設定",
|
||||
"agentDetail.configure.advancedSettings.toggle": "詳細設定の表示を切り替え",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "コマンドを実行しました",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "コマンドを実行中",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}}分",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}}秒",
|
||||
"agentDetail.configure.answer.thinking": "思考中",
|
||||
"agentDetail.configure.answer.workFinished": "作業が完了しました",
|
||||
"agentDetail.configure.answer.workedFor": "{{duration}} 実行しました",
|
||||
"agentDetail.configure.answer.workingFor": "{{duration}} 実行中",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "고급 설정",
|
||||
"agentDetail.configure.advancedSettings.toggle": "고급 설정 전환",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "명령 실행됨",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "명령 실행 중",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}}분",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}}초",
|
||||
"agentDetail.configure.answer.thinking": "생각 중",
|
||||
"agentDetail.configure.answer.workFinished": "작업 완료",
|
||||
"agentDetail.configure.answer.workedFor": "{{duration}} 동안 작업함",
|
||||
"agentDetail.configure.answer.workingFor": "{{duration}} 동안 작업 중",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Geavanceerde instellingen",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Geavanceerde instellingen in/uit",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Opdrachten uitgevoerd",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Opdrachten uitvoeren",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Aan het nadenken",
|
||||
"agentDetail.configure.answer.workFinished": "Werk voltooid",
|
||||
"agentDetail.configure.answer.workedFor": "Gewerkt gedurende {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Bezig gedurende {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Ustawienia zaawansowane",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Przełącz ustawienia zaawansowane",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Uruchomiono polecenia",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Uruchamianie poleceń",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Myśli",
|
||||
"agentDetail.configure.answer.workFinished": "Praca zakończona",
|
||||
"agentDetail.configure.answer.workedFor": "Pracował przez {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Pracuje od {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Configurações avançadas",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Alternar configurações avançadas",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Comandos executados",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Executando comandos",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Pensando",
|
||||
"agentDetail.configure.answer.workFinished": "Trabalho concluído",
|
||||
"agentDetail.configure.answer.workedFor": "Trabalhou por {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Trabalhando há {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Setări avansate",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Comută setările avansate",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Comenzi executate",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Se execută comenzile",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Se gândește",
|
||||
"agentDetail.configure.answer.workFinished": "Lucrare finalizată",
|
||||
"agentDetail.configure.answer.workedFor": "A lucrat timp de {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Lucrează de {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Расширенные настройки",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Переключить расширенные настройки",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Команды выполнены",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Выполнение команд",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} мин",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} с",
|
||||
"agentDetail.configure.answer.thinking": "Размышляет",
|
||||
"agentDetail.configure.answer.workFinished": "Работа завершена",
|
||||
"agentDetail.configure.answer.workedFor": "Работал {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Работает {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Napredne nastavitve",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Preklopi napredne nastavitve",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Ukazi izvedeni",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Izvajanje ukazov",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} min",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} s",
|
||||
"agentDetail.configure.answer.thinking": "Razmišlja",
|
||||
"agentDetail.configure.answer.workFinished": "Delo končano",
|
||||
"agentDetail.configure.answer.workedFor": "Delal {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Dela {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "การตั้งค่าขั้นสูง",
|
||||
"agentDetail.configure.advancedSettings.toggle": "สลับการตั้งค่าขั้นสูง",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "เรียกใช้คำสั่งแล้ว",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "กำลังเรียกใช้คำสั่ง",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} นาที",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} วินาที",
|
||||
"agentDetail.configure.answer.thinking": "กำลังคิด",
|
||||
"agentDetail.configure.answer.workFinished": "งานเสร็จสิ้น",
|
||||
"agentDetail.configure.answer.workedFor": "ทำงานเป็นเวลา {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "กำลังทำงานเป็นเวลา {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Gelişmiş Ayarlar",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Gelişmiş ayarları değiştir",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Komutlar çalıştırıldı",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Komutlar çalıştırılıyor",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} dk",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} sn",
|
||||
"agentDetail.configure.answer.thinking": "Düşünüyor",
|
||||
"agentDetail.configure.answer.workFinished": "İş tamamlandı",
|
||||
"agentDetail.configure.answer.workedFor": "{{duration}} çalıştı",
|
||||
"agentDetail.configure.answer.workingFor": "{{duration}} çalışıyor",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Розширені налаштування",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Перемкнути розширені налаштування",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Команди виконано",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Виконання команд",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} хв",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} с",
|
||||
"agentDetail.configure.answer.thinking": "Розмірковує",
|
||||
"agentDetail.configure.answer.workFinished": "Роботу завершено",
|
||||
"agentDetail.configure.answer.workedFor": "Працював {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Працює {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "Cài đặt nâng cao",
|
||||
"agentDetail.configure.advancedSettings.toggle": "Bật/tắt cài đặt nâng cao",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "Đã chạy lệnh",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "Đang chạy lệnh",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}} phút",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}} giây",
|
||||
"agentDetail.configure.answer.thinking": "Đang suy nghĩ",
|
||||
"agentDetail.configure.answer.workFinished": "Công việc đã hoàn tất",
|
||||
"agentDetail.configure.answer.workedFor": "Đã làm việc trong {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "Đang làm việc trong {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "高级设置",
|
||||
"agentDetail.configure.advancedSettings.toggle": "展开或收起高级设置",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "已运行命令",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "正在运行命令",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}}分",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}}秒",
|
||||
"agentDetail.configure.answer.thinking": "思考过程",
|
||||
"agentDetail.configure.answer.workFinished": "工作已完成",
|
||||
"agentDetail.configure.answer.workedFor": "工作了 {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "工作中 {{duration}}",
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
"agentDetail.configure.advancedSettings.envEditor.valuePlaceholder": "Value",
|
||||
"agentDetail.configure.advancedSettings.label": "進階設定",
|
||||
"agentDetail.configure.advancedSettings.toggle": "展開或收合進階設定",
|
||||
"agentDetail.configure.answer.activity.ranCommands": "已執行命令",
|
||||
"agentDetail.configure.answer.activity.runningCommands": "正在執行命令",
|
||||
"agentDetail.configure.answer.duration.minute": "{{count}}分",
|
||||
"agentDetail.configure.answer.duration.second": "{{count}}秒",
|
||||
"agentDetail.configure.answer.thinking": "思考過程",
|
||||
"agentDetail.configure.answer.workFinished": "工作已完成",
|
||||
"agentDetail.configure.answer.workedFor": "工作了 {{duration}}",
|
||||
"agentDetail.configure.answer.workingFor": "工作中 {{duration}}",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user