mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
refactor(web): scope app UI sessions to their owners (#42984)
This commit is contained in:
65 files changed
+1102
-1363
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import Configuration from '@/app/components/app/configuration'
|
||||
|
||||
const IConfiguration = async () => {
|
||||
return <Configuration />
|
||||
const IConfiguration = async ({ params }: { params: Promise<{ appId: string }> }) => {
|
||||
const { appId } = await params
|
||||
return <Configuration key={appId} />
|
||||
}
|
||||
|
||||
export default IConfiguration
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useStore } from '../store'
|
||||
|
||||
describe('app store', () => {
|
||||
beforeEach(() => {
|
||||
useStore.setState({
|
||||
currentLogModalActiveTab: 'DETAIL',
|
||||
showMessageLogModal: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('resets the active tab when the message log modal closes', () => {
|
||||
useStore.getState().setCurrentLogModalActiveTab('TRACE')
|
||||
useStore.getState().setShowMessageLogModal(true)
|
||||
|
||||
useStore.getState().setShowMessageLogModal(false)
|
||||
|
||||
expect(useStore.getState().showMessageLogModal).toBe(false)
|
||||
expect(useStore.getState().currentLogModalActiveTab).toBe('DETAIL')
|
||||
})
|
||||
})
|
||||
@@ -98,6 +98,7 @@ const createDeletedAgentTool = (providerId: string): AgentTool => ({
|
||||
|
||||
const createContextValue = (): ComponentProps<typeof ConfigContext.Provider>['value'] => ({
|
||||
appId: 'app-1',
|
||||
onOpenFeatures: vi.fn(),
|
||||
isTrailFinished: false,
|
||||
mode: AppModeEnum.CHAT,
|
||||
modelModeType: ModelModeType.chat,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import Log from '@/app/components/base/chat/chat/log'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app'
|
||||
@@ -21,14 +23,6 @@ const mockState = vi.hoisted(() => ({
|
||||
mockToastUpdate: vi.fn(),
|
||||
mockToastPromise: vi.fn(),
|
||||
mockText2speechDefaultModel: null as unknown,
|
||||
mockStoreState: {
|
||||
currentLogItem: null as unknown,
|
||||
setCurrentLogItem: vi.fn(),
|
||||
showPromptLogModal: false,
|
||||
setShowPromptLogModal: vi.fn(),
|
||||
showAgentLogModal: false,
|
||||
setShowAgentLogModal: vi.fn(),
|
||||
},
|
||||
mockFeaturesState: {
|
||||
moreLikeThis: { enabled: false },
|
||||
moderation: { enabled: false },
|
||||
@@ -121,18 +115,45 @@ vi.mock('@/app/components/app/configuration/prompt-value-panel', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (
|
||||
selector: (state: {
|
||||
currentLogItem: unknown
|
||||
setCurrentLogItem: () => void
|
||||
showPromptLogModal: boolean
|
||||
setShowPromptLogModal: () => void
|
||||
showAgentLogModal: boolean
|
||||
setShowAgentLogModal: () => void
|
||||
}) => unknown,
|
||||
) => selector(mockState.mockStoreState),
|
||||
}))
|
||||
const promptLogItem: IChatItem = {
|
||||
id: 'prompt-1',
|
||||
content: 'First answer',
|
||||
isAnswer: true,
|
||||
log: [{ role: 'user', text: 'First prompt' }],
|
||||
}
|
||||
const secondPromptLogItem: IChatItem = {
|
||||
id: 'prompt-2',
|
||||
content: 'Second answer',
|
||||
isAnswer: true,
|
||||
log: [{ role: 'user', text: 'Second prompt' }],
|
||||
}
|
||||
const agentLogItem: IChatItem = {
|
||||
id: 'agent-1',
|
||||
content: 'Agent answer',
|
||||
isAnswer: true,
|
||||
conversationId: 'conversation-1',
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought-1',
|
||||
thought: 'Agent thought',
|
||||
tool: '',
|
||||
tool_input: '',
|
||||
message_id: 'agent-1',
|
||||
conversation_id: 'conversation-1',
|
||||
observation: '',
|
||||
position: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
const LogActions = ({ onOpenLog }: { onOpenLog: (item: IChatItem) => void }) => (
|
||||
<>
|
||||
{[promptLogItem, secondPromptLogItem, agentLogItem].map((item) => (
|
||||
<section key={item.id} aria-label={item.content}>
|
||||
<Log logItem={item} onOpenLog={onOpenLog} />
|
||||
</section>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
vi.mock('@/app/components/app/text-generate/item', () => ({
|
||||
default: ({
|
||||
@@ -140,10 +161,12 @@ vi.mock('@/app/components/app/text-generate/item', () => ({
|
||||
isLoading,
|
||||
isShowTextToSpeech,
|
||||
messageId,
|
||||
onOpenLog,
|
||||
}: {
|
||||
content: string
|
||||
isLoading: boolean
|
||||
isShowTextToSpeech: boolean
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
messageId: string | null
|
||||
}) => (
|
||||
<div
|
||||
@@ -153,13 +176,15 @@ vi.mock('@/app/components/app/text-generate/item', () => ({
|
||||
data-message-id={messageId || ''}
|
||||
>
|
||||
{content}
|
||||
<LogActions onOpenLog={onOpenLog} />
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/agent-log-modal', () => ({
|
||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||
<div data-testid="agent-log-modal">
|
||||
default: ({ onCancel, currentLogItem }: { onCancel: () => void; currentLogItem: IChatItem }) => (
|
||||
<div data-testid="agent-log-modal" role="dialog" aria-label="Agent log">
|
||||
{currentLogItem.content}
|
||||
<button type="button" data-testid="agent-log-cancel" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -191,8 +216,9 @@ vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/prompt-log-modal', () => ({
|
||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||
<div data-testid="prompt-log-modal">
|
||||
default: ({ onCancel, currentLogItem }: { onCancel: () => void; currentLogItem: IChatItem }) => (
|
||||
<div data-testid="prompt-log-modal" role="dialog" aria-label="Prompt log">
|
||||
{currentLogItem.log?.map((item) => item.text).join(' ')}
|
||||
<button type="button" data-testid="prompt-log-cancel" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -250,8 +276,10 @@ vi.mock('../debug-with-multiple-model', () => ({
|
||||
default: ({
|
||||
checkCanSend,
|
||||
onDebugWithMultipleModelChange,
|
||||
onOpenLog,
|
||||
}: {
|
||||
checkCanSend: () => boolean
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
onDebugWithMultipleModelChange: (item: {
|
||||
id: string
|
||||
model: string
|
||||
@@ -260,6 +288,7 @@ vi.mock('../debug-with-multiple-model', () => ({
|
||||
}) => void
|
||||
}) => (
|
||||
<div data-testid="debug-with-multiple-model">
|
||||
<LogActions onOpenLog={onOpenLog} />
|
||||
<button type="button" data-testid="multiple-check-can-send" onClick={() => checkCanSend()}>
|
||||
Check
|
||||
</button>
|
||||
@@ -284,9 +313,13 @@ vi.mock('../debug-with-multiple-model', () => ({
|
||||
vi.mock('../debug-with-single-model', () => {
|
||||
function DebugWithSingleModelMock({
|
||||
checkCanSend,
|
||||
onOpenLog,
|
||||
chatContainerRef,
|
||||
ref,
|
||||
}: {
|
||||
checkCanSend: () => boolean
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
chatContainerRef: React.Ref<HTMLDivElement>
|
||||
ref?: React.Ref<{ handleRestart: () => void }>
|
||||
}) {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
@@ -294,7 +327,8 @@ vi.mock('../debug-with-single-model', () => {
|
||||
}))
|
||||
|
||||
return (
|
||||
<div data-testid="debug-with-single-model">
|
||||
<div ref={chatContainerRef} data-testid="debug-with-single-model">
|
||||
<LogActions onOpenLog={onOpenLog} />
|
||||
<button type="button" data-testid="single-check-can-send" onClick={() => checkCanSend()}>
|
||||
Check
|
||||
</button>
|
||||
@@ -309,6 +343,7 @@ const createContextValue = (overrides: Partial<DebugContextValue> = {}): DebugCo
|
||||
readonly: false,
|
||||
canTestAndRun: true,
|
||||
appId: 'app-id',
|
||||
onOpenFeatures: vi.fn(),
|
||||
isTrailFinished: false,
|
||||
mode: AppModeEnum.CHAT,
|
||||
modelModeType: ModelModeType.chat,
|
||||
@@ -460,14 +495,14 @@ const renderDebug = (
|
||||
...options.props,
|
||||
}
|
||||
|
||||
render(
|
||||
const view = render(
|
||||
// oxlint-disable-next-line eslint-react/no-context-provider -- use-context-selector contexts are not React 19 context components.
|
||||
<ConfigContext.Provider value={createContextValue(options.contextValue)}>
|
||||
<Debug {...props} />
|
||||
</ConfigContext.Provider>,
|
||||
)
|
||||
|
||||
return { onSetting, notify: mockState.mockToastCall, props }
|
||||
return { ...view, onSetting, notify: mockState.mockToastCall, props }
|
||||
}
|
||||
|
||||
describe('Debug', () => {
|
||||
@@ -478,14 +513,6 @@ describe('Debug', () => {
|
||||
mockState.mockSetFeatures.mockReset()
|
||||
mockState.mockEventEmitterEmit.mockReset()
|
||||
mockState.mockText2speechDefaultModel = null
|
||||
mockState.mockStoreState = {
|
||||
currentLogItem: null,
|
||||
setCurrentLogItem: vi.fn(),
|
||||
showPromptLogModal: false,
|
||||
setShowPromptLogModal: vi.fn(),
|
||||
showAgentLogModal: false,
|
||||
setShowAgentLogModal: vi.fn(),
|
||||
}
|
||||
mockState.mockFeaturesState = {
|
||||
moreLikeThis: { enabled: false },
|
||||
moderation: { enabled: false },
|
||||
@@ -966,42 +993,22 @@ describe('Debug', () => {
|
||||
expect(screen.getByText('appDebug.noResult'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render prompt log modal in completion mode when store flag is enabled', () => {
|
||||
mockState.mockStoreState = {
|
||||
...mockState.mockStoreState,
|
||||
showPromptLogModal: true,
|
||||
}
|
||||
|
||||
renderDebug({
|
||||
contextValue: {
|
||||
mode: AppModeEnum.COMPLETION,
|
||||
},
|
||||
it('opens and closes the completion result log from its action', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockState.mockSendCompletionMessage.mockImplementation((_appId, _data, handlers) => {
|
||||
handlers.onData('Completion answer', true, { messageId: 'completion-1' })
|
||||
handlers.onCompleted()
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('prompt-log-modal'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close prompt log modal in completion mode', () => {
|
||||
const setCurrentLogItem = vi.fn()
|
||||
const setShowPromptLogModal = vi.fn()
|
||||
|
||||
mockState.mockStoreState = {
|
||||
...mockState.mockStoreState,
|
||||
currentLogItem: { id: 'log-1' },
|
||||
setCurrentLogItem,
|
||||
showPromptLogModal: true,
|
||||
setShowPromptLogModal,
|
||||
}
|
||||
|
||||
renderDebug({
|
||||
contextValue: {
|
||||
mode: AppModeEnum.COMPLETION,
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('prompt-log-cancel'))
|
||||
expect(setCurrentLogItem).toHaveBeenCalledTimes(1)
|
||||
expect(setShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
renderDebug({ contextValue: { mode: AppModeEnum.COMPLETION } })
|
||||
await user.click(screen.getByRole('button', { name: 'Send' }))
|
||||
await user.click(
|
||||
within(screen.getByRole('region', { name: 'First answer' })).getByRole('button', {
|
||||
name: 'common.operation.log',
|
||||
}),
|
||||
)
|
||||
expect(screen.getByRole('dialog', { name: 'Prompt log' })).toHaveTextContent('First prompt')
|
||||
await user.click(screen.getByTestId('prompt-log-cancel'))
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1177,57 +1184,55 @@ describe('Debug', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should render prompt and agent log modals in multiple-model mode', () => {
|
||||
mockState.mockStoreState = {
|
||||
...mockState.mockStoreState,
|
||||
showPromptLogModal: true,
|
||||
showAgentLogModal: true,
|
||||
}
|
||||
|
||||
renderDebug({
|
||||
props: {
|
||||
debugWithMultipleModel: true,
|
||||
multipleModelConfigs: [
|
||||
{ id: '1', model: 'vision-model', provider: 'openai', parameters: {} },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('prompt-log-modal'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-log-modal'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close prompt and agent log modals in multiple-model mode', () => {
|
||||
const setCurrentLogItem = vi.fn()
|
||||
const setShowPromptLogModal = vi.fn()
|
||||
const setShowAgentLogModal = vi.fn()
|
||||
|
||||
mockState.mockStoreState = {
|
||||
...mockState.mockStoreState,
|
||||
currentLogItem: { id: 'log-1' },
|
||||
setCurrentLogItem,
|
||||
showPromptLogModal: true,
|
||||
setShowPromptLogModal,
|
||||
showAgentLogModal: true,
|
||||
setShowAgentLogModal,
|
||||
}
|
||||
|
||||
renderDebug({
|
||||
props: {
|
||||
debugWithMultipleModel: true,
|
||||
multipleModelConfigs: [
|
||||
{ id: '1', model: 'vision-model', provider: 'openai', parameters: {} },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('prompt-log-cancel'))
|
||||
fireEvent.click(screen.getByTestId('agent-log-cancel'))
|
||||
|
||||
expect(setCurrentLogItem).toHaveBeenCalledTimes(2)
|
||||
expect(setShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
expect(setShowAgentLogModal).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps log selections within each mounted debug session', async () => {
|
||||
const user = userEvent.setup()
|
||||
const first = renderDebug()
|
||||
const second = renderDebug()
|
||||
await user.click(
|
||||
within(second.container).getAllByRole('button', { name: 'common.operation.log' })[0]!,
|
||||
)
|
||||
expect(within(second.container).getByRole('dialog', { name: 'Prompt log' })).toHaveTextContent(
|
||||
'First prompt',
|
||||
)
|
||||
expect(within(first.container).queryByRole('dialog')).not.toBeInTheDocument()
|
||||
first.unmount()
|
||||
expect(within(second.container).getByRole('dialog', { name: 'Prompt log' })).toHaveTextContent(
|
||||
'First prompt',
|
||||
)
|
||||
second.unmount()
|
||||
renderDebug()
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'selects one log, switches item and type, and reopens after close (multiple=%s)',
|
||||
async (debugWithMultipleModel) => {
|
||||
const user = userEvent.setup()
|
||||
renderDebug({ props: { debugWithMultipleModel } })
|
||||
const openLog = (answer: string) =>
|
||||
user.click(
|
||||
within(screen.getByRole('region', { name: answer })).getByRole('button', {
|
||||
name: 'common.operation.log',
|
||||
}),
|
||||
)
|
||||
await openLog('First answer')
|
||||
expect(screen.getAllByRole('dialog')).toHaveLength(1)
|
||||
expect(screen.getByRole('dialog', { name: 'Prompt log' })).toHaveTextContent('First prompt')
|
||||
await openLog('Second answer')
|
||||
expect(screen.getAllByRole('dialog')).toHaveLength(1)
|
||||
expect(screen.getByRole('dialog', { name: 'Prompt log' })).toHaveTextContent('Second prompt')
|
||||
await openLog('Agent answer')
|
||||
expect(screen.getAllByRole('dialog')).toHaveLength(1)
|
||||
expect(screen.getByRole('dialog', { name: 'Agent log' })).toHaveTextContent('Agent answer')
|
||||
expect(screen.queryByRole('dialog', { name: 'Prompt log' })).not.toBeInTheDocument()
|
||||
await user.click(screen.getByTestId('agent-log-cancel'))
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
await openLog('First answer')
|
||||
expect(screen.getByRole('dialog', { name: 'Prompt log' })).toHaveTextContent('First prompt')
|
||||
await user.click(screen.getByTestId('prompt-log-cancel'))
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { Ref } from 'react'
|
||||
import type { ChatItem } from '@/app/components/base/chat/types'
|
||||
import { page } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { FeaturesProvider } from '@/app/components/base/features/context'
|
||||
import ConfigContext, { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import Debug from '../index'
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@tanstack/react-query')>()),
|
||||
useQuery: () => ({ data: [] }),
|
||||
}))
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useDefaultModel: () => ({ data: undefined }),
|
||||
}))
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({}),
|
||||
}))
|
||||
vi.mock('@/app/components/app/configuration/debug/chat-user-input', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/app/configuration/prompt-value-panel', () => ({ default: () => null }))
|
||||
vi.mock('../debug-with-multiple-model', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/base/agent-log-modal', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/app/text-generate/item', () => ({ default: () => null }))
|
||||
vi.mock('@/service/debug', () => ({ sendCompletionMessage: vi.fn() }))
|
||||
vi.mock('@/app/components/app/configuration/toast', () => ({ toast: { error: vi.fn() } }))
|
||||
|
||||
// Replace model execution, keeping the production Chat scroll container and log action path.
|
||||
vi.mock('../debug-with-single-model', () => ({
|
||||
default: ({
|
||||
onOpenLog,
|
||||
chatContainerRef,
|
||||
}: {
|
||||
onOpenLog: (item: ChatItem) => void
|
||||
chatContainerRef: Ref<HTMLDivElement>
|
||||
}) => (
|
||||
<Chat
|
||||
chatList={[
|
||||
{
|
||||
id: 'answer',
|
||||
isAnswer: true,
|
||||
content: 'Answer',
|
||||
log: [{ role: 'user', text: 'Measured prompt' }],
|
||||
},
|
||||
]}
|
||||
onOpenLog={onOpenLog}
|
||||
chatContainerRef={chatContainerRef}
|
||||
chatContainerClassName="debug-log-scrollbar"
|
||||
noChatInput
|
||||
/>
|
||||
),
|
||||
}))
|
||||
vi.mock('@/app/components/base/chat/chat/answer', async () => {
|
||||
const { default: Operation } = await import('@/app/components/base/chat/chat/answer/operation')
|
||||
return {
|
||||
default: ({ item }: { item: ChatItem }) => (
|
||||
<div className="group">
|
||||
<div style={{ height: 900 }}>{item.content}</div>
|
||||
<Operation
|
||||
item={item}
|
||||
question=""
|
||||
index={0}
|
||||
maxSize={500}
|
||||
contentWidth={100}
|
||||
hasWorkflowProcess={false}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
})
|
||||
vi.mock('@/app/components/base/chat/chat/question', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/base/chat/chat/chat-input-area', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/base/new-audio-button', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/app/annotation/edit-annotation-modal', () => ({ default: () => null }))
|
||||
vi.mock(
|
||||
'@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button',
|
||||
() => ({ default: () => null }),
|
||||
)
|
||||
|
||||
function DebugSession({ width, doubleGutter = false }: { width: number; doubleGutter?: boolean }) {
|
||||
const defaults = useDebugConfigurationContext()
|
||||
return (
|
||||
// oxlint-disable-next-line eslint-react/no-context-provider -- use-context-selector exposes a Provider, not a React 19 provider component.
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
...defaults,
|
||||
canTestAndRun: true,
|
||||
modelConfig: {
|
||||
...defaults.modelConfig,
|
||||
configs: { prompt_template: '', prompt_variables: [] },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FeaturesProvider>
|
||||
<style>{`.debug-log-scrollbar { scrollbar-gutter: stable; } .debug-log-scrollbar::-webkit-scrollbar { width: 16px; } .debug-double-gutter .debug-log-scrollbar { scrollbar-gutter: stable both-edges; }`}</style>
|
||||
<section
|
||||
aria-label="Debug pane"
|
||||
className={doubleGutter ? 'debug-double-gutter' : undefined}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 64,
|
||||
right: 8,
|
||||
bottom: 16,
|
||||
width,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Debug
|
||||
isPreview
|
||||
onSetting={vi.fn()}
|
||||
inputs={{}}
|
||||
modelParameterParams={{ setModel: vi.fn(), onCompletionParamsChange: vi.fn() }}
|
||||
debugWithMultipleModel={false}
|
||||
multipleModelConfigs={[]}
|
||||
onMultipleModelConfigsChange={vi.fn()}
|
||||
/>
|
||||
</section>
|
||||
</FeaturesProvider>
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
it('keeps the log beside the actual scrolling chat after resizing its pane', async () => {
|
||||
await page.viewport(1440, 900)
|
||||
const view = await render(<DebugSession width={540} />)
|
||||
const chat = page.getByTestId('chat-container').element()
|
||||
expect(chat.scrollHeight).toBeGreaterThan(chat.clientHeight)
|
||||
expect(chat.clientWidth).toBeLessThan(chat.getBoundingClientRect().width)
|
||||
|
||||
await page.getByText('Answer', { exact: true }).hover()
|
||||
await page.getByRole('button', { name: 'common.operation.log' }).click()
|
||||
await expect.element(page.getByText('Measured prompt')).toBeVisible()
|
||||
const title = page.getByText('PROMPT LOG').element()
|
||||
const initialTitleLeft = title.getBoundingClientRect().left
|
||||
const initialChatWidth = chat.clientWidth
|
||||
expect(title.getBoundingClientRect().right).toBeLessThan(chat.getBoundingClientRect().left)
|
||||
|
||||
await view.rerender(<DebugSession width={540} doubleGutter />)
|
||||
await expect.poll(() => chat.clientWidth).toBeLessThan(initialChatWidth)
|
||||
await expect
|
||||
.poll(() => title.getBoundingClientRect().left - initialTitleLeft)
|
||||
.toBe(initialChatWidth - chat.clientWidth)
|
||||
|
||||
await view.rerender(<DebugSession width={420} doubleGutter />)
|
||||
await expect
|
||||
.poll(() => title.getBoundingClientRect().left - initialTitleLeft)
|
||||
.toBe(initialChatWidth - chat.clientWidth)
|
||||
expect(title.getBoundingClientRect().right).toBeLessThan(chat.getBoundingClientRect().left)
|
||||
await expect.element(page.getByText('Measured prompt')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'common.operation.close' }).click()
|
||||
await expect.element(page.getByText('Measured prompt')).not.toBeInTheDocument()
|
||||
await page.getByText('Answer', { exact: true }).hover()
|
||||
await page.getByRole('button', { name: 'common.operation.log' }).click()
|
||||
await expect.element(page.getByText('Measured prompt')).toBeVisible()
|
||||
})
|
||||
+25
@@ -1,11 +1,15 @@
|
||||
import type { ModelAndParameter } from '../../types'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatConfig, ChatItem as ChatItemType, OnSend } from '@/app/components/base/chat/types'
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Log from '@/app/components/base/chat/chat/log'
|
||||
import { TransferMethod } from '@/app/components/base/chat/types'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { renderWithAccountProfile as render } from '@/test/console/account-profile'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART } from '../../types'
|
||||
import ChatItem from '../chat-item'
|
||||
import { DebugWithMultipleModelContextProvider } from '../context-provider'
|
||||
|
||||
const mockConsoleStateReader = vi.fn()
|
||||
const mockUseDebugConfigurationContext = vi.fn()
|
||||
@@ -23,6 +27,7 @@ const { mockToastError } = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
let capturedChatProps: {
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
config: ChatConfig
|
||||
chatList: ChatItemType[]
|
||||
isResponding: boolean
|
||||
@@ -87,6 +92,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
capturedChatProps = props
|
||||
return (
|
||||
<div data-testid="chat-component">
|
||||
{props?.chatList[0] && <Log logItem={props.chatList[0]} onOpenLog={props.onOpenLog} />}
|
||||
<span data-testid="chat-list-length">{props?.chatList?.length || 0}</span>
|
||||
<span data-testid="is-responding">{props?.isResponding ? 'yes' : 'no'}</span>
|
||||
<button
|
||||
@@ -209,6 +215,25 @@ describe('ChatItem', () => {
|
||||
createDefaultMocks()
|
||||
})
|
||||
|
||||
it('opens this model result through the owning debug session', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenLog = vi.fn()
|
||||
render(
|
||||
<DebugWithMultipleModelContextProvider
|
||||
multipleModelConfigs={[]}
|
||||
onMultipleModelConfigsChange={vi.fn()}
|
||||
onDebugWithMultipleModelChange={vi.fn()}
|
||||
onOpenLog={onOpenLog}
|
||||
>
|
||||
<ChatItem modelAndParameter={createModelAndParameter()} />
|
||||
</DebugWithMultipleModelContextProvider>,
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.log' }))
|
||||
expect(onOpenLog).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ id: 'msg-1', isAnswer: true }),
|
||||
)
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render Chat component when chatList is not empty', () => {
|
||||
renderComponent()
|
||||
|
||||
+4
-2
@@ -7,7 +7,6 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { Inputs, ModelConfig } from '@/models/debug'
|
||||
import type { PromptVariable } from '@/types/app'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
DEFAULT_AGENT_SETTING,
|
||||
DEFAULT_CHAT_PROMPT_CONFIG,
|
||||
@@ -161,6 +160,7 @@ const createModelConfig = (promptVariables: PromptVariableWithMeta[] = []): Mode
|
||||
})
|
||||
|
||||
type DebugConfiguration = {
|
||||
onOpenFeatures: () => void
|
||||
mode: AppModeEnum
|
||||
inputs: Inputs
|
||||
modelConfig: ModelConfig
|
||||
@@ -171,6 +171,7 @@ type DebugConfiguration = {
|
||||
const createDebugConfiguration = (
|
||||
overrides: Partial<DebugConfiguration> = {},
|
||||
): DebugConfiguration => ({
|
||||
onOpenFeatures: vi.fn(),
|
||||
mode: AppModeEnum.CHAT,
|
||||
inputs: {},
|
||||
modelConfig: createModelConfig(),
|
||||
@@ -193,6 +194,7 @@ const createProps = (
|
||||
overrides: Partial<DebugWithMultipleModelContextType> = {},
|
||||
): DebugWithMultipleModelContextType => ({
|
||||
multipleModelConfigs: [createModelAndParameter()],
|
||||
onOpenLog: vi.fn(),
|
||||
onMultipleModelConfigsChange: vi.fn(),
|
||||
onDebugWithMultipleModelChange: vi.fn(),
|
||||
...overrides,
|
||||
@@ -489,7 +491,7 @@ describe('DebugWithMultipleModel', () => {
|
||||
expect(capturedChatInputProps?.showFileUpload).toBe(false)
|
||||
expect(capturedChatInputProps?.speechToTextConfig).toEqual(featureState.features.speech2text)
|
||||
expect(capturedChatInputProps?.visionConfig).toEqual(featureState.features.file)
|
||||
expect(useAppStore.getState().showAppConfigureFeaturesModal).toBe(true)
|
||||
expect(debugConfiguration.onOpenFeatures).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should allow sending but disable feature configuration when configuration is readonly and test/run is allowed', () => {
|
||||
|
||||
+30
@@ -1,7 +1,11 @@
|
||||
import type { ModelAndParameter } from '../../types'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Log from '@/app/components/base/chat/chat/log'
|
||||
import { TransferMethod } from '@/app/components/base/chat/types'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../../types'
|
||||
import { DebugWithMultipleModelContextProvider } from '../context-provider'
|
||||
import TextGenerationItem from '../text-generation-item'
|
||||
|
||||
const mockUseDebugConfigurationContext = vi.fn()
|
||||
@@ -15,6 +19,7 @@ const { mockToastError } = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
let capturedTextGenerationProps: {
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
content: string
|
||||
isLoading: boolean
|
||||
isResponding: boolean
|
||||
@@ -63,6 +68,12 @@ vi.mock('@/app/components/app/text-generate/item', () => ({
|
||||
capturedTextGenerationProps = props
|
||||
return (
|
||||
<div data-testid="text-generation">
|
||||
{props && (
|
||||
<Log
|
||||
logItem={{ id: props.messageId!, isAnswer: true, content: props.content }}
|
||||
onOpenLog={props.onOpenLog}
|
||||
/>
|
||||
)}
|
||||
<span data-testid="content">{props?.content}</span>
|
||||
<span data-testid="is-loading">{props?.isLoading ? 'yes' : 'no'}</span>
|
||||
<span data-testid="is-responding">{props?.isResponding ? 'yes' : 'no'}</span>
|
||||
@@ -169,6 +180,25 @@ describe('TextGenerationItem', () => {
|
||||
createDefaultMocks()
|
||||
})
|
||||
|
||||
it('opens this model result through the owning debug session', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenLog = vi.fn()
|
||||
render(
|
||||
<DebugWithMultipleModelContextProvider
|
||||
multipleModelConfigs={[]}
|
||||
onMultipleModelConfigsChange={vi.fn()}
|
||||
onDebugWithMultipleModelChange={vi.fn()}
|
||||
onOpenLog={onOpenLog}
|
||||
>
|
||||
<TextGenerationItem modelAndParameter={createModelAndParameter()} />
|
||||
</DebugWithMultipleModelContextProvider>,
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.log' }))
|
||||
expect(onOpenLog).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ id: 'msg-123', isAnswer: true }),
|
||||
)
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render TextGeneration component', () => {
|
||||
renderComponent()
|
||||
|
||||
@@ -26,11 +26,13 @@ import {
|
||||
import { matchesProviderReference } from '@/utils/provider-reference'
|
||||
import { useConfigFromDebugContext, useFormattingChangedSubscription } from '../hooks'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART } from '../types'
|
||||
import { useDebugWithMultipleModelContext } from './context'
|
||||
|
||||
type ChatItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
}
|
||||
const ChatItem: FC<ChatItemProps> = ({ modelAndParameter }) => {
|
||||
const { onOpenLog } = useDebugWithMultipleModelContext()
|
||||
const { data: userProfile } = useSuspenseQuery({
|
||||
...userProfileQueryOptions(),
|
||||
select: (data) => data.profile,
|
||||
@@ -164,10 +166,9 @@ const ChatItem: FC<ChatItemProps> = ({ modelAndParameter }) => {
|
||||
chatFooterClassName="p-4 pb-0"
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={doSend}
|
||||
showPromptLog
|
||||
onOpenLog={onOpenLog}
|
||||
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="xl" />}
|
||||
allToolIcons={allToolIcons}
|
||||
hideLogModal
|
||||
noSpacing
|
||||
/>
|
||||
)
|
||||
|
||||
+2
@@ -13,6 +13,7 @@ export const DebugWithMultipleModelContextProvider = ({
|
||||
multipleModelConfigs,
|
||||
onDebugWithMultipleModelChange,
|
||||
checkCanSend,
|
||||
onOpenLog,
|
||||
}: DebugWithMultipleModelContextProviderProps) => {
|
||||
return (
|
||||
<DebugWithMultipleModelContext.Provider
|
||||
@@ -21,6 +22,7 @@ export const DebugWithMultipleModelContextProvider = ({
|
||||
multipleModelConfigs,
|
||||
onDebugWithMultipleModelChange,
|
||||
checkCanSend,
|
||||
onOpenLog,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
|
||||
@@ -8,11 +9,13 @@ export type DebugWithMultipleModelContextType = {
|
||||
multipleModelConfigs: ModelAndParameter[]
|
||||
onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void
|
||||
onDebugWithMultipleModelChange: (singleModelConfig: ModelAndParameter) => void
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
checkCanSend?: () => boolean
|
||||
}
|
||||
|
||||
export const DebugWithMultipleModelContext = createContext<DebugWithMultipleModelContextType>({
|
||||
multipleModelConfigs: [],
|
||||
onOpenLog: noop,
|
||||
onMultipleModelConfigsChange: noop,
|
||||
onDebugWithMultipleModelChange: noop,
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { DebugWithMultipleModelContextType } from './context'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import ChatInputArea from '@/app/components/base/chat/chat/chat-input-area'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
@@ -22,6 +21,7 @@ const DebugWithMultipleModel = () => {
|
||||
appId,
|
||||
readonly,
|
||||
canTestAndRun = false,
|
||||
onOpenFeatures,
|
||||
} = useDebugConfigurationContext()
|
||||
const speech2text = useFeatures((s) => s.features.speech2text)
|
||||
const file = useFeatures((s) => s.features.file)
|
||||
@@ -94,7 +94,6 @@ const DebugWithMultipleModel = () => {
|
||||
[twoLine, threeLine, fourLine],
|
||||
)
|
||||
|
||||
const setShowAppConfigureFeaturesModal = useAppStore((s) => s.setShowAppConfigureFeaturesModal)
|
||||
const inputsForm = modelConfig.configs.prompt_variables
|
||||
.filter((item) => item.type !== 'api')
|
||||
.map((item) => ({
|
||||
@@ -133,7 +132,7 @@ const DebugWithMultipleModel = () => {
|
||||
showFeatureBar
|
||||
featureBarReadonly={readonly}
|
||||
showFileUpload={false}
|
||||
onFeatureBarClick={setShowAppConfigureFeaturesModal}
|
||||
onFeatureBarClick={onOpenFeatures}
|
||||
onSend={handleSend}
|
||||
speechToTextConfig={speech2text as any}
|
||||
speechToTextTarget={{ type: 'consoleApp', appId }}
|
||||
@@ -154,6 +153,7 @@ const DebugWithMultipleModelWrapper: FC<DebugWithMultipleModelContextType> = ({
|
||||
multipleModelConfigs,
|
||||
onDebugWithMultipleModelChange,
|
||||
checkCanSend,
|
||||
onOpenLog,
|
||||
}) => {
|
||||
return (
|
||||
<DebugWithMultipleModelContextProvider
|
||||
@@ -161,6 +161,7 @@ const DebugWithMultipleModelWrapper: FC<DebugWithMultipleModelContextType> = ({
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onDebugWithMultipleModelChange={onDebugWithMultipleModelChange}
|
||||
checkCanSend={checkCanSend}
|
||||
onOpenLog={onOpenLog}
|
||||
>
|
||||
<DebugWithMultipleModelMemoed />
|
||||
</DebugWithMultipleModelContextProvider>
|
||||
|
||||
+3
@@ -18,11 +18,13 @@ import { consoleQuery } from '@/service/console'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
|
||||
import { useDebugWithMultipleModelContext } from './context'
|
||||
|
||||
type TextGenerationItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
}
|
||||
const TextGenerationItem: FC<TextGenerationItemProps> = ({ modelAndParameter }) => {
|
||||
const { onOpenLog } = useDebugWithMultipleModelContext()
|
||||
const {
|
||||
isAdvancedMode,
|
||||
modelConfig,
|
||||
@@ -141,6 +143,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({ modelAndParameter })
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={noop}
|
||||
onOpenLog={onOpenLog}
|
||||
inSidePanel
|
||||
/>
|
||||
)
|
||||
|
||||
+64
-21
@@ -6,7 +6,6 @@ import type { Collection } from '@/app/components/tools/types'
|
||||
import type { DatasetConfigs, ModelConfig } from '@/models/debug'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { createRef } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
@@ -134,6 +133,7 @@ const mockDebugConfigContext = {
|
||||
readonly: false,
|
||||
canTestAndRun: true,
|
||||
appId: 'test-app-id',
|
||||
onOpenFeatures: vi.fn(),
|
||||
isTrailFinished: false,
|
||||
mode: AppModeEnum.CHAT,
|
||||
modelModeType: ModelModeType.chat,
|
||||
@@ -550,6 +550,8 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
const logProps = { onOpenLog: vi.fn(), chatContainerRef: createRef<HTMLDivElement>() }
|
||||
|
||||
describe('DebugWithSingleModel', () => {
|
||||
let ref: RefObject<DebugWithSingleModelRefType | null>
|
||||
|
||||
@@ -581,6 +583,7 @@ describe('DebugWithSingleModel', () => {
|
||||
|
||||
render(
|
||||
<DebugWithSingleModel
|
||||
{...logProps}
|
||||
ref={ref as RefObject<DebugWithSingleModelRefType>}
|
||||
checkCanSend={checkCanSend}
|
||||
/>,
|
||||
@@ -597,6 +600,7 @@ describe('DebugWithSingleModel', () => {
|
||||
|
||||
render(
|
||||
<DebugWithSingleModel
|
||||
{...logProps}
|
||||
ref={ref as RefObject<DebugWithSingleModelRefType>}
|
||||
checkCanSend={checkCanSend}
|
||||
/>,
|
||||
@@ -629,6 +633,7 @@ describe('DebugWithSingleModel', () => {
|
||||
|
||||
render(
|
||||
<DebugWithSingleModel
|
||||
{...logProps}
|
||||
ref={ref as RefObject<DebugWithSingleModelRefType>}
|
||||
checkCanSend={checkCanSend}
|
||||
/>,
|
||||
@@ -648,11 +653,13 @@ describe('DebugWithSingleModel', () => {
|
||||
// User Interactions
|
||||
describe('User Interactions', () => {
|
||||
it('should open feature configuration when feature bar is clicked', () => {
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('feature-bar-button'))
|
||||
|
||||
expect(useAppStore.getState().showAppConfigureFeaturesModal).toBe(true)
|
||||
expect(mockDebugConfigContext.onOpenFeatures).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should allow sending but disable feature configuration when configuration is readonly and test/run is allowed', async () => {
|
||||
@@ -662,7 +669,9 @@ describe('DebugWithSingleModel', () => {
|
||||
canTestAndRun: true,
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-input')).not.toHaveAttribute('readonly')
|
||||
expect(screen.getByTestId('feature-bar-button')).toBeDisabled()
|
||||
@@ -684,7 +693,9 @@ describe('DebugWithSingleModel', () => {
|
||||
canTestAndRun: false,
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-input')).toHaveAttribute('readonly')
|
||||
expect(screen.getByTestId('chat-input')).toBeDisabled()
|
||||
@@ -704,7 +715,9 @@ describe('DebugWithSingleModel', () => {
|
||||
opening: { enabled: true, opening_statement: 'Hello!', suggested_questions: ['Q1'] },
|
||||
}
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-button'))
|
||||
|
||||
@@ -727,7 +740,9 @@ describe('DebugWithSingleModel', () => {
|
||||
},
|
||||
}
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-button'))
|
||||
|
||||
@@ -741,13 +756,17 @@ describe('DebugWithSingleModel', () => {
|
||||
})
|
||||
|
||||
it('should handle model without vision support', () => {
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle missing model in provider list', () => {
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -770,7 +789,9 @@ describe('DebugWithSingleModel', () => {
|
||||
}),
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
// Component should render successfully with filtered variables
|
||||
// Component should render successfully with filtered variables
|
||||
@@ -788,7 +809,9 @@ describe('DebugWithSingleModel', () => {
|
||||
}),
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -797,7 +820,9 @@ describe('DebugWithSingleModel', () => {
|
||||
// Tool Icons Tests
|
||||
describe('Tool Icons', () => {
|
||||
it('should map tool icons from collection list', () => {
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -815,7 +840,9 @@ describe('DebugWithSingleModel', () => {
|
||||
}),
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -844,7 +871,9 @@ describe('DebugWithSingleModel', () => {
|
||||
collectionList: [],
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -858,7 +887,9 @@ describe('DebugWithSingleModel', () => {
|
||||
inputs: {} as any,
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -874,7 +905,9 @@ describe('DebugWithSingleModel', () => {
|
||||
},
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -885,7 +918,9 @@ describe('DebugWithSingleModel', () => {
|
||||
completionParams: {} as any,
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
|
||||
})
|
||||
@@ -894,7 +929,9 @@ describe('DebugWithSingleModel', () => {
|
||||
// Imperative Handle Tests
|
||||
describe('Imperative Handle', () => {
|
||||
it('should expose handleRestart method via ref', () => {
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
expect(ref.current).not.toBeNull()
|
||||
expect(ref.current?.handleRestart).toBeDefined()
|
||||
@@ -902,7 +939,9 @@ describe('DebugWithSingleModel', () => {
|
||||
})
|
||||
|
||||
it('should call handleRestart when invoked via ref', () => {
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
act(() => {
|
||||
ref.current?.handleRestart()
|
||||
@@ -925,7 +964,9 @@ describe('DebugWithSingleModel', () => {
|
||||
file: { enabled: true },
|
||||
}
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-with-document'))
|
||||
|
||||
@@ -957,7 +998,9 @@ describe('DebugWithSingleModel', () => {
|
||||
file: { enabled: true },
|
||||
}
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
render(
|
||||
<DebugWithSingleModel {...logProps} ref={ref as RefObject<DebugWithSingleModelRefType>} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-with-files'))
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { Ref } from 'react'
|
||||
import type { IChatItem, InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { Avatar } from '@langgenius/dify-ui/avatar'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { memo, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||
import { toast } from '@/app/components/app/configuration/toast'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
||||
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
|
||||
@@ -22,6 +22,8 @@ import { useConfigFromDebugContext, useFormattingChangedSubscription } from '../
|
||||
|
||||
type DebugWithSingleModelProps = {
|
||||
checkCanSend?: () => boolean
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
chatContainerRef: Ref<HTMLDivElement>
|
||||
}
|
||||
export type DebugWithSingleModelRefType = {
|
||||
handleRestart: () => void
|
||||
@@ -29,6 +31,8 @@ export type DebugWithSingleModelRefType = {
|
||||
const DebugWithSingleModel = ({
|
||||
ref,
|
||||
checkCanSend,
|
||||
onOpenLog,
|
||||
chatContainerRef,
|
||||
}: DebugWithSingleModelProps & {
|
||||
ref: React.RefObject<DebugWithSingleModelRefType>
|
||||
}) => {
|
||||
@@ -39,6 +43,7 @@ const DebugWithSingleModel = ({
|
||||
const {
|
||||
readonly,
|
||||
canTestAndRun = false,
|
||||
onOpenFeatures,
|
||||
modelConfig,
|
||||
appId,
|
||||
inputs,
|
||||
@@ -173,8 +178,6 @@ const DebugWithSingleModel = ({
|
||||
}
|
||||
}, [handleRestart])
|
||||
|
||||
const setShowAppConfigureFeaturesModal = useAppStore((s) => s.setShowAppConfigureFeaturesModal)
|
||||
|
||||
return (
|
||||
<Chat
|
||||
readonly={debugInputReadonly}
|
||||
@@ -187,7 +190,7 @@ const DebugWithSingleModel = ({
|
||||
showFeatureBar
|
||||
featureBarReadonly={readonly}
|
||||
showFileUpload={false}
|
||||
onFeatureBarClick={setShowAppConfigureFeaturesModal}
|
||||
onFeatureBarClick={onOpenFeatures}
|
||||
inputDisabled={!canTestAndRun}
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={doSend}
|
||||
@@ -196,7 +199,8 @@ const DebugWithSingleModel = ({
|
||||
onRegenerate={doRegenerate}
|
||||
switchSibling={(siblingMessageId) => setTargetMessageId(siblingMessageId)}
|
||||
onStopResponding={handleStop}
|
||||
showPromptLog
|
||||
onOpenLog={onOpenLog}
|
||||
chatContainerRef={chatContainerRef}
|
||||
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="xl" />}
|
||||
allToolIcons={allToolIcons}
|
||||
onAnnotationEdited={canManageAnnotation ? handleAnnotationEdited : undefined}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AppModelConfigPayload } from '@dify/contracts/api/console/apps/typ
|
||||
import type { FC } from 'react'
|
||||
import type { DebugWithSingleModelRefType } from './debug-with-single-model'
|
||||
import type { ModelAndParameter } from './types'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import type { Inputs } from '@/models/debug'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
@@ -25,11 +26,9 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import ChatUserInput from '@/app/components/app/configuration/debug/chat-user-input'
|
||||
import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import { toast } from '@/app/components/app/configuration/toast'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import AgentLogModal from '@/app/components/base/agent-log-modal'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
@@ -407,33 +406,40 @@ const Debug: FC<IDebug> = ({
|
||||
handleVisionConfigInMultipleModel()
|
||||
}, [multipleModelConfigs, mode, handleVisionConfigInMultipleModel])
|
||||
|
||||
const {
|
||||
currentLogItem,
|
||||
setCurrentLogItem,
|
||||
showPromptLogModal,
|
||||
setShowPromptLogModal,
|
||||
showAgentLogModal,
|
||||
setShowAgentLogModal,
|
||||
} = useAppStore(
|
||||
useShallow((state) => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
showPromptLogModal: state.showPromptLogModal,
|
||||
setShowPromptLogModal: state.setShowPromptLogModal,
|
||||
showAgentLogModal: state.showAgentLogModal,
|
||||
setShowAgentLogModal: state.setShowAgentLogModal,
|
||||
})),
|
||||
)
|
||||
const [width, setWidth] = useState(0)
|
||||
const [selectedLogItem, setSelectedLogItem] = useState<IChatItem>()
|
||||
const [logModalWidth, setLogModalWidth] = useState(0)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const singleChatContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const adjustModalWidth = () => {
|
||||
if (ref.current) setWidth(document.body.clientWidth - (ref.current?.clientWidth + 16) - 8)
|
||||
const handleOpenLog = (item: IChatItem) => {
|
||||
const container =
|
||||
debugWithMultipleModel || mode === AppModeEnum.COMPLETION
|
||||
? ref.current
|
||||
: singleChatContainerRef.current
|
||||
if (container) setLogModalWidth(document.body.clientWidth - (container.clientWidth + 16) - 8)
|
||||
setSelectedLogItem(item)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
adjustModalWidth()
|
||||
}, [])
|
||||
if (!selectedLogItem) return
|
||||
const container =
|
||||
debugWithMultipleModel || mode === AppModeEnum.COMPLETION
|
||||
? ref.current
|
||||
: singleChatContainerRef.current
|
||||
if (!container) return
|
||||
|
||||
const adjustModalWidth = () => {
|
||||
setLogModalWidth(document.body.clientWidth - (container.clientWidth + 16) - 8)
|
||||
}
|
||||
const observer = new ResizeObserver(adjustModalWidth)
|
||||
observer.observe(container)
|
||||
window.addEventListener('resize', adjustModalWidth)
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
window.removeEventListener('resize', adjustModalWidth)
|
||||
}
|
||||
}, [debugWithMultipleModel, mode, selectedLogItem])
|
||||
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
|
||||
@@ -545,27 +551,8 @@ const Debug: FC<IDebug> = ({
|
||||
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
|
||||
onDebugWithMultipleModelChange={handleChangeToSingleModel}
|
||||
checkCanSend={checkCanSend}
|
||||
onOpenLog={handleOpenLog}
|
||||
/>
|
||||
{showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowPromptLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showAgentLogModal && (
|
||||
<AgentLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowAgentLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!debugWithMultipleModel && (
|
||||
@@ -597,7 +584,12 @@ const Debug: FC<IDebug> = ({
|
||||
{/* Chat */}
|
||||
{mode !== AppModeEnum.COMPLETION && (
|
||||
<div className="h-0 grow overflow-hidden">
|
||||
<DebugWithSingleModel ref={debugWithSingleModelRef} checkCanSend={checkCanSend} />
|
||||
<DebugWithSingleModel
|
||||
ref={debugWithSingleModelRef}
|
||||
checkCanSend={checkCanSend}
|
||||
onOpenLog={handleOpenLog}
|
||||
chatContainerRef={singleChatContainerRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Text Generation */}
|
||||
@@ -620,6 +612,7 @@ const Debug: FC<IDebug> = ({
|
||||
isError={false}
|
||||
onRetry={noop}
|
||||
siteInfo={null}
|
||||
onOpenLog={handleOpenLog}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -637,21 +630,25 @@ const Debug: FC<IDebug> = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{mode === AppModeEnum.COMPLETION && showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowPromptLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isShowCannotQueryDataset && (
|
||||
<CannotQueryDataset onConfirm={() => setShowCannotQueryDataset(false)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedLogItem &&
|
||||
(selectedLogItem.agent_thoughts?.length ? (
|
||||
<AgentLogModal
|
||||
width={logModalWidth}
|
||||
currentLogItem={selectedLogItem}
|
||||
onCancel={() => setSelectedLogItem(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<PromptLogModal
|
||||
width={logModalWidth}
|
||||
currentLogItem={selectedLogItem}
|
||||
onCancel={() => setSelectedLogItem(undefined)}
|
||||
/>
|
||||
))}
|
||||
{isShowFormattingChangeConfirm && (
|
||||
<FormattingChanged onConfirm={handleConfirm} onCancel={handleCancel} />
|
||||
)}
|
||||
|
||||
@@ -25,7 +25,6 @@ const renderHook = (callback: () => ReturnType<typeof useConfiguration>) => {
|
||||
}
|
||||
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
const mockSetShowAppConfigureFeaturesModal = vi.fn()
|
||||
const mockHandleMultipleModelConfigsChange = vi.fn()
|
||||
const mockFetchCollectionList = vi.fn()
|
||||
const mockFetchAppDetailDirect = vi.fn()
|
||||
@@ -98,8 +97,6 @@ vi.mock('@/app/components/app/store', () => ({
|
||||
mode: AppModeEnum.CHAT,
|
||||
permission_keys: mockAppPermissionKeys,
|
||||
},
|
||||
showAppConfigureFeaturesModal: false,
|
||||
setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -275,6 +272,18 @@ describe('useConfiguration', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('opens, closes, and reopens feature configuration within the current session', async () => {
|
||||
const { result } = renderHook(() => useConfiguration())
|
||||
await waitFor(() => expect(result.current.showLoading).toBe(false))
|
||||
expect(result.current.showAppConfigureFeaturesModal).toBe(false)
|
||||
act(() => result.current.contextValue.onOpenFeatures())
|
||||
expect(result.current.showAppConfigureFeaturesModal).toBe(true)
|
||||
act(() => result.current.onCloseFeaturePanel())
|
||||
expect(result.current.showAppConfigureFeaturesModal).toBe(false)
|
||||
act(() => result.current.contextValue.onOpenFeatures())
|
||||
expect(result.current.showAppConfigureFeaturesModal).toBe(true)
|
||||
})
|
||||
|
||||
it('should leave loading through the error boundary when the app has no model configuration', async () => {
|
||||
mockFetchAppDetailDirect.mockResolvedValueOnce(createAppDetailFixture({ model_config: null }))
|
||||
const queryClient = createTestQueryClient()
|
||||
@@ -742,7 +751,7 @@ describe('useConfiguration', () => {
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalledWith(true)
|
||||
expect(result.current.showAppConfigureFeaturesModal).toBe(true)
|
||||
expect(mockFormattingChangedDispatcher).toHaveBeenCalled()
|
||||
expect(mockHandleMultipleModelConfigsChange).toHaveBeenCalled()
|
||||
expect(Cookies.get(DETAIL_SIDEBAR_COOKIE_NAME)).toBe('collapse')
|
||||
@@ -855,6 +864,6 @@ describe('useConfiguration', () => {
|
||||
expect(result.current.contextValue.visionConfig.enabled).toBe(true)
|
||||
expect(result.current.contextValue.canReturnToSimpleMode).toBe(false)
|
||||
expect(mockFormattingChangedDispatcher).toHaveBeenCalledTimes(2)
|
||||
expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalledWith(false)
|
||||
expect(result.current.showAppConfigureFeaturesModal).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ type ModelConfiguration = ReturnType<typeof useModelConfigurationState>
|
||||
type ContextBase = Pick<
|
||||
DebugConfigurationValue,
|
||||
| 'appId'
|
||||
| 'onOpenFeatures'
|
||||
| 'canReturnToSimpleMode'
|
||||
| 'canTestAndRun'
|
||||
| 'collectionList'
|
||||
|
||||
+1
-11
@@ -1,7 +1,6 @@
|
||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
||||
import { currentWorkspaceAtom, currentWorkspaceLoadingAtom } from '@/context/workspace-state'
|
||||
@@ -18,14 +17,7 @@ export function useConfigurationAppContext() {
|
||||
select: (data) => data.profile.id,
|
||||
})
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const { appDetail, showAppConfigureFeaturesModal, setShowAppConfigureFeaturesModal } =
|
||||
useAppStore(
|
||||
useShallow((state) => ({
|
||||
appDetail: state.appDetail,
|
||||
showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal,
|
||||
setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal,
|
||||
})),
|
||||
)
|
||||
const appDetail = useAppStore((state) => state.appDetail)
|
||||
const pathname = usePathname()
|
||||
const matched = /\/app\/([^/]+)/.exec(pathname)
|
||||
const appId = matched?.[1] || ''
|
||||
@@ -58,8 +50,6 @@ export function useConfigurationAppContext() {
|
||||
currentWorkspace,
|
||||
isLoadingCurrentWorkspace,
|
||||
serverLatestPublishedAt,
|
||||
setShowAppConfigureFeaturesModal,
|
||||
showAppConfigureFeaturesModal,
|
||||
updateModelConfig,
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,10 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
currentWorkspace,
|
||||
isLoadingCurrentWorkspace,
|
||||
serverLatestPublishedAt,
|
||||
setShowAppConfigureFeaturesModal,
|
||||
showAppConfigureFeaturesModal,
|
||||
updateModelConfig,
|
||||
} = useConfigurationAppContext()
|
||||
const { data: fileUploadConfigResponse } = useFileUploadConfig()
|
||||
const [showAppConfigureFeaturesModal, setShowAppConfigureFeaturesModal] = useState(false)
|
||||
const [formattingChanged, setFormattingChanged] = useState(false)
|
||||
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
|
||||
// oxlint-disable-next-line eslint-react/use-state -- This custom hook returns a state object.
|
||||
@@ -354,6 +353,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
model: modelConfiguration,
|
||||
base: {
|
||||
appId,
|
||||
onOpenFeatures: () => setShowAppConfigureFeaturesModal(true),
|
||||
canReturnToSimpleMode,
|
||||
canTestAndRun: appACLCapabilities.canTestAndRun,
|
||||
collectionList,
|
||||
|
||||
@@ -7,18 +7,7 @@ import ConfigContext from '@/context/debug-configuration'
|
||||
import { AppModeEnum, ModelModeType, Resolution } from '@/types/app'
|
||||
import PromptValuePanel from '../index'
|
||||
|
||||
const mockSetShowAppConfigureFeaturesModal = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (
|
||||
selector: (state: {
|
||||
setShowAppConfigureFeaturesModal: typeof mockSetShowAppConfigureFeaturesModal
|
||||
}) => unknown,
|
||||
) =>
|
||||
selector({
|
||||
setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal,
|
||||
}),
|
||||
}))
|
||||
const mockOnOpenFeatures = vi.fn()
|
||||
|
||||
// Use real store - global zustand mock will auto-reset between tests
|
||||
vi.mock('@/app/components/base/features/new-feature-panel/feature-bar', () => ({
|
||||
@@ -79,6 +68,7 @@ const promptVariables = [
|
||||
] as const
|
||||
|
||||
const baseContextValue: any = {
|
||||
onOpenFeatures: mockOnOpenFeatures,
|
||||
modelModeType: ModelModeType.completion,
|
||||
modelConfig: {
|
||||
configs: {
|
||||
@@ -125,7 +115,7 @@ describe('PromptValuePanel', () => {
|
||||
vi.clearAllMocks()
|
||||
mockSetInputs.mockClear()
|
||||
mockOnSend.mockClear()
|
||||
mockSetShowAppConfigureFeaturesModal.mockClear()
|
||||
mockOnOpenFeatures.mockClear()
|
||||
})
|
||||
|
||||
it('updates inputs, clears values, and triggers run when ready', async () => {
|
||||
@@ -193,7 +183,7 @@ describe('PromptValuePanel', () => {
|
||||
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeDisabled()
|
||||
|
||||
fireEvent.click(screen.getByText('feature bar'))
|
||||
expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalled()
|
||||
expect(mockOnOpenFeatures).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('disables run for advanced completion mode when the completion prompt is empty', () => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import * as React from 'react'
|
||||
import { useEffect, useId, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
@@ -48,6 +47,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
const {
|
||||
readonly,
|
||||
canTestAndRun = false,
|
||||
onOpenFeatures,
|
||||
modelModeType,
|
||||
modelConfig,
|
||||
setInputs,
|
||||
@@ -131,8 +131,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
setInputs(newInputs)
|
||||
}
|
||||
|
||||
const setShowAppConfigureFeaturesModal = useAppStore((s) => s.setShowAppConfigureFeaturesModal)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative z-1 mx-3 rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-md">
|
||||
@@ -323,7 +321,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<FeatureBar
|
||||
showFileUpload={false}
|
||||
isChatMode={appType !== AppModeEnum.COMPLETION}
|
||||
onFeatureBarClick={setShowAppConfigureFeaturesModal}
|
||||
onFeatureBarClick={onOpenFeatures}
|
||||
disabled={readonly}
|
||||
hideEditEntrance={readonly}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
|
||||
import { createAccountProfileQueryClient } from '@/test/console/account-profile'
|
||||
import { QueryClientTestProvider } from '@/test/console/query-provider'
|
||||
import { render } from '@/test/console/render'
|
||||
import { createAppDetailFixture } from '@/test/fixtures/app'
|
||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import ConversationList from '../list'
|
||||
@@ -11,20 +15,11 @@ const mockFetchChatMessages = vi.fn()
|
||||
const mockUpdateLogMessageFeedbacks = vi.fn()
|
||||
const mockUpdateLogMessageAnnotations = vi.fn()
|
||||
const mockOnRefresh = vi.fn()
|
||||
const mockSetCurrentLogItem = vi.fn()
|
||||
const mockSetShowPromptLogModal = vi.fn()
|
||||
const mockSetShowAgentLogModal = vi.fn()
|
||||
const mockSetShowMessageLogModal = vi.fn()
|
||||
const mockCompletionRefetch = vi.fn()
|
||||
const mockDelAnnotation = vi.fn()
|
||||
|
||||
let mockChatConversationDetail: Record<string, unknown> | undefined
|
||||
let mockCompletionConversationDetail: Record<string, unknown> | undefined
|
||||
let mockShowMessageLogModal = false
|
||||
let mockShowPromptLogModal = false
|
||||
let mockShowAgentLogModal = false
|
||||
let mockCurrentLogItem: Record<string, unknown> | undefined
|
||||
let mockCurrentLogModalActiveTab = 'messages'
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
default: () => ({
|
||||
formatTime: (timestamp: number) => `formatted-${timestamp}`,
|
||||
@@ -59,21 +54,6 @@ vi.mock('@/service/annotation', () => ({
|
||||
delAnnotation: (...args: unknown[]) => mockDelAnnotation(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
currentLogItem: mockCurrentLogItem,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
showMessageLogModal: mockShowMessageLogModal,
|
||||
setShowPromptLogModal: mockSetShowPromptLogModal,
|
||||
setShowAgentLogModal: mockSetShowAgentLogModal,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
showPromptLogModal: mockShowPromptLogModal,
|
||||
showAgentLogModal: mockShowAgentLogModal,
|
||||
currentLogModalActiveTab: mockCurrentLogModalActiveTab,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/loading-placeholder', () => ({
|
||||
LoadingPlaceholder: () => <div>loading</div>,
|
||||
}))
|
||||
@@ -97,14 +77,14 @@ vi.mock('@/app/components/base/copy-icon', () => ({
|
||||
vi.mock('@/app/components/app/text-generate/item', () => ({
|
||||
default: ({
|
||||
content,
|
||||
hideLogAction,
|
||||
onOpenLog,
|
||||
onFeedback,
|
||||
}: {
|
||||
content: string
|
||||
hideLogAction?: boolean
|
||||
onOpenLog?: (item: IChatItem) => void
|
||||
onFeedback: (value: { rating: string; content?: string }) => Promise<boolean>
|
||||
}) => (
|
||||
<div data-testid="text-generation" data-hide-log-action={String(hideLogAction)}>
|
||||
<div data-testid="text-generation" data-log-enabled={String(!!onOpenLog)}>
|
||||
<div>{content}</div>
|
||||
<button onClick={() => void onFeedback({ rating: 'like', content: 'great' })}>
|
||||
completion-feedback
|
||||
@@ -121,8 +101,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
switchSibling,
|
||||
hideLogModal,
|
||||
showPromptLog,
|
||||
onOpenLog,
|
||||
}: {
|
||||
chatList: Array<{ id: string }>
|
||||
onFeedback: (mid: string, value: { rating: string; content?: string }) => Promise<boolean>
|
||||
@@ -136,15 +115,78 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
onAnnotationEdited: (query: string, answer: string, index: number) => void
|
||||
onAnnotationRemoved: (index: number) => Promise<boolean>
|
||||
switchSibling: (siblingMessageId: string) => void
|
||||
hideLogModal?: boolean
|
||||
showPromptLog?: boolean
|
||||
onOpenLog?: (item: IChatItem) => void
|
||||
}) => (
|
||||
<div
|
||||
data-testid="chat-panel"
|
||||
data-hide-log-modal={String(hideLogModal)}
|
||||
data-show-prompt-log={String(showPromptLog)}
|
||||
>
|
||||
<div data-testid="chat-panel" data-log-enabled={String(!!onOpenLog)}>
|
||||
<div>{chatList.length}</div>
|
||||
{onOpenLog && (
|
||||
<>
|
||||
<button
|
||||
onClick={() =>
|
||||
onOpenLog({
|
||||
id: 'log-1',
|
||||
content: 'answer',
|
||||
isAnswer: true,
|
||||
workflow_run_id: 'run-1',
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought',
|
||||
tool: '',
|
||||
thought: 'thinking',
|
||||
tool_input: '',
|
||||
message_id: 'log',
|
||||
conversation_id: 'conversation-1',
|
||||
observation: '',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
log: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
open-workflow-log
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onOpenLog({
|
||||
id: 'log-2',
|
||||
content: 'answer',
|
||||
isAnswer: true,
|
||||
agent_thoughts: [
|
||||
{
|
||||
id: 'thought',
|
||||
tool: '',
|
||||
thought: 'thinking',
|
||||
tool_input: '',
|
||||
message_id: 'log',
|
||||
conversation_id: 'conversation-1',
|
||||
observation: '',
|
||||
position: 0,
|
||||
},
|
||||
],
|
||||
log: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
open-agent-log
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onOpenLog({
|
||||
id: 'log-3',
|
||||
content: 'answer',
|
||||
isAnswer: true,
|
||||
log: [{ role: 'user', text: 'prompt' }],
|
||||
})
|
||||
}
|
||||
>
|
||||
open-prompt-log
|
||||
</button>
|
||||
<button onClick={() => onOpenLog({ id: 'log-4', content: 'answer', isAnswer: true })}>
|
||||
open-no-log
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={() => void onFeedback('message-1', { rating: 'like', content: 'nice' })}>
|
||||
chat-feedback
|
||||
</button>
|
||||
@@ -276,11 +318,6 @@ describe('ConversationList', () => {
|
||||
vi.clearAllMocks()
|
||||
mockChatConversationDetail = undefined
|
||||
mockCompletionConversationDetail = undefined
|
||||
mockShowMessageLogModal = false
|
||||
mockShowPromptLogModal = false
|
||||
mockShowAgentLogModal = false
|
||||
mockCurrentLogItem = undefined
|
||||
mockCurrentLogModalActiveTab = 'messages'
|
||||
mockDelAnnotation.mockResolvedValue(undefined)
|
||||
mockFetchChatMessages.mockResolvedValue({
|
||||
data: [],
|
||||
@@ -352,7 +389,7 @@ describe('ConversationList', () => {
|
||||
},
|
||||
)
|
||||
|
||||
it('should close the drawer, refresh, and clear modal flags', async () => {
|
||||
it('should close the drawer and refresh', async () => {
|
||||
mockChatConversationDetail = {
|
||||
id: 'conversation-1',
|
||||
created_at: 1710000000,
|
||||
@@ -378,9 +415,6 @@ describe('ConversationList', () => {
|
||||
fireEvent.click(await screen.findByRole('button', { name: /(?:^|\.)operation\.close(?=$|:)/ }))
|
||||
|
||||
expect(mockOnRefresh).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
expect(mockSetShowAgentLogModal).toHaveBeenCalledWith(false)
|
||||
expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(false)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUrlUpdate).toHaveBeenCalled()
|
||||
@@ -432,8 +466,6 @@ describe('ConversationList', () => {
|
||||
],
|
||||
has_more: false,
|
||||
})
|
||||
mockShowMessageLogModal = true
|
||||
mockCurrentLogItem = { id: 'log-1' }
|
||||
|
||||
renderConversationList({
|
||||
searchParams: '?page=2&conversation_id=conversation-1',
|
||||
@@ -455,8 +487,6 @@ describe('ConversationList', () => {
|
||||
|
||||
expect(screen.getByTestId('var-panel')).toHaveTextContent('query:Latest question')
|
||||
expect(screen.getByTestId('model-info')).toHaveTextContent('gpt-4o')
|
||||
expect(screen.getByTestId('chat-panel')).toHaveAttribute('data-hide-log-modal', 'true')
|
||||
expect(screen.getByTestId('message-log-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('chat-feedback'))
|
||||
|
||||
@@ -501,65 +531,7 @@ describe('ConversationList', () => {
|
||||
searchParams: '?conversation_id=conversation-1',
|
||||
})
|
||||
|
||||
expect(await screen.findByTestId('chat-panel')).toHaveAttribute(
|
||||
'data-show-prompt-log',
|
||||
expected,
|
||||
)
|
||||
})
|
||||
|
||||
it('should mount agent log modals from the detail panel instead of the nested chat layout', async () => {
|
||||
mockChatConversationDetail = {
|
||||
id: 'conversation-1',
|
||||
created_at: 1710000000,
|
||||
model_config: {
|
||||
model: 'gpt-4o',
|
||||
configs: {
|
||||
introduction: 'Hello there',
|
||||
},
|
||||
user_input_form: [],
|
||||
},
|
||||
message: {
|
||||
inputs: {},
|
||||
},
|
||||
}
|
||||
mockShowAgentLogModal = true
|
||||
mockCurrentLogItem = {
|
||||
id: 'message-1',
|
||||
conversationId: 'conversation-1',
|
||||
}
|
||||
mockFetchChatMessages.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'message-1',
|
||||
answer: 'Assistant reply',
|
||||
query: 'Latest question',
|
||||
created_at: 1710000000,
|
||||
inputs: {},
|
||||
feedbacks: [],
|
||||
message: [],
|
||||
message_files: [],
|
||||
agent_thoughts: [{ id: 'thought-1' }],
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
})
|
||||
|
||||
renderConversationList({
|
||||
searchParams: '?page=2&conversation_id=conversation-1',
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('chat-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('chat-panel')).toHaveAttribute('data-hide-log-modal', 'true')
|
||||
expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-log-modal')).toHaveAttribute('data-floating', 'true')
|
||||
|
||||
fireEvent.click(screen.getByText('close-agent-log-modal'))
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalled()
|
||||
expect(mockSetShowAgentLogModal).toHaveBeenCalledWith(false)
|
||||
expect(await screen.findByTestId('chat-panel')).toHaveAttribute('data-log-enabled', expected)
|
||||
})
|
||||
|
||||
it('should render completion details and refetch after feedback updates', async () => {
|
||||
@@ -586,8 +558,6 @@ describe('ConversationList', () => {
|
||||
message_files: [{ url: 'https://example.com/file.txt' }],
|
||||
},
|
||||
}
|
||||
mockShowPromptLogModal = true
|
||||
mockCurrentLogItem = { id: 'log-2', log: [{ role: 'user', text: 'Prompt body' }] }
|
||||
|
||||
renderConversationList({
|
||||
appDetail: { id: 'app-1', mode: AppModeEnum.COMPLETION } as any,
|
||||
@@ -600,8 +570,8 @@ describe('ConversationList', () => {
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('var-panel')).toHaveTextContent('query:Question')
|
||||
expect(screen.getByTestId('text-generation')).toHaveAttribute('data-hide-log-action', 'true')
|
||||
expect(screen.getByTestId('prompt-log-modal')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('text-generation')).toHaveAttribute('data-log-enabled', 'false')
|
||||
expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('completion-feedback'))
|
||||
|
||||
@@ -683,7 +653,7 @@ describe('ConversationList', () => {
|
||||
expect(screen.getAllByText('1').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should support annotation changes, modal closing, and paginated scroll loading in the detail drawer', async () => {
|
||||
it('should support annotation changes and paginated scroll loading in the detail drawer', async () => {
|
||||
mockChatConversationDetail = {
|
||||
id: 'conversation-1',
|
||||
created_at: 1710000000,
|
||||
@@ -706,8 +676,6 @@ describe('ConversationList', () => {
|
||||
},
|
||||
},
|
||||
}
|
||||
mockShowMessageLogModal = true
|
||||
mockCurrentLogItem = { id: 'log-1' }
|
||||
mockFetchChatMessages
|
||||
.mockResolvedValueOnce({
|
||||
data: [
|
||||
@@ -747,10 +715,6 @@ describe('ConversationList', () => {
|
||||
expect(mockDelAnnotation).toHaveBeenCalledWith('app-1', 'annotation-2')
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('close-message-log-modal'))
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalled()
|
||||
expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(false)
|
||||
|
||||
const scrollableDiv = document.getElementById('scrollableDiv') as HTMLDivElement
|
||||
Object.defineProperty(scrollableDiv, 'clientHeight', { configurable: true, value: 100 })
|
||||
Object.defineProperty(scrollableDiv, 'scrollHeight', { configurable: true, value: 500 })
|
||||
@@ -765,44 +729,82 @@ describe('ConversationList', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should close the prompt log modal from completion detail drawers', async () => {
|
||||
mockCompletionConversationDetail = {
|
||||
it.each(['workflow', 'agent', 'prompt'])(
|
||||
'should open and close the selected %s log within the conversation',
|
||||
async (kind) => {
|
||||
mockChatConversationDetail = {
|
||||
id: 'conversation-1',
|
||||
model_config: { user_input_form: [] },
|
||||
message: { inputs: {} },
|
||||
}
|
||||
renderConversationList({
|
||||
appDetail: { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT },
|
||||
searchParams: '?conversation_id=conversation-1',
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
await user.click(await screen.findByRole('button', { name: `open-${kind}-log` }))
|
||||
const modalKind = kind === 'workflow' ? 'message' : kind
|
||||
expect(screen.getByTestId(`${modalKind}-log-modal`)).toBeInTheDocument()
|
||||
for (const other of ['message', 'agent', 'prompt'].filter((value) => value !== modalKind))
|
||||
expect(screen.queryByTestId(`${other}-log-modal`)).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: `close-${modalKind}-log-modal` }))
|
||||
expect(screen.queryByTestId(`${modalKind}-log-modal`)).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'open-no-log' }))
|
||||
expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
|
||||
},
|
||||
)
|
||||
|
||||
it('should discard the selected log when the conversation drawer closes and reopens', async () => {
|
||||
mockChatConversationDetail = {
|
||||
id: 'conversation-1',
|
||||
created_at: 1710000000,
|
||||
model_config: {
|
||||
model: 'gpt-4o-mini',
|
||||
user_input_form: [
|
||||
{
|
||||
query: {
|
||||
variable: 'query',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
message: {
|
||||
id: 'message-1',
|
||||
answer: 'Generated output',
|
||||
inputs: {
|
||||
query: 'Question',
|
||||
},
|
||||
feedbacks: [],
|
||||
message_files: [{ url: 'https://example.com/file.txt' }],
|
||||
},
|
||||
model_config: { user_input_form: [] },
|
||||
message: { inputs: {} },
|
||||
}
|
||||
mockShowPromptLogModal = true
|
||||
mockCurrentLogItem = { id: 'log-2', log: [{ role: 'user', text: 'Prompt body' }] }
|
||||
|
||||
renderConversationList({
|
||||
appDetail: { id: 'app-1', mode: AppModeEnum.COMPLETION } as any,
|
||||
logs: createCompletionLogs() as any,
|
||||
searchParams: '?page=2&conversation_id=conversation-1',
|
||||
appDetail: { id: 'app-1', mode: AppModeEnum.ADVANCED_CHAT },
|
||||
searchParams: '?conversation_id=conversation-1',
|
||||
})
|
||||
|
||||
expect(await screen.findByTestId('prompt-log-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('close-prompt-log-modal'))
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalled()
|
||||
expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
const user = userEvent.setup()
|
||||
await user.click(await screen.findByRole('button', { name: 'open-workflow-log' }))
|
||||
expect(screen.getByTestId('message-log-modal')).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: /(?:^|\.)operation\.close(?=$|:)/ }))
|
||||
await waitFor(() => expect(screen.queryByTestId('message-log-modal')).not.toBeInTheDocument())
|
||||
await user.click(screen.getByText('hello world'))
|
||||
await screen.findByTestId('chat-panel')
|
||||
expect(screen.queryByTestId('message-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
it('should clear a selected log when URL navigation changes or closes the conversation', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = createAccountProfileQueryClient({ timezone: 'Asia/Shanghai' })
|
||||
const appDetail = createAppDetailFixture({ mode: AppModeEnum.ADVANCED_CHAT })
|
||||
const view = (searchParams: string) => (
|
||||
<NuqsTestingAdapter searchParams={searchParams} hasMemory>
|
||||
<QueryClientTestProvider queryClient={queryClient}>
|
||||
<ConversationList
|
||||
appDetail={appDetail}
|
||||
logs={{ data: [], has_more: false, limit: 20, page: 1, total: 0 }}
|
||||
onRefresh={mockOnRefresh}
|
||||
/>
|
||||
</QueryClientTestProvider>
|
||||
</NuqsTestingAdapter>
|
||||
)
|
||||
mockChatConversationDetail = {
|
||||
id: 'conversation-1',
|
||||
model_config: { user_input_form: [] },
|
||||
message: { inputs: {} },
|
||||
}
|
||||
const { rerender } = render(view('?conversation_id=conversation-1'))
|
||||
await user.click(await screen.findByRole('button', { name: 'open-workflow-log' }))
|
||||
expect(screen.getByTestId('message-log-modal')).toBeInTheDocument()
|
||||
mockChatConversationDetail = { ...mockChatConversationDetail, id: 'conversation-2' }
|
||||
rerender(view('?conversation_id=conversation-2'))
|
||||
await waitFor(() => expect(screen.queryByTestId('message-log-modal')).not.toBeInTheDocument())
|
||||
await user.click(await screen.findByRole('button', { name: 'open-agent-log' }))
|
||||
expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument()
|
||||
rerender(view(''))
|
||||
await waitFor(() => expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument())
|
||||
rerender(view('?conversation_id=conversation-2'))
|
||||
await screen.findByTestId('chat-panel')
|
||||
expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -42,9 +42,7 @@ import { parseAsString, useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import ModelInfo from '@/app/components/app/log/model-info'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import AgentLogModal from '@/app/components/base/agent-log-modal'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
@@ -81,7 +79,6 @@ import {
|
||||
} from './list-utils'
|
||||
import VarPanel from './var-panel'
|
||||
|
||||
type AppStoreState = ReturnType<typeof useAppStore.getState>
|
||||
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
|
||||
type ConversationSelection = ConversationListItem | { id: string; isPlaceholder?: true }
|
||||
|
||||
@@ -174,29 +171,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
select: (data) => data.profile.timezone ?? undefined,
|
||||
})
|
||||
const { formatTime } = useTimestamp()
|
||||
const {
|
||||
currentLogItem,
|
||||
setCurrentLogItem,
|
||||
showMessageLogModal,
|
||||
setShowMessageLogModal,
|
||||
showPromptLogModal,
|
||||
setShowPromptLogModal,
|
||||
showAgentLogModal,
|
||||
setShowAgentLogModal,
|
||||
currentLogModalActiveTab,
|
||||
} = useAppStore(
|
||||
useShallow((state: AppStoreState) => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
showPromptLogModal: state.showPromptLogModal,
|
||||
setShowPromptLogModal: state.setShowPromptLogModal,
|
||||
showAgentLogModal: state.showAgentLogModal,
|
||||
setShowAgentLogModal: state.setShowAgentLogModal,
|
||||
currentLogModalActiveTab: state.currentLogModalActiveTab,
|
||||
})),
|
||||
)
|
||||
const [selectedLogItem, setSelectedLogItem] = useState<IChatItem>()
|
||||
const { t } = useTranslation(['appLog', 'common'])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [varValues, setVarValues] = useState<Record<string, string>>({})
|
||||
@@ -427,7 +402,13 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
|
||||
const isChatMode = appDetail.mode !== AppModeEnum.COMPLETION
|
||||
const isAdvanced = appDetail.mode === AppModeEnum.ADVANCED_CHAT
|
||||
const shouldShowPromptLogModal = showPromptLogModal && !!currentLogItem?.log
|
||||
const logKind = selectedLogItem?.workflow_run_id
|
||||
? 'workflow'
|
||||
: selectedLogItem?.agent_thoughts?.length
|
||||
? 'agent'
|
||||
: selectedLogItem?.log
|
||||
? 'prompt'
|
||||
: undefined
|
||||
|
||||
const varList = getDetailVarList(detail, varValues)
|
||||
const message_files = getCompletionMessageFiles(detail, isChatMode)
|
||||
@@ -518,7 +499,6 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
onRetry={noop}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
hideLogAction
|
||||
onFeedback={(feedback) => onFeedback(detail.message.id, feedback)}
|
||||
isShowTextToSpeech
|
||||
siteInfo={null}
|
||||
@@ -547,9 +527,8 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
noChatInput
|
||||
showPromptLog={isAdvanced}
|
||||
onOpenLog={isAdvanced ? setSelectedLogItem : undefined}
|
||||
hideProcessDetail
|
||||
hideLogModal
|
||||
chatContainerInnerClassName="px-3"
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
@@ -589,9 +568,8 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
noChatInput
|
||||
showPromptLog={isAdvanced}
|
||||
onOpenLog={isAdvanced ? setSelectedLogItem : undefined}
|
||||
hideProcessDetail
|
||||
hideLogModal
|
||||
chatContainerInnerClassName="px-3"
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
@@ -607,38 +585,28 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
{logKind === 'workflow' && (
|
||||
<WorkflowContextProvider>
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
defaultTab={currentLogModalActiveTab}
|
||||
currentLogItem={selectedLogItem}
|
||||
onCancel={() => setSelectedLogItem(undefined)}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
)}
|
||||
{showAgentLogModal && (
|
||||
{logKind === 'agent' && (
|
||||
<AgentLogModal
|
||||
floating
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowAgentLogModal(false)
|
||||
}}
|
||||
currentLogItem={selectedLogItem}
|
||||
onCancel={() => setSelectedLogItem(undefined)}
|
||||
/>
|
||||
)}
|
||||
{shouldShowPromptLogModal && (
|
||||
{logKind === 'prompt' && (
|
||||
<PromptLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowPromptLogModal(false)
|
||||
}}
|
||||
currentLogItem={selectedLogItem}
|
||||
onCancel={() => setSelectedLogItem(undefined)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -707,6 +675,7 @@ const CompletionConversationDetailComp: FC<ConversationDetailProps> = ({
|
||||
|
||||
return (
|
||||
<DetailPanel
|
||||
key={`${appDetail.id}:${conversationId}`}
|
||||
appDetail={appDetail}
|
||||
detail={conversationDetail}
|
||||
onClose={onClose}
|
||||
@@ -768,6 +737,7 @@ const ChatConversationDetailComp: FC<ConversationDetailProps> = ({
|
||||
|
||||
return (
|
||||
<DetailPanel
|
||||
key={`${appDetail.id}:${conversationId}`}
|
||||
appDetail={appDetail}
|
||||
detail={conversationDetail}
|
||||
onClose={onClose}
|
||||
@@ -800,14 +770,6 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
const pendingConversationCacheRef = useRef<ConversationSelection | undefined>(undefined)
|
||||
const isChatMode = appDetail.mode !== AppModeEnum.COMPLETION // Whether the app is a chat app
|
||||
const isChatflow = appDetail.mode === AppModeEnum.ADVANCED_CHAT // Whether the app is a chatflow app
|
||||
const { setShowPromptLogModal, setShowAgentLogModal, setShowMessageLogModal } = useAppStore(
|
||||
useShallow((state: AppStoreState) => ({
|
||||
setShowPromptLogModal: state.setShowPromptLogModal,
|
||||
setShowAgentLogModal: state.setShowAgentLogModal,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
})),
|
||||
)
|
||||
|
||||
const activeConversationId =
|
||||
conversationIdInUrl ?? pendingConversationIdRef.current ?? currentConversation?.id
|
||||
|
||||
@@ -874,22 +836,12 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
onRefresh()
|
||||
setShowDrawer(false)
|
||||
setCurrentConversation(undefined)
|
||||
setShowPromptLogModal(false)
|
||||
setShowAgentLogModal(false)
|
||||
setShowMessageLogModal(false)
|
||||
pendingConversationIdRef.current = null
|
||||
pendingConversationCacheRef.current = undefined
|
||||
closingConversationIdRef.current = conversationIdInUrl ?? null
|
||||
|
||||
if (conversationIdInUrl) void setConversationIdInUrl(null, { history: 'replace' })
|
||||
}, [
|
||||
conversationIdInUrl,
|
||||
onRefresh,
|
||||
setConversationIdInUrl,
|
||||
setShowAgentLogModal,
|
||||
setShowMessageLogModal,
|
||||
setShowPromptLogModal,
|
||||
])
|
||||
}, [conversationIdInUrl, onRefresh, setConversationIdInUrl])
|
||||
|
||||
// Annotated data needs to be highlighted
|
||||
const renderTdValue = (
|
||||
|
||||
@@ -1,52 +1,15 @@
|
||||
import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { create } from 'zustand'
|
||||
|
||||
type State = {
|
||||
appDetail?: AppDetailWithSite
|
||||
currentLogItem?: IChatItem
|
||||
currentLogModalActiveTab: string
|
||||
showPromptLogModal: boolean
|
||||
showAgentLogModal: boolean
|
||||
showMessageLogModal: boolean
|
||||
showAppConfigureFeaturesModal: boolean
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setAppDetail: (appDetail?: AppDetailWithSite) => void
|
||||
setCurrentLogItem: (item?: IChatItem) => void
|
||||
setCurrentLogModalActiveTab: (tab: string) => void
|
||||
setShowPromptLogModal: (showPromptLogModal: boolean) => void
|
||||
setShowAgentLogModal: (showAgentLogModal: boolean) => void
|
||||
setShowMessageLogModal: (showMessageLogModal: boolean) => void
|
||||
setShowAppConfigureFeaturesModal: (showAppConfigureFeaturesModal: boolean) => void
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>((set) => ({
|
||||
appDetail: undefined,
|
||||
setAppDetail: (appDetail) => set(() => ({ appDetail })),
|
||||
currentLogItem: undefined,
|
||||
currentLogModalActiveTab: 'DETAIL',
|
||||
setCurrentLogItem: (currentLogItem) => set(() => ({ currentLogItem })),
|
||||
setCurrentLogModalActiveTab: (currentLogModalActiveTab) =>
|
||||
set(() => ({ currentLogModalActiveTab })),
|
||||
showPromptLogModal: false,
|
||||
setShowPromptLogModal: (showPromptLogModal) => set(() => ({ showPromptLogModal })),
|
||||
showAgentLogModal: false,
|
||||
setShowAgentLogModal: (showAgentLogModal) => set(() => ({ showAgentLogModal })),
|
||||
showMessageLogModal: false,
|
||||
setShowMessageLogModal: (showMessageLogModal) =>
|
||||
set(() => {
|
||||
if (showMessageLogModal) {
|
||||
return { showMessageLogModal }
|
||||
} else {
|
||||
return {
|
||||
showMessageLogModal,
|
||||
currentLogModalActiveTab: 'DETAIL',
|
||||
}
|
||||
}
|
||||
}),
|
||||
showAppConfigureFeaturesModal: false,
|
||||
setShowAppConfigureFeaturesModal: (showAppConfigureFeaturesModal) =>
|
||||
set(() => ({ showAppConfigureFeaturesModal })),
|
||||
}))
|
||||
@@ -7,7 +7,7 @@ const mockCopy = vi.fn()
|
||||
const mockSuccess = vi.fn()
|
||||
const mockOnFeedback = vi.fn()
|
||||
const mockOnMoreLikeThis = vi.fn()
|
||||
const mockOnOpenLogModal = vi.fn()
|
||||
const mockOnOpenLog = vi.fn()
|
||||
const mockOnRetry = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
@@ -37,7 +37,7 @@ describe('GenerationActionGroups', () => {
|
||||
messageId="msg-1"
|
||||
onFeedback={mockOnFeedback}
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
onSave={mockOnSave}
|
||||
supportFeedback
|
||||
@@ -66,7 +66,7 @@ describe('GenerationActionGroups', () => {
|
||||
moreLikeThis
|
||||
onFeedback={mockOnFeedback}
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
onSave={mockOnSave}
|
||||
supportFeedback
|
||||
@@ -96,7 +96,7 @@ describe('GenerationActionGroups', () => {
|
||||
messageId="msg-1"
|
||||
moreLikeThis
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
/>,
|
||||
)
|
||||
@@ -106,19 +106,17 @@ describe('GenerationActionGroups', () => {
|
||||
).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should hide the log action when requested by the owning surface', () => {
|
||||
it('should hide the log action when the owner does not provide it', () => {
|
||||
render(
|
||||
<GenerationActionGroups
|
||||
appSourceType={AppSourceType.webApp}
|
||||
content="hello world"
|
||||
currentTab="DETAIL"
|
||||
depth={1}
|
||||
hideLogAction
|
||||
isError={false}
|
||||
isInWebApp={false}
|
||||
messageId="msg-1"
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onRetry={mockOnRetry}
|
||||
/>,
|
||||
)
|
||||
@@ -139,7 +137,7 @@ describe('GenerationActionGroups', () => {
|
||||
isInWebApp
|
||||
messageId="msg-2"
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
/>,
|
||||
)
|
||||
@@ -162,7 +160,7 @@ describe('GenerationActionGroups', () => {
|
||||
messageId="msg-3"
|
||||
onFeedback={mockOnFeedback}
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
/>,
|
||||
)
|
||||
@@ -189,7 +187,7 @@ describe('GenerationActionGroups', () => {
|
||||
messageId="msg-4"
|
||||
onFeedback={mockOnFeedback}
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
/>,
|
||||
)
|
||||
@@ -213,7 +211,7 @@ describe('GenerationActionGroups', () => {
|
||||
messageId="msg-4"
|
||||
onFeedback={mockOnFeedback}
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
/>,
|
||||
)
|
||||
@@ -237,7 +235,7 @@ describe('GenerationActionGroups', () => {
|
||||
messageId="msg-4"
|
||||
onFeedback={mockOnFeedback}
|
||||
onMoreLikeThis={mockOnMoreLikeThis}
|
||||
onOpenLogModal={mockOnOpenLogModal}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={mockOnRetry}
|
||||
/>,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import GenerationItem from '../index'
|
||||
|
||||
const mockFetchMoreLikeThis = vi.fn()
|
||||
const mockFetchTextGenerationMessage = vi.fn()
|
||||
const mockUpdateFeedback = vi.fn()
|
||||
const mockSetCurrentLogItem = vi.fn()
|
||||
const mockSetShowPromptLogModal = vi.fn()
|
||||
const mockOnOpenLog = vi.fn()
|
||||
const mockSubmitHumanInputForm = vi.fn()
|
||||
const mockSubmitHumanInputFormWorkflow = vi.fn()
|
||||
const mockToastWarning = vi.fn()
|
||||
@@ -34,14 +34,6 @@ vi.mock('@/service/debug', () => ({
|
||||
fetchTextGenerationMessage: (...args: unknown[]) => mockFetchTextGenerationMessage(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowPromptLogModal: mockSetShowPromptLogModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat/context', () => ({
|
||||
useChatContext: () => ({
|
||||
config: {
|
||||
@@ -146,7 +138,7 @@ describe('GenerationItem', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should open the prompt log modal with normalized log data', async () => {
|
||||
it('should send normalized log data to the owning surface', async () => {
|
||||
mockFetchTextGenerationMessage.mockResolvedValue({
|
||||
answer: 'assistant answer',
|
||||
message: [{ role: 'user', text: 'hello' }],
|
||||
@@ -160,6 +152,7 @@ describe('GenerationItem', () => {
|
||||
isError={false}
|
||||
messageId="msg-1"
|
||||
onRetry={vi.fn()}
|
||||
onOpenLog={mockOnOpenLog}
|
||||
siteInfo={null}
|
||||
/>,
|
||||
)
|
||||
@@ -172,7 +165,7 @@ describe('GenerationItem', () => {
|
||||
appId: 'app-1',
|
||||
messageId: 'msg-1',
|
||||
})
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledWith(
|
||||
expect(mockOnOpenLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
log: [
|
||||
{ role: 'user', text: 'hello' },
|
||||
@@ -184,7 +177,64 @@ describe('GenerationItem', () => {
|
||||
],
|
||||
}),
|
||||
)
|
||||
expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('keeps the log action absent without an owning surface', () => {
|
||||
render(
|
||||
<GenerationItem
|
||||
appSourceType={AppSourceType.webApp}
|
||||
content="Answer"
|
||||
isError={false}
|
||||
messageId="msg-1"
|
||||
onRetry={vi.fn()}
|
||||
siteInfo={null}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /operation\.log/ })).not.toBeInTheDocument()
|
||||
expect(mockFetchTextGenerationMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens a more-like-this child log through the same owner using the child message', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockFetchMoreLikeThis.mockResolvedValue({ answer: 'Child answer', id: 'child-message' })
|
||||
mockFetchTextGenerationMessage.mockResolvedValue({
|
||||
id: 'child-message',
|
||||
answer: 'Child answer',
|
||||
message: [{ role: 'user', text: 'Child prompt' }],
|
||||
})
|
||||
render(
|
||||
<GenerationItem
|
||||
appSourceType={AppSourceType.webApp}
|
||||
content="Parent answer"
|
||||
isError={false}
|
||||
messageId="parent-message"
|
||||
moreLikeThis
|
||||
onOpenLog={mockOnOpenLog}
|
||||
onRetry={vi.fn()}
|
||||
siteInfo={null}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /feature\.moreLikeThis\.title/ }))
|
||||
expect(await screen.findByText('markdown:Child answer')).toBeInTheDocument()
|
||||
await user.click(screen.getAllByRole('button', { name: /operation\.log/ })[1]!)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockOnOpenLog).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({
|
||||
id: 'child-message',
|
||||
log: [
|
||||
{ role: 'user', text: 'Child prompt' },
|
||||
{ role: 'assistant', text: 'Child answer', files: [] },
|
||||
],
|
||||
}),
|
||||
),
|
||||
)
|
||||
expect(mockFetchTextGenerationMessage).toHaveBeenCalledExactlyOnceWith({
|
||||
appId: 'app-1',
|
||||
messageId: 'child-message',
|
||||
})
|
||||
})
|
||||
|
||||
it('should route human input submissions to the workflow service for installed apps', async () => {
|
||||
|
||||
@@ -23,7 +23,6 @@ type GenerationActionGroupsProps = {
|
||||
currentTab: string
|
||||
depth: number
|
||||
feedback?: FeedbackType
|
||||
hideLogAction?: boolean
|
||||
isError: boolean
|
||||
isInWebApp: boolean
|
||||
isResponding?: boolean
|
||||
@@ -33,7 +32,7 @@ type GenerationActionGroupsProps = {
|
||||
moreLikeThis?: boolean
|
||||
onFeedback?: (feedback: FeedbackType) => void
|
||||
onMoreLikeThis: () => void
|
||||
onOpenLogModal: () => void
|
||||
onOpenLog?: () => void
|
||||
onRetry: () => void
|
||||
onSave?: (messageId: string) => void
|
||||
supportFeedback?: boolean
|
||||
@@ -47,7 +46,6 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
|
||||
currentTab,
|
||||
depth,
|
||||
feedback,
|
||||
hideLogAction,
|
||||
isError,
|
||||
isInWebApp,
|
||||
isResponding,
|
||||
@@ -57,7 +55,7 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
|
||||
moreLikeThis,
|
||||
onFeedback,
|
||||
onMoreLikeThis,
|
||||
onOpenLogModal,
|
||||
onOpenLog,
|
||||
onRetry,
|
||||
onSave,
|
||||
supportFeedback,
|
||||
@@ -70,7 +68,7 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideLogAction &&
|
||||
{onOpenLog &&
|
||||
!isInWebApp &&
|
||||
appSourceType !== AppSourceTypeEnum.installedApp &&
|
||||
!isResponding && (
|
||||
@@ -79,7 +77,7 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
|
||||
aria-label={t(($) => $['operation.log'], { ns: 'common' })}
|
||||
disabled={isError || !messageId}
|
||||
title={t(($) => $['operation.log'], { ns: 'common' })}
|
||||
onClick={onOpenLogModal}
|
||||
onClick={onOpenLog}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-file-list-3-line size-4" />
|
||||
</IconButton>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import type { FeedbackType, IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
@@ -10,7 +10,6 @@ import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
@@ -58,7 +57,7 @@ type IGenerationItemProps = {
|
||||
controlClearMoreLikeThis?: number
|
||||
supportFeedback?: boolean
|
||||
isShowTextToSpeech?: boolean
|
||||
hideLogAction?: boolean
|
||||
onOpenLog?: (item: IChatItem) => void
|
||||
hideProcessDetail?: boolean
|
||||
siteInfo: SiteInfo | null
|
||||
inSidePanel?: boolean
|
||||
@@ -87,7 +86,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
controlClearMoreLikeThis,
|
||||
supportFeedback,
|
||||
isShowTextToSpeech,
|
||||
hideLogAction,
|
||||
onOpenLog,
|
||||
hideProcessDetail,
|
||||
siteInfo,
|
||||
inSidePanel,
|
||||
@@ -102,9 +101,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
})
|
||||
const { config } = useChatContext()
|
||||
|
||||
const setCurrentLogItem = useAppStore((s) => s.setCurrentLogItem)
|
||||
const setShowPromptLogModal = useAppStore((s) => s.setShowPromptLogModal)
|
||||
|
||||
const handleFeedback = async (childFeedback: FeedbackType) => {
|
||||
await updateFeedback(
|
||||
{ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } },
|
||||
@@ -132,7 +128,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
installedAppId,
|
||||
controlClearMoreLikeThis,
|
||||
isWorkflow,
|
||||
hideLogAction,
|
||||
onOpenLog,
|
||||
siteInfo,
|
||||
taskId,
|
||||
}
|
||||
@@ -168,13 +164,13 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
setChildMessageId(null)
|
||||
}, [isLoading])
|
||||
|
||||
const handleOpenLogModal = async () => {
|
||||
const handleOpenLog = async () => {
|
||||
if (!onOpenLog) return
|
||||
const data = await fetchTextGenerationMessage({
|
||||
appId: params.appId as string,
|
||||
messageId: messageId!,
|
||||
})
|
||||
setCurrentLogItem(buildPromptLogItem(data))
|
||||
setShowPromptLogModal(true)
|
||||
onOpenLog(buildPromptLogItem(data))
|
||||
}
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>('DETAIL')
|
||||
@@ -277,7 +273,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
currentTab={currentTab}
|
||||
depth={depth}
|
||||
feedback={feedback}
|
||||
hideLogAction={hideLogAction}
|
||||
isError={isError}
|
||||
isInWebApp={isInWebApp}
|
||||
isResponding={isResponding}
|
||||
@@ -287,7 +282,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
moreLikeThis={moreLikeThis}
|
||||
onFeedback={onFeedback}
|
||||
onMoreLikeThis={handleMoreLikeThis}
|
||||
onOpenLogModal={handleOpenLogModal}
|
||||
onOpenLog={onOpenLog ? handleOpenLog : undefined}
|
||||
onRetry={onRetry}
|
||||
onSave={onSave}
|
||||
supportFeedback={supportFeedback}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import type { IChatItem } from '../type'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import ChatLogModals from '../chat-log-modals'
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
fetchAgentLogDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ChatLogModals', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({
|
||||
appDetail: { id: 'app-1' } as ReturnType<typeof useAppStore.getState>['appDetail'],
|
||||
})
|
||||
})
|
||||
|
||||
// Modal visibility should follow the two booleans unless log modals are globally hidden.
|
||||
describe('Rendering', () => {
|
||||
it('should render real prompt and agent log modals when enabled', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
render(
|
||||
<ChatLogModals
|
||||
width={480}
|
||||
currentLogItem={
|
||||
{
|
||||
id: 'log-1',
|
||||
isAnswer: true,
|
||||
content: 'reply',
|
||||
input: { question: 'hello' },
|
||||
log: [{ role: 'user', text: 'Prompt body' }],
|
||||
conversationId: 'conversation-1',
|
||||
} as IChatItem
|
||||
}
|
||||
showPromptLogModal={true}
|
||||
showAgentLogModal={true}
|
||||
setCurrentLogItem={vi.fn()}
|
||||
setShowPromptLogModal={vi.fn()}
|
||||
setShowAgentLogModal={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('PROMPT LOG')).toBeInTheDocument()
|
||||
expect(screen.getByText('Prompt body')).toBeInTheDocument()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render nothing when hideLogModal is true', () => {
|
||||
render(
|
||||
<ChatLogModals
|
||||
width={320}
|
||||
currentLogItem={
|
||||
{
|
||||
id: 'log-2',
|
||||
isAnswer: true,
|
||||
content: 'reply',
|
||||
log: [{ role: 'user', text: 'Prompt body' }],
|
||||
conversationId: 'conversation-2',
|
||||
} as IChatItem
|
||||
}
|
||||
showPromptLogModal={true}
|
||||
showAgentLogModal={true}
|
||||
hideLogModal={true}
|
||||
setCurrentLogItem={vi.fn()}
|
||||
setShowPromptLogModal={vi.fn()}
|
||||
setShowAgentLogModal={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('PROMPT LOG')).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: /appLog.runDetail.workflowTitle/i }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Cancel actions should clear the current item and close only the targeted modal.
|
||||
describe('User Interactions', () => {
|
||||
it('should close the prompt log modal through the real close action', async () => {
|
||||
const user = userEvent.setup()
|
||||
const setCurrentLogItem = vi.fn()
|
||||
const setShowPromptLogModal = vi.fn()
|
||||
const setShowAgentLogModal = vi.fn()
|
||||
|
||||
render(
|
||||
<ChatLogModals
|
||||
width={480}
|
||||
currentLogItem={
|
||||
{
|
||||
id: 'log-3',
|
||||
isAnswer: true,
|
||||
content: 'reply',
|
||||
input: { question: 'hello' },
|
||||
log: [{ role: 'user', text: 'Prompt body' }],
|
||||
} as IChatItem
|
||||
}
|
||||
showPromptLogModal={true}
|
||||
showAgentLogModal={false}
|
||||
setCurrentLogItem={setCurrentLogItem}
|
||||
setShowPromptLogModal={setShowPromptLogModal}
|
||||
setShowAgentLogModal={setShowAgentLogModal}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
expect(setCurrentLogItem).toHaveBeenCalled()
|
||||
expect(setShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
expect(setShowAgentLogModal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close the agent log modal through the real close action', async () => {
|
||||
const user = userEvent.setup()
|
||||
const setCurrentLogItem = vi.fn()
|
||||
const setShowPromptLogModal = vi.fn()
|
||||
const setShowAgentLogModal = vi.fn()
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
|
||||
render(
|
||||
<ChatLogModals
|
||||
width={480}
|
||||
currentLogItem={
|
||||
{
|
||||
id: 'log-4',
|
||||
isAnswer: true,
|
||||
content: 'reply',
|
||||
input: { question: 'hello' },
|
||||
log: [{ role: 'user', text: 'Prompt body' }],
|
||||
conversationId: 'conversation-4',
|
||||
} as IChatItem
|
||||
}
|
||||
showPromptLogModal={false}
|
||||
showAgentLogModal={true}
|
||||
setCurrentLogItem={setCurrentLogItem}
|
||||
setShowPromptLogModal={setShowPromptLogModal}
|
||||
setShowAgentLogModal={setShowAgentLogModal}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
await user.click(
|
||||
screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i })
|
||||
.nextElementSibling as HTMLElement,
|
||||
)
|
||||
|
||||
expect(setCurrentLogItem).toHaveBeenCalled()
|
||||
expect(setShowAgentLogModal).toHaveBeenCalledWith(false)
|
||||
expect(setShowPromptLogModal).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -31,7 +31,6 @@ describe('ChatContextProvider', () => {
|
||||
config: {} as ChatContextValue['config'],
|
||||
isResponding: false,
|
||||
chatList: [{ id: '1', content: 'hello' } as ChatItem],
|
||||
showPromptLog: false,
|
||||
questionIcon: <span data-testid="custom-icon">Icon</span>,
|
||||
answerIcon: null,
|
||||
onSend: mockOnSend,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { ChatProps } from '../index'
|
||||
import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types'
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { createTheme } from '../../embedded-chatbot/theme/theme'
|
||||
import Chat from '../index'
|
||||
|
||||
@@ -14,9 +13,6 @@ import Chat from '../index'
|
||||
// Question – pulls Markdown, copy-to-clipboard, react-textarea-autosize.
|
||||
// ChatInputArea – pulls browser audio APIs unavailable in the test DOM runtime
|
||||
// and the VoiceInput / FileContextProvider chains.
|
||||
// PromptLogModal– pulls CopyFeedback and deep modal dep chain.
|
||||
// AgentLogModal – pulls @remixicon/react (causes lint push error), useClickAway
|
||||
// from ahooks, and AgentLogDetail (workflow graph renderer).
|
||||
// es-toolkit/compat – debounce must return a fn with .cancel() or the cleanup
|
||||
// effect throws on unmount.
|
||||
//
|
||||
@@ -80,26 +76,6 @@ vi.mock('../chat-input-area', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/prompt-log-modal', () => ({
|
||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||
<div data-testid="prompt-log-modal">
|
||||
<button data-testid="prompt-log-cancel" onClick={onCancel}>
|
||||
cancel
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/agent-log-modal', () => ({
|
||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||
<div data-testid="agent-log-modal">
|
||||
<button data-testid="agent-log-cancel" onClick={onCancel}>
|
||||
cancel
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: (fn: (...args: unknown[]) => void) => {
|
||||
const debounced = (...args: unknown[]) => fn(...args)
|
||||
@@ -130,19 +106,6 @@ const makeChatItem = (overrides: Partial<ChatItem> = {}): ChatItem => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const mockSetCurrentLogItem = vi.fn()
|
||||
const mockSetShowPromptLogModal = vi.fn()
|
||||
const mockSetShowAgentLogModal = vi.fn()
|
||||
|
||||
const baseStoreState = {
|
||||
currentLogItem: undefined,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
showPromptLogModal: false,
|
||||
setShowPromptLogModal: mockSetShowPromptLogModal,
|
||||
showAgentLogModal: false,
|
||||
setShowAgentLogModal: mockSetShowAgentLogModal,
|
||||
}
|
||||
|
||||
const renderChat = (props: Partial<ChatProps> = {}) => render(<Chat chatList={[]} {...props} />)
|
||||
|
||||
// ─── Suite ────────────────────────────────────────────────────────────────────
|
||||
@@ -171,8 +134,6 @@ describe('Chat', () => {
|
||||
disconnect() {}
|
||||
},
|
||||
)
|
||||
|
||||
useAppStore.setState(baseStoreState)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -476,68 +437,6 @@ describe('Chat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('PromptLogModal', () => {
|
||||
it('should render when showPromptLogModal=true and hideLogModal is falsy', () => {
|
||||
useAppStore.setState({ ...baseStoreState, showPromptLogModal: true })
|
||||
renderChat({ hideLogModal: false })
|
||||
expect(screen.getByTestId('prompt-log-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render when showPromptLogModal=false', () => {
|
||||
useAppStore.setState({ ...baseStoreState, showPromptLogModal: false })
|
||||
renderChat()
|
||||
expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render when hideLogModal=true even if showPromptLogModal=true', () => {
|
||||
useAppStore.setState({ ...baseStoreState, showPromptLogModal: true })
|
||||
renderChat({ hideLogModal: true })
|
||||
expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call setCurrentLogItem and setShowPromptLogModal(false) on cancel', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ ...baseStoreState, showPromptLogModal: true })
|
||||
renderChat({ hideLogModal: false })
|
||||
|
||||
await user.click(screen.getByTestId('prompt-log-cancel'))
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('AgentLogModal', () => {
|
||||
it('should render when showAgentLogModal=true and hideLogModal is falsy', () => {
|
||||
useAppStore.setState({ ...baseStoreState, showAgentLogModal: true })
|
||||
renderChat({ hideLogModal: false })
|
||||
expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render when showAgentLogModal=false', () => {
|
||||
useAppStore.setState({ ...baseStoreState, showAgentLogModal: false })
|
||||
renderChat()
|
||||
expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render when hideLogModal=true even if showAgentLogModal=true', () => {
|
||||
useAppStore.setState({ ...baseStoreState, showAgentLogModal: true })
|
||||
renderChat({ hideLogModal: true })
|
||||
expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call setCurrentLogItem and setShowAgentLogModal(false) on cancel', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ ...baseStoreState, showAgentLogModal: true })
|
||||
renderChat({ hideLogModal: false })
|
||||
|
||||
await user.click(screen.getByTestId('agent-log-cancel'))
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetShowAgentLogModal).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Window Resize', () => {
|
||||
it('should register a resize listener on mount', () => {
|
||||
const addSpy = vi.spyOn(window, 'addEventListener')
|
||||
@@ -677,26 +576,6 @@ describe('Chat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should render no modals when both modal flags are false', () => {
|
||||
useAppStore.setState({
|
||||
...baseStoreState,
|
||||
showPromptLogModal: false,
|
||||
showAgentLogModal: false,
|
||||
})
|
||||
renderChat()
|
||||
expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render both modals when both flags are true and hideLogModal is false', () => {
|
||||
useAppStore.setState({ ...baseStoreState, showPromptLogModal: true, showAgentLogModal: true })
|
||||
renderChat({ hideLogModal: false })
|
||||
expect(screen.getByTestId('prompt-log-modal')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Question Rendering with Config', () => {
|
||||
it('should pass questionEditEnable from config to Question component', () => {
|
||||
renderChat({
|
||||
@@ -778,17 +657,6 @@ describe('Chat', () => {
|
||||
expect(screen.getByTestId('answer-item')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass showPromptLog to Answer component', () => {
|
||||
renderChat({
|
||||
showPromptLog: true,
|
||||
chatList: [
|
||||
makeChatItem({ id: 'q1', isAnswer: false }),
|
||||
makeChatItem({ id: 'a1', isAnswer: true }),
|
||||
],
|
||||
})
|
||||
expect(screen.getByTestId('answer-item')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass hideProcessDetail to Answer component', () => {
|
||||
renderChat({
|
||||
hideProcessDetail: true,
|
||||
@@ -1202,50 +1070,4 @@ describe('Chat', () => {
|
||||
expect(() => window.dispatchEvent(new Event('resize'))).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Modal Interaction Paths', () => {
|
||||
it('should handle prompt log cancel and subsequent reopen', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ ...baseStoreState, showPromptLogModal: true })
|
||||
const { rerender } = renderChat({ hideLogModal: false })
|
||||
|
||||
await user.click(screen.getByTestId('prompt-log-cancel'))
|
||||
|
||||
expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
|
||||
// Reopen modal
|
||||
useAppStore.setState({ ...baseStoreState, showPromptLogModal: true })
|
||||
rerender(<Chat chatList={[]} hideLogModal={false} />)
|
||||
|
||||
expect(screen.getByTestId('prompt-log-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle agent log cancel and subsequent reopen', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ ...baseStoreState, showAgentLogModal: true })
|
||||
const { rerender } = renderChat({ hideLogModal: false })
|
||||
|
||||
await user.click(screen.getByTestId('agent-log-cancel'))
|
||||
|
||||
expect(mockSetShowAgentLogModal).toHaveBeenCalledWith(false)
|
||||
|
||||
// Reopen modal
|
||||
useAppStore.setState({ ...baseStoreState, showAgentLogModal: true })
|
||||
rerender(<Chat chatList={[]} hideLogModal={false} />)
|
||||
|
||||
expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle hideLogModal preventing both modals from showing', () => {
|
||||
useAppStore.setState({
|
||||
...baseStoreState,
|
||||
showPromptLogModal: true,
|
||||
showAgentLogModal: true,
|
||||
})
|
||||
renderChat({ hideLogModal: true })
|
||||
|
||||
expect(screen.queryByTestId('prompt-log-modal')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('agent-log-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { ChatItem } from '../../types'
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Chat from '../index'
|
||||
|
||||
// Keep the Chat/context/Operation/Log capability path real while omitting rich answer rendering.
|
||||
vi.mock('../answer', async () => {
|
||||
const { default: Operation } = await import('../answer/operation')
|
||||
return {
|
||||
default: ({ item }: { item: ChatItem }) => (
|
||||
<Operation
|
||||
item={item}
|
||||
question=""
|
||||
index={0}
|
||||
maxSize={500}
|
||||
contentWidth={100}
|
||||
hasWorkflowProcess={false}
|
||||
/>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../question', () => ({ default: () => null }))
|
||||
vi.mock('../chat-input-area', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/base/new-audio-button', () => ({ default: () => null }))
|
||||
vi.mock('@/app/components/app/annotation/edit-annotation-modal', () => ({ default: () => null }))
|
||||
vi.mock(
|
||||
'@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button',
|
||||
() => ({ default: () => null }),
|
||||
)
|
||||
|
||||
const firstAnswer: ChatItem = { id: 'first-answer', content: 'First answer', isAnswer: true }
|
||||
const secondAnswer: ChatItem = { id: 'second-answer', content: 'Second answer', isAnswer: true }
|
||||
|
||||
describe('Chat log actions', () => {
|
||||
it('keeps two mounted chat surfaces connected to their own log owner', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onFirstLog = vi.fn()
|
||||
const onSecondLog = vi.fn()
|
||||
render(
|
||||
<>
|
||||
<section aria-label="First chat">
|
||||
<Chat chatList={[firstAnswer]} noChatInput onOpenLog={onFirstLog} />
|
||||
</section>
|
||||
<section aria-label="Second chat">
|
||||
<Chat chatList={[secondAnswer]} noChatInput onOpenLog={onSecondLog} />
|
||||
</section>
|
||||
</>,
|
||||
)
|
||||
|
||||
await user.click(
|
||||
within(screen.getByRole('region', { name: 'Second chat' })).getByRole('button', {
|
||||
name: /operation\.log/,
|
||||
}),
|
||||
)
|
||||
expect(onSecondLog).toHaveBeenCalledExactlyOnceWith(secondAnswer)
|
||||
expect(onFirstLog).not.toHaveBeenCalled()
|
||||
|
||||
await user.click(
|
||||
within(screen.getByRole('region', { name: 'First chat' })).getByRole('button', {
|
||||
name: /operation\.log/,
|
||||
}),
|
||||
)
|
||||
expect(onFirstLog).toHaveBeenCalledExactlyOnceWith(firstAnswer)
|
||||
expect(onSecondLog).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('removes the log action when its owner withdraws the capability', () => {
|
||||
const { rerender } = render(<Chat chatList={[firstAnswer]} noChatInput onOpenLog={vi.fn()} />)
|
||||
expect(screen.getByRole('button', { name: /operation\.log/ })).toBeInTheDocument()
|
||||
|
||||
rerender(<Chat chatList={[firstAnswer]} noChatInput />)
|
||||
expect(screen.queryByRole('button', { name: /operation\.log/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows log actions only for items accepted by the owner', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onOpenLog = vi.fn()
|
||||
const answerWithRun = { ...secondAnswer, workflow_run_id: 'run-1' }
|
||||
|
||||
render(
|
||||
<Chat
|
||||
chatList={[firstAnswer, answerWithRun]}
|
||||
noChatInput
|
||||
onOpenLog={onOpenLog}
|
||||
canOpenLog={(item) => !!item.workflow_run_id}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getAllByRole('button', { name: /operation\.log/ })).toHaveLength(1)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /operation\.log/ }))
|
||||
expect(onOpenLog).toHaveBeenCalledExactlyOnceWith(answerWithRun)
|
||||
})
|
||||
})
|
||||
@@ -105,7 +105,6 @@ const renderWithProvider = (
|
||||
config={{} as unknown as ChatConfig | undefined}
|
||||
isResponding={false}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={props.questionIcon}
|
||||
answerIcon={props.answerIcon}
|
||||
onSend={vi.fn()}
|
||||
@@ -961,7 +960,6 @@ describe('Question component', () => {
|
||||
config={{} as unknown as ChatConfig}
|
||||
isResponding={false}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
onSend={vi.fn()}
|
||||
onRegenerate={undefined as unknown as OnRegenerate}
|
||||
onAnnotationEdited={vi.fn()}
|
||||
|
||||
@@ -44,7 +44,7 @@ const LayoutHarness = ({
|
||||
sidebarCollapseState?: boolean
|
||||
attachRefs?: boolean
|
||||
}) => {
|
||||
const { width, chatContainerRef, chatContainerInnerRef, chatFooterRef, chatFooterInnerRef } =
|
||||
const { chatContainerRef, chatContainerInnerRef, chatFooterRef, chatFooterInnerRef } =
|
||||
useChatLayout({ chatList, sidebarCollapseState })
|
||||
|
||||
return (
|
||||
@@ -85,7 +85,6 @@ const LayoutHarness = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<output data-testid="layout-width">{width}</output>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -150,7 +149,6 @@ describe('useChatLayout', () => {
|
||||
vi.runAllTimers()
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('layout-width')).toHaveTextContent('600')
|
||||
expect(screen.getByTestId('chat-footer').style.width).toBe('400px')
|
||||
expect(screen.getByTestId('chat-footer-inner').style.width).toBe('360px')
|
||||
expect((screen.getByTestId('chat-container') as HTMLDivElement).scrollTop).toBe(640)
|
||||
@@ -265,7 +263,6 @@ describe('useChatLayout', () => {
|
||||
vi.runAllTimers()
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('layout-width')).toHaveTextContent('0')
|
||||
expect(capturedResizeCallbacks).toHaveLength(0)
|
||||
expect(screen.getByTestId('chat-footer').style.width).toBe('')
|
||||
expect(screen.getByTestId('chat-footer-inner').style.width).toBe('')
|
||||
|
||||
@@ -107,14 +107,6 @@ vi.mock('@/app/components/base/new-audio-button', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat/log', () => ({
|
||||
default: () => (
|
||||
<button data-testid="log-btn">
|
||||
<div className="i-ri-file-list-3-line" />
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: vi.fn(() => ({ appId: 'test-app' })),
|
||||
usePathname: vi.fn(() => '/apps/test-app'),
|
||||
@@ -177,7 +169,6 @@ type OperationProps = {
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
showPromptLog?: boolean
|
||||
maxSize: number
|
||||
contentWidth: number
|
||||
hasWorkflowProcess: boolean
|
||||
@@ -241,6 +232,7 @@ describe('Operation', () => {
|
||||
mockContextValue.onAnnotationRemoved = vi.fn()
|
||||
mockContextValue.readonly = false
|
||||
mockContextValue.showRegenerate = false
|
||||
mockContextValue.onOpenLog = undefined
|
||||
|
||||
mockAddAnnotation.mockResolvedValue({ id: 'ann-new', account: { name: 'Test User' } })
|
||||
})
|
||||
@@ -343,26 +335,29 @@ describe('Operation', () => {
|
||||
expect(screen.queryByTestId('annotation-ctrl')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show prompt log when showPromptLog is true', () => {
|
||||
renderOperation({ ...baseProps, showPromptLog: true })
|
||||
expect(screen.getByTestId('log-btn'))!.toBeInTheDocument()
|
||||
it('should show the log action when the owner provides it', () => {
|
||||
mockContextValue.onOpenLog = vi.fn()
|
||||
renderOperation()
|
||||
expect(screen.getByRole('button', { name: 'operation.log' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep hover-only controls visible when a descendant popup is open', () => {
|
||||
renderOperation({ ...baseProps, showPromptLog: true })
|
||||
mockContextValue.onOpenLog = vi.fn()
|
||||
renderOperation()
|
||||
|
||||
expect(screen.getByTestId('operation-actions')).toHaveClass(
|
||||
'group-has-[[data-popup-open]]:flex',
|
||||
)
|
||||
expect(screen.getByTestId('log-btn').parentElement).toHaveClass(
|
||||
'group-has-[[data-popup-open]]:block',
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'operation.log' }).parentElement?.parentElement,
|
||||
).toHaveClass('group-has-[[data-popup-open]]:block')
|
||||
})
|
||||
|
||||
it('should not show prompt log for opening statements', () => {
|
||||
const item = { ...baseItem, isOpeningStatement: true }
|
||||
renderOperation({ ...baseProps, item, showPromptLog: true })
|
||||
expect(screen.queryByTestId('log-btn')).not.toBeInTheDocument()
|
||||
mockContextValue.onOpenLog = vi.fn()
|
||||
renderOperation({ ...baseProps, item })
|
||||
expect(screen.queryByRole('button', { name: 'operation.log' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -897,7 +892,8 @@ describe('Operation', () => {
|
||||
feedback: { rating: 'like' as const },
|
||||
adminFeedback: { rating: 'dislike' as const },
|
||||
}
|
||||
renderOperation({ ...baseProps, item, showPromptLog: true })
|
||||
mockContextValue.onOpenLog = vi.fn()
|
||||
renderOperation({ ...baseProps, item })
|
||||
const bar = screen.getByTestId('operation-bar')
|
||||
expect(bar)!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -19,15 +19,10 @@ const meta = {
|
||||
'If set to true, some buttons that are supposed to be shown on hover will not be displayed.',
|
||||
},
|
||||
responding: { control: 'boolean', description: 'Indicates if the answer is being generated.' },
|
||||
showPromptLog: {
|
||||
control: 'boolean',
|
||||
description: 'If set to true, the prompt log button will be shown on hover.',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
noChatInput: false,
|
||||
responding: false,
|
||||
showPromptLog: false,
|
||||
},
|
||||
} satisfies Meta<typeof Answer>
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ type AnswerProps = {
|
||||
config?: ChatConfig
|
||||
answerIcon?: ReactNode
|
||||
responding?: boolean
|
||||
showPromptLog?: boolean
|
||||
chatAnswerContainerInner?: string
|
||||
hideProcessDetail?: boolean
|
||||
appData?: AppData
|
||||
@@ -53,7 +52,6 @@ const Answer: FC<AnswerProps> = ({
|
||||
config,
|
||||
answerIcon,
|
||||
responding,
|
||||
showPromptLog,
|
||||
chatAnswerContainerInner,
|
||||
hideProcessDetail,
|
||||
appData,
|
||||
@@ -195,7 +193,6 @@ const Answer: FC<AnswerProps> = ({
|
||||
item={item}
|
||||
question={question}
|
||||
index={index}
|
||||
showPromptLog={showPromptLog}
|
||||
noChatInput={noChatInput}
|
||||
/>
|
||||
)}
|
||||
@@ -256,7 +253,6 @@ const Answer: FC<AnswerProps> = ({
|
||||
item={item}
|
||||
question={question}
|
||||
index={index}
|
||||
showPromptLog={showPromptLog}
|
||||
noChatInput={noChatInput}
|
||||
/>
|
||||
)}
|
||||
@@ -333,7 +329,6 @@ const Answer: FC<AnswerProps> = ({
|
||||
item={item}
|
||||
question={question}
|
||||
index={index}
|
||||
showPromptLog={showPromptLog}
|
||||
noChatInput={noChatInput}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -29,7 +29,6 @@ type OperationProps = {
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
showPromptLog?: boolean
|
||||
maxSize: number
|
||||
contentWidth: number
|
||||
hasWorkflowProcess: boolean
|
||||
@@ -84,7 +83,6 @@ function Operation({
|
||||
item,
|
||||
question,
|
||||
index,
|
||||
showPromptLog,
|
||||
maxSize,
|
||||
contentWidth,
|
||||
hasWorkflowProcess,
|
||||
@@ -93,6 +91,8 @@ function Operation({
|
||||
const { t } = useTranslation(['appLog', 'common'])
|
||||
const {
|
||||
config,
|
||||
onOpenLog,
|
||||
canOpenLog,
|
||||
onAnnotationAdded,
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
@@ -116,6 +116,7 @@ function Operation({
|
||||
|
||||
const content = getPublicResponseContent(item)
|
||||
const hasPublicContent = !!content.trim()
|
||||
const availableLogAction = onOpenLog && (canOpenLog?.(item) ?? true) ? onOpenLog : undefined
|
||||
|
||||
const displayUserFeedback = userFeedbackOverride ?? feedback
|
||||
const displayAdminFeedback = adminFeedbackOverride ?? adminFeedback
|
||||
@@ -200,7 +201,7 @@ function Operation({
|
||||
const operationWidth = useMemo(() => {
|
||||
let width = 0
|
||||
if (!isOpeningStatement) width += 26
|
||||
if (!isOpeningStatement && showPromptLog) width += 28 + 8
|
||||
if (!isOpeningStatement && availableLogAction) width += 28 + 8
|
||||
if (!isOpeningStatement && config?.text_to_speech?.enabled && hasPublicContent) width += 26
|
||||
if (!isOpeningStatement && shouldShowAnnotationAction) width += 26
|
||||
if (shouldShowUserFeedbackBar) width += hasUserFeedback ? 28 + 8 : 60 + 8
|
||||
@@ -214,10 +215,10 @@ function Operation({
|
||||
hasPublicContent,
|
||||
hasUserFeedback,
|
||||
isOpeningStatement,
|
||||
availableLogAction,
|
||||
shouldShowAdminFeedbackBar,
|
||||
shouldShowAnnotationAction,
|
||||
shouldShowUserFeedbackBar,
|
||||
showPromptLog,
|
||||
])
|
||||
|
||||
const positionRight = useMemo(
|
||||
@@ -459,9 +460,9 @@ function Operation({
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{showPromptLog && !isOpeningStatement && (
|
||||
{availableLogAction && !isOpeningStatement && (
|
||||
<div className={cn('hidden', answerActiveBlockClassName)}>
|
||||
<Log logItem={item} />
|
||||
<Log logItem={item} onOpenLog={availableLogAction} />
|
||||
</div>
|
||||
)}
|
||||
{!isOpeningStatement && (
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import type { IChatItem } from './type'
|
||||
import AgentLogModal from '@/app/components/base/agent-log-modal'
|
||||
import PromptLogModal from '@/app/components/base/prompt-log-modal'
|
||||
|
||||
type ChatLogModalsProps = {
|
||||
width: number
|
||||
currentLogItem?: IChatItem
|
||||
showPromptLogModal: boolean
|
||||
showAgentLogModal: boolean
|
||||
hideLogModal?: boolean
|
||||
setCurrentLogItem: (item?: IChatItem) => void
|
||||
setShowPromptLogModal: (showPromptLogModal: boolean) => void
|
||||
setShowAgentLogModal: (showAgentLogModal: boolean) => void
|
||||
}
|
||||
|
||||
const ChatLogModals: FC<ChatLogModalsProps> = ({
|
||||
width,
|
||||
currentLogItem,
|
||||
showPromptLogModal,
|
||||
showAgentLogModal,
|
||||
hideLogModal,
|
||||
setCurrentLogItem,
|
||||
setShowPromptLogModal,
|
||||
setShowAgentLogModal,
|
||||
}) => {
|
||||
if (hideLogModal) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowPromptLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showAgentLogModal && (
|
||||
<AgentLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowAgentLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatLogModals
|
||||
@@ -14,7 +14,8 @@ export const ChatContextProvider = ({
|
||||
config,
|
||||
isResponding,
|
||||
chatList,
|
||||
showPromptLog,
|
||||
onOpenLog,
|
||||
canOpenLog,
|
||||
questionIcon,
|
||||
answerIcon,
|
||||
onSend,
|
||||
@@ -34,7 +35,8 @@ export const ChatContextProvider = ({
|
||||
readonly,
|
||||
isResponding,
|
||||
chatList: chatList || [],
|
||||
showPromptLog,
|
||||
onOpenLog,
|
||||
canOpenLog,
|
||||
questionIcon,
|
||||
answerIcon,
|
||||
onSend,
|
||||
|
||||
@@ -8,7 +8,8 @@ export type ChatContextValue = Pick<
|
||||
| 'config'
|
||||
| 'isResponding'
|
||||
| 'chatList'
|
||||
| 'showPromptLog'
|
||||
| 'onOpenLog'
|
||||
| 'canOpenLog'
|
||||
| 'questionIcon'
|
||||
| 'answerIcon'
|
||||
| 'onSend'
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { FC, ReactNode, Ref } from 'react'
|
||||
import type { Theme } from '../embedded-chatbot/theme/theme'
|
||||
import type { ChatConfig, ChatItem, OnFeedback, OnRegenerate, OnSend } from '../types'
|
||||
import type { HumanInputFormSubmitData } from './answer/human-input-content/type'
|
||||
import type { AnswerActionPosition } from './answer/operation'
|
||||
import type { InputForm } from './type'
|
||||
import type { IChatItem, InputForm } from './type'
|
||||
import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types'
|
||||
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import type { AppData, ToolIcon } from '@/models/share'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { memo, useEffect, useImperativeHandle, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Answer from './answer'
|
||||
import ChatInputArea from './chat-input-area'
|
||||
import ChatLogModals from './chat-log-modals'
|
||||
import { ChatContextProvider } from './context-provider'
|
||||
import Question from './question'
|
||||
import TryToAsk from './try-to-ask'
|
||||
@@ -38,12 +35,14 @@ export type ChatProps = {
|
||||
inputs?: Record<string, unknown>
|
||||
inputsForm?: InputForm[]
|
||||
onRegenerate?: OnRegenerate
|
||||
chatContainerRef?: Ref<HTMLDivElement>
|
||||
chatContainerClassName?: string
|
||||
chatContainerInnerClassName?: string
|
||||
chatFooterClassName?: string
|
||||
chatFooterInnerClassName?: string
|
||||
suggestedQuestions?: string[]
|
||||
showPromptLog?: boolean
|
||||
onOpenLog?: (item: IChatItem) => void
|
||||
canOpenLog?: (item: IChatItem) => boolean
|
||||
questionIcon?: ReactNode
|
||||
answerIcon?: ReactNode
|
||||
allToolIcons?: Record<string, ToolIcon>
|
||||
@@ -61,7 +60,6 @@ export type ChatProps = {
|
||||
onFeedback?: OnFeedback
|
||||
chatAnswerContainerInner?: string
|
||||
hideProcessDetail?: boolean
|
||||
hideLogModal?: boolean
|
||||
theme?: Theme
|
||||
switchSibling?: (siblingMessageId: string) => void
|
||||
showFeatureBar?: boolean
|
||||
@@ -106,12 +104,14 @@ const Chat: FC<ChatProps> = ({
|
||||
onStopResponding,
|
||||
noChatInput,
|
||||
showRegenerate,
|
||||
chatContainerRef: externalChatContainerRef,
|
||||
chatContainerClassName,
|
||||
chatContainerInnerClassName,
|
||||
chatFooterClassName,
|
||||
chatFooterInnerClassName,
|
||||
suggestedQuestions,
|
||||
showPromptLog,
|
||||
onOpenLog,
|
||||
canOpenLog,
|
||||
questionIcon,
|
||||
answerIcon,
|
||||
onAnnotationAdded,
|
||||
@@ -122,7 +122,6 @@ const Chat: FC<ChatProps> = ({
|
||||
onFeedback,
|
||||
chatAnswerContainerInner,
|
||||
hideProcessDetail,
|
||||
hideLogModal,
|
||||
theme,
|
||||
switchSibling,
|
||||
showFeatureBar,
|
||||
@@ -168,29 +167,14 @@ const Chat: FC<ChatProps> = ({
|
||||
if (announcement && status.textContent !== announcement) status.textContent = announcement
|
||||
wasRespondingRef.current = !!isResponding
|
||||
}, [hasAgentContent, isResponding, t])
|
||||
const {
|
||||
currentLogItem,
|
||||
setCurrentLogItem,
|
||||
showPromptLogModal,
|
||||
setShowPromptLogModal,
|
||||
showAgentLogModal,
|
||||
setShowAgentLogModal,
|
||||
} = useAppStore(
|
||||
useShallow((state) => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
showPromptLogModal: state.showPromptLogModal,
|
||||
setShowPromptLogModal: state.setShowPromptLogModal,
|
||||
showAgentLogModal: state.showAgentLogModal,
|
||||
setShowAgentLogModal: state.setShowAgentLogModal,
|
||||
})),
|
||||
)
|
||||
const { width, chatContainerRef, chatContainerInnerRef, chatFooterRef, chatFooterInnerRef } =
|
||||
const { chatContainerRef, chatContainerInnerRef, chatFooterRef, chatFooterInnerRef } =
|
||||
useChatLayout({
|
||||
chatList,
|
||||
sidebarCollapseState,
|
||||
})
|
||||
|
||||
useImperativeHandle(externalChatContainerRef, () => chatContainerRef.current!, [chatContainerRef])
|
||||
|
||||
const hasTryToAsk =
|
||||
config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend
|
||||
|
||||
@@ -200,7 +184,8 @@ const Chat: FC<ChatProps> = ({
|
||||
config={config}
|
||||
chatList={chatList}
|
||||
isResponding={isResponding}
|
||||
showPromptLog={showPromptLog}
|
||||
onOpenLog={onOpenLog}
|
||||
canOpenLog={canOpenLog}
|
||||
questionIcon={questionIcon}
|
||||
answerIcon={answerIcon}
|
||||
onSend={onSend}
|
||||
@@ -250,7 +235,6 @@ const Chat: FC<ChatProps> = ({
|
||||
config={config}
|
||||
answerIcon={answerIcon}
|
||||
responding={isLast && isResponding}
|
||||
showPromptLog={showPromptLog}
|
||||
chatAnswerContainerInner={chatAnswerContainerInner}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
noChatInput={noChatInput}
|
||||
@@ -334,16 +318,6 @@ const Chat: FC<ChatProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChatLogModals
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
showPromptLogModal={showPromptLogModal}
|
||||
showAgentLogModal={showAgentLogModal}
|
||||
hideLogModal={hideLogModal}
|
||||
setCurrentLogItem={setCurrentLogItem}
|
||||
setShowPromptLogModal={setShowPromptLogModal}
|
||||
setShowAgentLogModal={setShowAgentLogModal}
|
||||
/>
|
||||
</div>
|
||||
</ChatContextProvider>
|
||||
)
|
||||
|
||||
@@ -1,126 +1,41 @@
|
||||
import type { IChatItem, ThoughtItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { IChatItem } from '../../type'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vite-plus/test'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Log from '../index'
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: vi.fn(),
|
||||
}))
|
||||
const logItem: IChatItem = {
|
||||
id: 'message-1',
|
||||
content: 'Answer',
|
||||
isAnswer: true,
|
||||
workflow_run_id: 'run-1',
|
||||
}
|
||||
|
||||
describe('Log', () => {
|
||||
const mockSetCurrentLogItem = vi.fn()
|
||||
const mockSetShowPromptLogModal = vi.fn()
|
||||
const mockSetShowAgentLogModal = vi.fn()
|
||||
const mockSetShowMessageLogModal = vi.fn()
|
||||
|
||||
const createLogItem = (overrides?: Partial<IChatItem>): IChatItem => ({
|
||||
id: '1',
|
||||
content: 'test',
|
||||
isAnswer: true, // Required per your IChatItem type
|
||||
workflow_run_id: '',
|
||||
agent_thoughts: [],
|
||||
message_files: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useAppStore).mockImplementation((selector) =>
|
||||
selector({
|
||||
// State properties
|
||||
currentLogModalActiveTab: 'question',
|
||||
showPromptLogModal: false,
|
||||
showAgentLogModal: false,
|
||||
showMessageLogModal: false,
|
||||
showAppConfigureFeaturesModal: false, // Fixed: Added missing required property
|
||||
currentLogItem: null,
|
||||
// Action functions
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowPromptLogModal: mockSetShowPromptLogModal,
|
||||
setShowAgentLogModal: mockSetShowAgentLogModal,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
} as unknown as Parameters<typeof selector>[0]),
|
||||
) // Fixed: Double cast to avoid overlap error
|
||||
})
|
||||
|
||||
it('should render correctly', () => {
|
||||
render(<Log logItem={createLogItem()} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show message log modal when workflow_run_id exists', async () => {
|
||||
it('sends the selected item to its owner without activating the surrounding surface', async () => {
|
||||
const user = userEvent.setup()
|
||||
const logItem = createLogItem({ workflow_run_id: 'run-123' })
|
||||
const onOpenLog = vi.fn()
|
||||
const onParentClick = vi.fn()
|
||||
render(
|
||||
<div role="presentation" onClick={onParentClick}>
|
||||
<Log logItem={logItem} onOpenLog={onOpenLog} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
render(<Log logItem={logItem} />)
|
||||
const container = screen.getByRole('button').parentElement
|
||||
if (container) await user.click(container)
|
||||
await user.click(screen.getByRole('button', { name: /operation\.log/ }))
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledWith(logItem)
|
||||
expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(true)
|
||||
expect(onOpenLog).toHaveBeenCalledExactlyOnceWith(logItem)
|
||||
expect(onParentClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show agent log modal when agent_thoughts exists and workflow_run_id is missing', async () => {
|
||||
it.each(['{Enter}', ' '])('opens the selected log with %s', async (key) => {
|
||||
const user = userEvent.setup()
|
||||
const thought: ThoughtItem = {
|
||||
id: 't1',
|
||||
tool: 'test',
|
||||
thought: 'thinking',
|
||||
tool_input: '',
|
||||
message_id: 'm1',
|
||||
conversation_id: 'c1',
|
||||
observation: '',
|
||||
position: 1,
|
||||
}
|
||||
const logItem = createLogItem({
|
||||
workflow_run_id: '',
|
||||
agent_thoughts: [thought],
|
||||
})
|
||||
const onOpenLog = vi.fn()
|
||||
render(<Log logItem={logItem} onOpenLog={onOpenLog} />)
|
||||
|
||||
render(<Log logItem={logItem} />)
|
||||
const container = screen.getByRole('button').parentElement
|
||||
if (container) await user.click(container)
|
||||
await user.tab()
|
||||
expect(screen.getByRole('button', { name: /operation\.log/ })).toHaveFocus()
|
||||
await user.keyboard(key)
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledWith(logItem)
|
||||
expect(mockSetShowAgentLogModal).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should show prompt log modal when both workflow_run_id and agent_thoughts are missing', async () => {
|
||||
const user = userEvent.setup()
|
||||
const logItem = createLogItem({
|
||||
workflow_run_id: '',
|
||||
agent_thoughts: [],
|
||||
})
|
||||
|
||||
render(<Log logItem={logItem} />)
|
||||
const container = screen.getByRole('button').parentElement
|
||||
if (container) await user.click(container)
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledWith(logItem)
|
||||
expect(mockSetShowPromptLogModal).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should prevent event propagation on click', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
// 1. Spy on both the standard propagation and the immediate propagation
|
||||
const stopPropagationSpy = vi.spyOn(Event.prototype, 'stopPropagation')
|
||||
const stopImmediatePropagationSpy = vi.spyOn(Event.prototype, 'stopImmediatePropagation')
|
||||
|
||||
render(<Log logItem={createLogItem()} />)
|
||||
|
||||
// Find the container div that has the onClick handler
|
||||
const container = screen.getByRole('button').parentElement
|
||||
|
||||
if (container) await user.click(container)
|
||||
|
||||
// 2. Assert that both were called
|
||||
expect(stopPropagationSpy).toHaveBeenCalled()
|
||||
expect(stopImmediatePropagationSpy).toHaveBeenCalled()
|
||||
|
||||
// 3. Clean up spies (Good practice to avoid interfering with other tests)
|
||||
stopPropagationSpy.mockRestore()
|
||||
stopImmediatePropagationSpy.mockRestore()
|
||||
expect(onOpenLog).toHaveBeenCalledExactlyOnceWith(logItem)
|
||||
})
|
||||
})
|
||||
@@ -3,33 +3,24 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { RiFileList3Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
type LogProps = {
|
||||
logItem: IChatItem
|
||||
onOpenLog: (item: IChatItem) => void
|
||||
}
|
||||
const Log: FC<LogProps> = ({ logItem }) => {
|
||||
const Log: FC<LogProps> = ({ logItem, onOpenLog }) => {
|
||||
const { t } = useTranslation(['common'])
|
||||
const setCurrentLogItem = useAppStore((s) => s.setCurrentLogItem)
|
||||
const setShowPromptLogModal = useAppStore((s) => s.setShowPromptLogModal)
|
||||
const setShowAgentLogModal = useAppStore((s) => s.setShowAgentLogModal)
|
||||
const setShowMessageLogModal = useAppStore((s) => s.setShowMessageLogModal)
|
||||
const { workflow_run_id: runID, agent_thoughts } = logItem
|
||||
const isAgent = agent_thoughts && agent_thoughts.length > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setCurrentLogItem(logItem)
|
||||
if (runID) setShowMessageLogModal(true)
|
||||
else if (isAgent) setShowAgentLogModal(true)
|
||||
else setShowPromptLogModal(true)
|
||||
}}
|
||||
>
|
||||
<IconButton aria-label={t(($) => $['operation.log'], { ns: 'common' })}>
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs">
|
||||
<IconButton
|
||||
aria-label={t(($) => $['operation.log'], { ns: 'common' })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onOpenLog(logItem)
|
||||
}}
|
||||
>
|
||||
<RiFileList3Line aria-hidden="true" className="size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ChatItem } from '../types'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
type UseChatLayoutOptions = {
|
||||
chatList: ChatItem[]
|
||||
@@ -16,7 +16,6 @@ const setStyleValue = (
|
||||
}
|
||||
|
||||
export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutOptions) => {
|
||||
const [width, setWidth] = useState(0)
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chatContainerInnerRef = useRef<HTMLDivElement>(null)
|
||||
const chatFooterRef = useRef<HTMLDivElement>(null)
|
||||
@@ -40,11 +39,6 @@ export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutO
|
||||
}, [chatList.length])
|
||||
|
||||
const handleWindowResize = useCallback(() => {
|
||||
if (chatContainerRef.current) {
|
||||
const nextWidth = document.body.clientWidth - (chatContainerRef.current.clientWidth + 16) - 8
|
||||
setWidth((currentWidth) => (currentWidth === nextWidth ? currentWidth : nextWidth))
|
||||
}
|
||||
|
||||
if (chatContainerRef.current && chatFooterRef.current)
|
||||
setStyleValue(chatFooterRef.current, 'width', `${chatContainerRef.current.clientWidth}px`)
|
||||
|
||||
@@ -172,7 +166,6 @@ export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutO
|
||||
}, [handleWindowResize, sidebarCollapseState])
|
||||
|
||||
return {
|
||||
width,
|
||||
chatContainerRef,
|
||||
chatContainerInnerRef,
|
||||
chatFooterRef,
|
||||
|
||||
@@ -12,7 +12,6 @@ const renderWithContext = (children: React.ReactNode, isResponding: boolean = tr
|
||||
config={undefined}
|
||||
isResponding={isResponding}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
@@ -99,7 +98,6 @@ describe('ThinkBlock', () => {
|
||||
config={undefined}
|
||||
isResponding={true}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
@@ -129,7 +127,6 @@ describe('ThinkBlock', () => {
|
||||
config={undefined}
|
||||
isResponding={false}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
|
||||
@@ -17,7 +17,6 @@ const ThinkBlockDemo = ({ responding = false }: { responding?: boolean }) => {
|
||||
config={undefined}
|
||||
isResponding={isResponding}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { useEffect } from 'react'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import PromptLogModal from '.'
|
||||
|
||||
type PromptLogModalProps = React.ComponentProps<typeof PromptLogModal>
|
||||
@@ -26,18 +24,7 @@ const mockLogItem: IChatItem = {
|
||||
],
|
||||
}
|
||||
|
||||
const usePromptLogMocks = () => {
|
||||
useEffect(() => {
|
||||
useStore.getState().setCurrentLogItem(mockLogItem)
|
||||
return () => {
|
||||
useStore.getState().setCurrentLogItem(undefined)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
const PromptLogPreview = (props: PromptLogModalProps) => {
|
||||
usePromptLogMocks()
|
||||
|
||||
return (
|
||||
<div className="relative min-h-135 w-full bg-background-default-subtle p-6">
|
||||
<PromptLogModal {...props} currentLogItem={mockLogItem} />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import type { Shape } from '@/app/components/workflow/store/workflow'
|
||||
import { act, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import WorkflowPanel from '../workflow-panel'
|
||||
|
||||
@@ -13,23 +15,11 @@ type AppStoreState = {
|
||||
id?: string
|
||||
}
|
||||
}
|
||||
currentLogItem?: { id: string }
|
||||
setCurrentLogItem: (item?: { id: string }) => void
|
||||
showMessageLogModal: boolean
|
||||
setShowMessageLogModal: (show: boolean) => void
|
||||
currentLogModalActiveTab?: string
|
||||
}
|
||||
|
||||
type WorkflowStoreState = {
|
||||
historyWorkflowData?: Record<string, unknown>
|
||||
showDebugAndPreviewPanel: boolean
|
||||
showChatVariablePanel: boolean
|
||||
showGlobalVariablePanel: boolean
|
||||
}
|
||||
type WorkflowStoreState = Partial<Shape>
|
||||
|
||||
const mockUseIsChatMode = vi.fn()
|
||||
const mockSetCurrentLogItem = vi.fn()
|
||||
const mockSetShowMessageLogModal = vi.fn()
|
||||
|
||||
let appStoreState: AppStoreState
|
||||
let workflowStoreState: WorkflowStoreState
|
||||
@@ -38,10 +28,6 @@ vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: <T,>(selector: (state: AppStoreState) => T) => selector(appStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: <T,>(selector: (state: WorkflowStoreState) => T) => selector(workflowStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/panel', () => ({
|
||||
default: ({
|
||||
components,
|
||||
@@ -88,26 +74,9 @@ vi.mock('next/dynamic', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/message-log-modal', () => ({
|
||||
default: ({
|
||||
currentLogItem,
|
||||
defaultTab,
|
||||
onCancel,
|
||||
}: {
|
||||
currentLogItem?: { id: string }
|
||||
defaultTab?: string
|
||||
onCancel: () => void
|
||||
}) => (
|
||||
<div
|
||||
data-testid="message-log-modal"
|
||||
data-current-log-id={currentLogItem?.id ?? ''}
|
||||
data-default-tab={defaultTab ?? ''}
|
||||
>
|
||||
<button type="button" onClick={onCancel}>
|
||||
close-message-log
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
vi.mock('@/service/log', () => ({
|
||||
fetchRunDetail: vi.fn().mockResolvedValue(undefined),
|
||||
fetchTracingList: vi.fn().mockResolvedValue({ data: [] }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/panel/record', () => ({
|
||||
@@ -149,11 +118,6 @@ describe('WorkflowPanel', () => {
|
||||
id: 'workflow-version-id',
|
||||
},
|
||||
},
|
||||
currentLogItem: { id: 'log-1' },
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
showMessageLogModal: false,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
currentLogModalActiveTab: 'detail',
|
||||
}
|
||||
workflowStoreState = {
|
||||
historyWorkflowData: undefined,
|
||||
@@ -165,7 +129,7 @@ describe('WorkflowPanel', () => {
|
||||
})
|
||||
|
||||
it('should configure workflow version history urls and latest version id for the panel shell', async () => {
|
||||
render(<WorkflowPanel />)
|
||||
renderWorkflowComponent(<WorkflowPanel />, { initialStoreState: workflowStoreState })
|
||||
|
||||
const panel = await screen.findByTestId('panel')
|
||||
expect(panel).toHaveAttribute('data-version-list-url', '/apps/app-123/workflows')
|
||||
@@ -179,37 +143,50 @@ describe('WorkflowPanel', () => {
|
||||
expect(panel).toHaveAttribute('data-app-mode', AppModeEnum.WORKFLOW)
|
||||
})
|
||||
|
||||
it('should render and close the message log modal from the left panel slot', async () => {
|
||||
it('should close the selected log and reopen on the default detail tab', async () => {
|
||||
await import('@/app/components/base/message-log-modal')
|
||||
const user = userEvent.setup()
|
||||
appStoreState = {
|
||||
...appStoreState,
|
||||
showMessageLogModal: true,
|
||||
const messageLogItem = {
|
||||
id: 'log-1',
|
||||
content: 'Answer',
|
||||
isAnswer: true,
|
||||
workflow_run_id: 'run-1',
|
||||
}
|
||||
|
||||
render(<WorkflowPanel />)
|
||||
|
||||
expect(await screen.findByTestId('message-log-modal')).toHaveAttribute(
|
||||
'data-current-log-id',
|
||||
'log-1',
|
||||
const { store } = renderWorkflowComponent(<WorkflowPanel />, {
|
||||
initialStoreState: { messageLogItem },
|
||||
})
|
||||
expect(await screen.findByRole('tab', { name: 'runLog.detail' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
)
|
||||
await user.click(screen.getByRole('tab', { name: 'runLog.tracing' }))
|
||||
expect(screen.getByRole('tab', { name: 'runLog.tracing' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('tab', { name: 'runLog.detail' })).not.toBeInTheDocument(),
|
||||
)
|
||||
act(() => store.getState().setMessageLogItem(messageLogItem))
|
||||
expect(await screen.findByRole('tab', { name: 'runLog.detail' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
)
|
||||
expect(screen.getByTestId('message-log-modal')).toHaveAttribute('data-default-tab', 'detail')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /close-message-log/i }))
|
||||
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledWith()
|
||||
expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should switch right-side workflow panels based on chat mode and workflow state', async () => {
|
||||
workflowStoreState = {
|
||||
historyWorkflowData: { id: 'history-1' },
|
||||
historyWorkflowData: { id: 'history-1', status: 'succeeded' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
showChatVariablePanel: true,
|
||||
showGlobalVariablePanel: true,
|
||||
}
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
|
||||
const { unmount } = render(<WorkflowPanel />)
|
||||
const { unmount } = renderWorkflowComponent(<WorkflowPanel />, {
|
||||
initialStoreState: workflowStoreState,
|
||||
})
|
||||
|
||||
expect(await screen.findByTestId('chat-record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('debug-and-preview-panel')).toBeInTheDocument()
|
||||
@@ -220,7 +197,7 @@ describe('WorkflowPanel', () => {
|
||||
|
||||
unmount()
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
render(<WorkflowPanel />)
|
||||
renderWorkflowComponent(<WorkflowPanel />, { initialStoreState: workflowStoreState })
|
||||
|
||||
expect(await screen.findByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-preview-panel')).toBeInTheDocument()
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import type { HeaderProps } from '@/app/components/workflow/header'
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Header from '@/app/components/workflow/header'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { useIsChatMode } from '../../hooks/use-is-chat-mode'
|
||||
import ChatVariableTrigger from './chat-variable-trigger'
|
||||
import FeaturesTrigger from './features-trigger'
|
||||
|
||||
const WorkflowHeader = () => {
|
||||
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(
|
||||
useShallow((state) => ({
|
||||
appDetail: state.appDetail,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
})),
|
||||
)
|
||||
const appDetail = useAppStore((state) => state.appDetail)
|
||||
const setMessageLogItem = useStore((state) => state.setMessageLogItem)
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const handleClearLogAndMessageModal = useCallback(() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}, [setCurrentLogItem, setShowMessageLogModal])
|
||||
setMessageLogItem(undefined)
|
||||
}, [setMessageLogItem])
|
||||
|
||||
const viewHistoryProps = useMemo(() => {
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { PanelProps } from '@/app/components/workflow/panel'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Panel from '@/app/components/workflow/panel'
|
||||
import CommentsPanel from '@/app/components/workflow/panel/comments-panel'
|
||||
@@ -37,33 +36,16 @@ const GlobalVariablePanel = dynamic(
|
||||
)
|
||||
|
||||
const WorkflowPanelOnLeft = () => {
|
||||
const {
|
||||
currentLogItem,
|
||||
setCurrentLogItem,
|
||||
showMessageLogModal,
|
||||
setShowMessageLogModal,
|
||||
currentLogModalActiveTab,
|
||||
} = useAppStore(
|
||||
useShallow((state) => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
currentLogModalActiveTab: state.currentLogModalActiveTab,
|
||||
})),
|
||||
)
|
||||
const messageLogItem = useStore((state) => state.messageLogItem)
|
||||
const setMessageLogItem = useStore((state) => state.setMessageLogItem)
|
||||
return (
|
||||
<>
|
||||
{showMessageLogModal && (
|
||||
{messageLogItem && (
|
||||
<MessageLogModal
|
||||
fixedWidth
|
||||
width={400}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
defaultTab={currentLogModalActiveTab}
|
||||
currentLogItem={messageLogItem}
|
||||
onCancel={() => setMessageLogItem(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { act, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
|
||||
import ViewWorkflowHistory from '../view-workflow-history'
|
||||
|
||||
describe('ViewWorkflowHistory log session', () => {
|
||||
it('clears the selected message when history opens and closes with Escape', async () => {
|
||||
const user = userEvent.setup()
|
||||
const messageLogItem = {
|
||||
id: 'message-1',
|
||||
content: 'Answer',
|
||||
isAnswer: true,
|
||||
workflow_run_id: 'run-1',
|
||||
}
|
||||
const { store } = renderWorkflowFlowComponent(<ViewWorkflowHistory />, {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
initialStoreState: { messageLogItem },
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'workflowHistory.changeHistory.title' }))
|
||||
expect(store.getState().messageLogItem).toBeUndefined()
|
||||
act(() => store.getState().setMessageLogItem(messageLogItem))
|
||||
await user.keyboard('{Escape}')
|
||||
expect(store.getState().messageLogItem).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -5,13 +5,12 @@ import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@langgeni
|
||||
import { Separator } from '@langgenius/dify-ui/separator'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import { useCollaborativeWorkflow } from '../hooks/use-collaborative-workflow'
|
||||
import { useNodesReadOnly } from '../hooks/use-workflow'
|
||||
import { useWorkflowHistory } from '../hooks/use-workflow-history'
|
||||
import TipPopup from '../operator/tip-popup'
|
||||
import { useStore } from '../store'
|
||||
|
||||
type ChangeHistoryEntry = {
|
||||
label: string
|
||||
@@ -30,13 +29,7 @@ const ViewWorkflowHistory = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { setCurrentLogItem, setShowMessageLogModal } = useAppStore(
|
||||
useShallow((state) => ({
|
||||
appDetail: state.appDetail,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
})),
|
||||
)
|
||||
const setMessageLogItem = useStore((state) => state.setMessageLogItem)
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
const { store, getHistoryLabel } = useWorkflowHistory()
|
||||
|
||||
@@ -143,6 +136,7 @@ const ViewWorkflowHistory = () => {
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (nodesReadOnly) return
|
||||
setMessageLogItem(undefined)
|
||||
setOpen(nextOpen)
|
||||
}}
|
||||
>
|
||||
@@ -162,11 +156,6 @@ const ViewWorkflowHistory = () => {
|
||||
focusableWhenDisabled
|
||||
aria-label={t(($) => $['changeHistory.title'], { ns: 'workflowHistory' })}
|
||||
className="rounded-md"
|
||||
onClick={() => {
|
||||
if (nodesReadOnly) return
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
>
|
||||
<span aria-hidden className="i-ri-history-line size-4 shrink-0" />
|
||||
</IconButton>
|
||||
@@ -190,10 +179,6 @@ const ViewWorkflowHistory = () => {
|
||||
<span aria-hidden className="i-ri-close-line size-4 text-text-secondary" />
|
||||
</IconButton>
|
||||
}
|
||||
onClick={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
+10
-9
@@ -16,7 +16,6 @@ const mockSetSettingsDestination = vi.fn()
|
||||
const mockHandleSingleRun = vi.fn()
|
||||
const mockHandleStop = vi.fn()
|
||||
const mockHandleRunWithParams = vi.fn()
|
||||
let mockShowMessageLogModal = false
|
||||
let mockNodesReadOnly = false
|
||||
let mockCanRun = true
|
||||
let mockBuiltInTools = [
|
||||
@@ -67,11 +66,8 @@ const mockLastRunState = {
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (
|
||||
selector: (state: { showMessageLogModal: boolean; appDetail: { id: string } }) => unknown,
|
||||
) =>
|
||||
useStore: (selector: (state: { appDetail: { id: string } }) => unknown) =>
|
||||
selector({
|
||||
showMessageLogModal: mockShowMessageLogModal,
|
||||
appDetail: { id: 'app-1' },
|
||||
}),
|
||||
}))
|
||||
@@ -386,7 +382,6 @@ const createData = (overrides: Record<string, unknown> = {}) => ({
|
||||
describe('workflow-panel index', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockShowMessageLogModal = false
|
||||
mockNodesReadOnly = false
|
||||
mockCanRun = true
|
||||
mockBuiltInTools = [
|
||||
@@ -744,9 +739,7 @@ describe('workflow-panel index', () => {
|
||||
})
|
||||
|
||||
it('should stop a running node and offset when the log modal is visible', () => {
|
||||
mockShowMessageLogModal = true
|
||||
|
||||
const { container } = renderWorkflowComponent(
|
||||
const { container, store } = renderWorkflowComponent(
|
||||
<BasePanel
|
||||
id="node-1"
|
||||
data={createData({ _singleRunningStatus: NodeRunningStatus.Running }) as never}
|
||||
@@ -757,6 +750,12 @@ describe('workflow-panel index', () => {
|
||||
initialStoreState: {
|
||||
nodePanelWidth: 480,
|
||||
otherPanelWidth: 240,
|
||||
messageLogItem: {
|
||||
id: 'log-1',
|
||||
isAnswer: true,
|
||||
content: 'answer',
|
||||
workflow_run_id: 'run-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -770,6 +769,8 @@ describe('workflow-panel index', () => {
|
||||
)
|
||||
|
||||
expect(mockHandleStop).toHaveBeenCalledTimes(1)
|
||||
act(() => store.getState().setMessageLogItem(undefined))
|
||||
expect(root.style.right).toBe('0px')
|
||||
})
|
||||
|
||||
it('should resize the node panel with the keyboard, persist its width, and allow focus to leave', async () => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { cloneElement, memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import ResizeHandle from '@/app/components/base/resize-handle'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
@@ -104,11 +103,7 @@ const BasePanel: FC<BasePanelProps> = ({ id, data, children }) => {
|
||||
})
|
||||
const canEdit = useHooksStore((s) => s.accessControl.canEdit)
|
||||
const { isConnected, nodePanelPresence } = useCollaboration(appId as string, canEdit)
|
||||
const { showMessageLogModal } = useAppStore(
|
||||
useShallow((state) => ({
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
})),
|
||||
)
|
||||
const messageLogItem = useStore((state) => state.messageLogItem)
|
||||
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
|
||||
|
||||
const currentUserPresence = useMemo(() => {
|
||||
@@ -519,7 +514,7 @@ const BasePanel: FC<BasePanelProps> = ({ id, data, children }) => {
|
||||
const singleRunActionLabel = isSingleRunning
|
||||
? t(($) => $['debug.variableInspect.trigger.stop'], { ns: 'workflowDebug' })
|
||||
: runThisStepLabel
|
||||
const nodePanelRightOffset = !showMessageLogModal ? '4px' : `${otherPanelWidth + 8}px`
|
||||
const nodePanelRightOffset = !messageLogItem ? '4px' : `${otherPanelWidth + 8}px`
|
||||
const isStartPlaceholderPanel = data.type === BlockEnum.StartPlaceholder
|
||||
const panelChildren = cloneElement(children as any, {
|
||||
id,
|
||||
@@ -549,12 +544,12 @@ const BasePanel: FC<BasePanelProps> = ({ id, data, children }) => {
|
||||
<div
|
||||
className={cn(
|
||||
'relative mr-1 h-full',
|
||||
showMessageLogModal &&
|
||||
messageLogItem &&
|
||||
'absolute z-0 mr-2 w-100 overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
|
||||
)}
|
||||
style={
|
||||
{
|
||||
right: !showMessageLogModal ? '0' : `${otherPanelWidth}px`,
|
||||
right: !messageLogItem ? '0' : `${otherPanelWidth}px`,
|
||||
'--workflow-node-panel-right': nodePanelRightOffset,
|
||||
} as CSSProperties
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ describe('ChatRecord', () => {
|
||||
expect(screen.getByText('Question 3')).toBeInTheDocument()
|
||||
expect(screen.getByText('Answer 3')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Question 2')).not.toBeInTheDocument()
|
||||
expect(screen.queryAllByRole('button', { name: 'common.operation.log' })).toHaveLength(0)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Previous' }))
|
||||
|
||||
@@ -91,7 +92,14 @@ describe('ChatRecord', () => {
|
||||
|
||||
mockFetchConversationMessages.mockResolvedValue({
|
||||
data: [
|
||||
{ id: 'msg-1', query: 'Question 1', answer: 'Answer 1', metadata: {}, message_files: [] },
|
||||
{
|
||||
id: 'msg-1',
|
||||
query: 'Question 1',
|
||||
answer: 'Answer 1',
|
||||
metadata: {},
|
||||
message_files: [],
|
||||
workflow_run_id: 'run-1',
|
||||
},
|
||||
],
|
||||
} as never)
|
||||
|
||||
@@ -101,11 +109,14 @@ describe('ChatRecord', () => {
|
||||
})
|
||||
|
||||
await screen.findByText('Question 1')
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.log' }))
|
||||
expect(store.getState().messageLogItem?.workflow_run_id).toBe('run-1')
|
||||
|
||||
await user.click(container.querySelector('.size-6.cursor-pointer') as HTMLElement)
|
||||
|
||||
expect(handleLoadBackupDraft).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState().historyWorkflowData).toBeUndefined()
|
||||
expect(store.getState().messageLogItem).toBeUndefined()
|
||||
})
|
||||
|
||||
it('stops loading when conversation fetching fails', async () => {
|
||||
|
||||
@@ -53,6 +53,7 @@ const ChatRecord = () => {
|
||||
const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
|
||||
const appDetail = useAppStore((s) => s.appDetail)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const setMessageLogItem = useStore((state) => state.setMessageLogItem)
|
||||
const { handleLoadBackupDraft } = useWorkflowRun()
|
||||
const historyWorkflowData = useStore((s) => s.historyWorkflowData)
|
||||
const currentConversationID = historyWorkflowData?.conversation_id
|
||||
@@ -106,7 +107,10 @@ const ChatRecord = () => {
|
||||
className="flex size-6 cursor-pointer items-center justify-center"
|
||||
onClick={() => {
|
||||
handleLoadBackupDraft()
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
workflowStore.setState({
|
||||
historyWorkflowData: undefined,
|
||||
messageLogItem: undefined,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<RiCloseLine className="size-4 text-text-tertiary" />
|
||||
@@ -128,7 +132,8 @@ const ChatRecord = () => {
|
||||
chatNode={<UserInput />}
|
||||
noChatInput
|
||||
allToolIcons={{}}
|
||||
showPromptLog
|
||||
onOpenLog={setMessageLogItem}
|
||||
canOpenLog={(item) => !!item.workflow_run_id}
|
||||
switchSibling={switchSibling}
|
||||
noSpacing
|
||||
chatAnswerContainerInner="pr-2!"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChatWrapperRefType } from '../index'
|
||||
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types'
|
||||
import { act, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -29,6 +30,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
onHumanInputFormSubmit,
|
||||
onFeatureBarClick,
|
||||
speechToTextTarget,
|
||||
onOpenLog,
|
||||
}: {
|
||||
chatNode: React.ReactNode
|
||||
inputDisabled?: boolean
|
||||
@@ -46,6 +48,7 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
) => Promise<void>
|
||||
onFeatureBarClick?: (state: boolean) => void
|
||||
speechToTextTarget?: SpeechToTextTarget
|
||||
onOpenLog?: (item: IChatItem) => void
|
||||
}) => (
|
||||
<div
|
||||
data-testid="chat-shell"
|
||||
@@ -85,6 +88,13 @@ vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
<button type="button" onClick={() => onFeatureBarClick?.(true)}>
|
||||
open-feature-panel
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onOpenLog?.({ id: 'log-1', isAnswer: true, content: 'answer', workflow_run_id: 'run-1' })
|
||||
}
|
||||
>
|
||||
open-log
|
||||
</button>
|
||||
{chatNode}
|
||||
</div>
|
||||
),
|
||||
@@ -208,11 +218,15 @@ describe('ChatWrapper', () => {
|
||||
})
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'open-log' }))
|
||||
expect(store.getState().messageLogItem?.workflow_run_id).toBe('run-1')
|
||||
|
||||
act(() => {
|
||||
chatRef.current?.handleRestart()
|
||||
})
|
||||
|
||||
expect(chatState.handleRestart).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState().messageLogItem).toBeUndefined()
|
||||
expect(store.getState().inputs).toEqual({
|
||||
name: 'Ada',
|
||||
})
|
||||
|
||||
@@ -40,6 +40,7 @@ const ChatWrapper = ({
|
||||
const startVariables = startNode?.data.variables
|
||||
const appDetail = useAppStore((s) => s.appDetail)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const setMessageLogItem = useStore((state) => state.setMessageLogItem)
|
||||
const inputs = useStore((s) => s.inputs)
|
||||
const setInputs = useStore((s) => s.setInputs)
|
||||
|
||||
@@ -101,8 +102,9 @@ const ChatWrapper = ({
|
||||
|
||||
const handleRestartChat = useCallback(() => {
|
||||
handleRestart()
|
||||
setMessageLogItem(undefined)
|
||||
setInputs(initialInputs)
|
||||
}, [handleRestart, setInputs, initialInputs])
|
||||
}, [handleRestart, setInputs, initialInputs, setMessageLogItem])
|
||||
|
||||
const doSend: OnSend = useCallback(
|
||||
(message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
|
||||
@@ -223,7 +225,8 @@ const ChatWrapper = ({
|
||||
}
|
||||
noSpacing
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
showPromptLog
|
||||
onOpenLog={setMessageLogItem}
|
||||
canOpenLog={(item) => !!item.workflow_run_id}
|
||||
chatAnswerContainerInner="pr-2!"
|
||||
switchSibling={doSwitchSibling}
|
||||
inputDisabled={inputDisabled}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
|
||||
export type WorkflowContextMenuTarget =
|
||||
| { type: 'panel' }
|
||||
@@ -7,6 +8,8 @@ export type WorkflowContextMenuTarget =
|
||||
| { type: 'edge'; edgeId: string }
|
||||
|
||||
export type PanelSliceShape = {
|
||||
messageLogItem?: IChatItem & { workflow_run_id: string }
|
||||
setMessageLogItem: (item?: IChatItem) => void
|
||||
panelWidth: number
|
||||
setPanelWidth: (width: number) => void
|
||||
showFeaturesPanel: boolean
|
||||
@@ -34,6 +37,16 @@ export type PanelSliceShape = {
|
||||
}
|
||||
|
||||
export const createPanelSlice: StateCreator<PanelSliceShape> = (set) => ({
|
||||
messageLogItem: undefined,
|
||||
setMessageLogItem: (item) => {
|
||||
if (!item) {
|
||||
set({ messageLogItem: undefined })
|
||||
return
|
||||
}
|
||||
if (item.workflow_run_id) {
|
||||
set({ messageLogItem: { ...item, workflow_run_id: item.workflow_run_id } })
|
||||
}
|
||||
},
|
||||
panelWidth: 420,
|
||||
setPanelWidth: (width) =>
|
||||
set((state) => (state.panelWidth === width ? state : { panelWidth: width })),
|
||||
|
||||
@@ -36,6 +36,7 @@ import { AppModeEnum, ModelModeType, Resolution, RETRIEVE_TYPE, TransferMethod }
|
||||
type IDebugConfiguration = {
|
||||
readonly?: boolean
|
||||
canTestAndRun?: boolean
|
||||
onOpenFeatures: () => void
|
||||
appId: string
|
||||
isTrailFinished: boolean
|
||||
mode: AppModeEnum
|
||||
@@ -119,6 +120,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
||||
readonly: false,
|
||||
canTestAndRun: false,
|
||||
appId: '',
|
||||
onOpenFeatures: noop,
|
||||
isTrailFinished: false,
|
||||
mode: AppModeEnum.CHAT,
|
||||
modelModeType: ModelModeType.chat,
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import type { AgentPreviewChatController } from '../chat-conversation'
|
||||
import type { AgentChatRuntimeEmptyStateProps } from '../chat-runtime'
|
||||
import type { ChatProps } from '@/app/components/base/chat/chat'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types'
|
||||
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
@@ -62,7 +63,7 @@ vi.mock('next/dynamic', async () => {
|
||||
onStopResponding: () => void
|
||||
sendButtonLabel?: string
|
||||
sendButtonLoading?: boolean
|
||||
showPromptLog?: boolean
|
||||
onOpenLog?: ChatProps['onOpenLog']
|
||||
footerNotice?: string
|
||||
chatNode?: ReactNode
|
||||
chatList?: Array<{
|
||||
@@ -89,7 +90,7 @@ vi.mock('next/dynamic', async () => {
|
||||
data-send-button-label={props.sendButtonLabel ?? ''}
|
||||
data-send-button-loading={String(!!props.sendButtonLoading)}
|
||||
data-answer-action-position={props.answerActionPosition ?? 'auto'}
|
||||
data-show-prompt-log={String(!!props.showPromptLog)}
|
||||
data-can-open-log={String(!!props.onOpenLog)}
|
||||
data-footer-notice={props.footerNotice ?? ''}
|
||||
data-no-chat-input={String(!!props.noChatInput)}
|
||||
data-show-regenerate={String(!!props.showRegenerate)}
|
||||
@@ -1320,7 +1321,7 @@ describe('AgentPreviewChat', () => {
|
||||
await waitFor(() => expect(screen.getByRole('region', { name: 'chat' })).toBeInTheDocument())
|
||||
|
||||
expect(screen.getByRole('region', { name: 'chat' })).toHaveAttribute(
|
||||
'data-show-prompt-log',
|
||||
'data-can-open-log',
|
||||
'false',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -147,7 +147,6 @@ export function AgentLogDetailPanel({
|
||||
chatList={chatList}
|
||||
noChatInput
|
||||
hideProcessDetail
|
||||
hideLogModal
|
||||
chatContainerInnerClassName="px-3"
|
||||
onFeedback={handleFeedback}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user