chore: only show debug view because of backend not support preveiew chat

This commit is contained in:
Joel 2026-06-24 17:36:02 +08:00
parent 865426f2f5
commit d96ba750db
6 changed files with 242 additions and 134 deletions

View File

@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'
import { AgentConfigurePage } from '../page'
const mocks = vi.hoisted(() => ({
refreshDebugConversation: vi.fn(),
queryState: {
agent: {
data: {
@ -65,6 +66,16 @@ vi.mock('@/service/client', () => ({
queryOptions: () => ({ queryKey: ['agent'] }),
queryKey: () => ['agent'],
},
debugConversation: {
refresh: {
post: {
mutationOptions: (options?: { onSuccess?: (data: { debug_conversation_id: string }) => void }) => ({
mutationFn: mocks.refreshDebugConversation,
...options,
}),
},
},
},
composer: {
get: {
queryOptions: () => ({ queryKey: ['composer'] }),
@ -101,17 +112,29 @@ vi.mock('../components/orchestrate', () => ({
}))
vi.mock('../components/preview/build-chat', () => ({
AgentBuildChat: () => (
<div role="region" aria-label="preview-chat">
build
AgentBuildChat: (props: {
conversationId?: string | null
onConversationIdChange?: (conversationId: string) => void
}) => (
<div role="region" aria-label="build-chat">
<span>{`build:${props.conversationId ?? 'none'}`}</span>
<button type="button" onClick={() => props.onConversationIdChange?.('build-conversation-new')}>
save build conversation
</button>
</div>
),
}))
vi.mock('../components/preview/preview-chat', () => ({
AgentPreviewChat: () => (
AgentPreviewChat: (props: {
conversationId?: string | null
onConversationIdChange?: (conversationId: string) => void
}) => (
<div role="region" aria-label="preview-chat">
preview
<span>{`preview:${props.conversationId ?? 'none'}`}</span>
<button type="button" onClick={() => props.onConversationIdChange?.('preview-conversation-new')}>
save preview conversation
</button>
</div>
),
}))
@ -123,15 +146,19 @@ vi.mock('../components/preview/chat-features-panel', () => ({
vi.mock('../components/preview/header', () => ({
AgentPreviewHeader: (props: {
mode: 'build' | 'preview'
previewEnabled: boolean
onModeChange: (mode: 'build' | 'preview') => void
onRestart: () => void
onRefresh: () => void
}) => (
<div>
<div>{props.mode}</div>
<button type="button" onClick={() => props.onModeChange('preview')}>
<button type="button" disabled={!props.previewEnabled} onClick={() => props.onModeChange('preview')}>
preview mode
</button>
<button type="button" onClick={props.onRestart}>
<button type="button" onClick={() => props.onModeChange('build')}>
build mode
</button>
<button type="button" onClick={props.onRefresh}>
restart preview
</button>
</div>
@ -147,6 +174,9 @@ vi.mock('../components/preview/versions-panel', () => ({
describe('AgentConfigurePage', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.refreshDebugConversation.mockResolvedValue({
debug_conversation_id: 'debug-conversation-new',
})
mocks.queryState.agent = {
data: {
icon: 'agent',
@ -189,7 +219,7 @@ describe('AgentConfigurePage', () => {
})
describe('Right panel mode', () => {
it('should render build mode by default and switch to preview mode', async () => {
it('should keep preview disabled and stay in build mode', async () => {
const user = userEvent.setup()
const queryClient = new QueryClient()
mocks.queryState.composer = {
@ -205,11 +235,59 @@ describe('AgentConfigurePage', () => {
</QueryClientProvider>,
)
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent('build')
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:debug-conversation-old')
expect(screen.queryByRole('region', { name: 'preview-chat' })).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'preview mode' }))
await user.click(screen.getByRole('button', { name: 'save build conversation' }))
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent('preview')
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:build-conversation-new')
expect(mocks.refreshDebugConversation).not.toHaveBeenCalled()
await user.click(screen.getByRole('button', { name: 'restart preview' }))
expect(mocks.refreshDebugConversation).toHaveBeenCalledWith({
params: {
agent_id: 'agent-1',
},
body: {
debug_conversation_id: 'build-conversation-new',
},
}, expect.any(Object))
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:none')
const previewButton = screen.getByRole('button', { name: 'preview mode' })
expect(previewButton).toBeDisabled()
await user.click(previewButton)
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:none')
expect(screen.queryByRole('region', { name: 'preview-chat' })).not.toBeInTheDocument()
})
it('should keep preview disabled', async () => {
const user = userEvent.setup()
const queryClient = new QueryClient()
mocks.queryState.composer = {
data: {},
isFetching: false,
isPending: false,
isSuccess: true,
}
render(
<QueryClientProvider client={queryClient}>
<AgentConfigurePage agentId="agent-1" />
</QueryClientProvider>,
)
const previewButton = screen.getByRole('button', { name: 'preview mode' })
expect(previewButton).toBeDisabled()
await user.click(previewButton)
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:debug-conversation-old')
expect(screen.queryByRole('region', { name: 'preview-chat', hidden: true })).not.toBeInTheDocument()
})
})
})

View File

@ -167,7 +167,7 @@ describe('AgentPreviewChat', () => {
})
renderPreviewChat({
debugConversationId: 'debug-conversation-1',
conversationId: 'debug-conversation-1',
})
await waitFor(() => expect(useChatMock).toHaveBeenCalled())

View File

@ -1,91 +1,59 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AgentPreviewHeader } from '../header'
const mocks = vi.hoisted(() => ({
refreshDebugConversation: vi.fn(),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
agent: {
byAgentId: {
get: {
queryKey: () => ['agent'],
},
debugConversation: {
refresh: {
post: {
mutationOptions: (options?: { onSuccess?: (data: { debug_conversation_id: string }) => void }) => ({
mutationFn: mocks.refreshDebugConversation,
...options,
}),
},
},
},
},
},
},
}))
function renderHeader({
mode = 'preview',
previewEnabled = true,
onModeChange = vi.fn(),
onOpenVersions = vi.fn(),
onRestart = vi.fn(),
onRefresh = vi.fn(),
}: {
mode?: 'build' | 'preview'
previewEnabled?: boolean
onModeChange?: (mode: 'build' | 'preview') => void
onOpenVersions?: () => void
onRestart?: () => void
onRefresh?: () => void
} = {}) {
const queryClient = new QueryClient()
queryClient.setQueryData(['agent'], {
debug_conversation_id: 'debug-conversation-old',
name: 'Research Agent',
})
render(
<QueryClientProvider client={queryClient}>
<AgentPreviewHeader
agentId="agent-1"
mode={mode}
isChatFeaturesOpen={false}
onModeChange={onModeChange}
onToggleChatFeatures={vi.fn()}
onOpenVersions={onOpenVersions}
onRestart={onRestart}
/>
</QueryClientProvider>,
<AgentPreviewHeader
mode={mode}
previewEnabled={previewEnabled}
isChatFeaturesOpen={false}
onModeChange={onModeChange}
onToggleChatFeatures={vi.fn()}
onOpenVersions={onOpenVersions}
onRefresh={onRefresh}
/>,
)
return {
queryClient,
}
}
describe('AgentPreviewHeader', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.refreshDebugConversation.mockResolvedValue({
debug_conversation_id: 'debug-conversation-new',
})
})
it('should refresh debug conversation before clearing preview chat', async () => {
const onRestart = vi.fn()
const { queryClient } = renderHeader({ onRestart })
it('should emit refresh from the restart button', async () => {
const user = userEvent.setup()
const onRefresh = vi.fn()
renderHeader({ mode: 'build', onRefresh })
fireEvent.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.preview.restart' }))
await user.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.preview.restart' }))
await waitFor(() => expect(mocks.refreshDebugConversation).toHaveBeenCalledWith({
params: {
agent_id: 'agent-1',
},
}, expect.any(Object)))
expect(queryClient.getQueryData(['agent'])).toEqual(expect.objectContaining({
debug_conversation_id: 'debug-conversation-new',
}))
expect(onRestart).toHaveBeenCalledTimes(1)
expect(onRefresh).toHaveBeenCalledTimes(1)
})
it('should disable preview mode when preview is unavailable', async () => {
const user = userEvent.setup()
const onModeChange = vi.fn()
renderHeader({
mode: 'build',
previewEnabled: false,
onModeChange,
})
await user.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.rightPanel\.preview/ }))
expect(onModeChange).not.toHaveBeenCalled()
})
})

View File

@ -405,10 +405,11 @@ export type AgentChatRuntimeProps = {
agentName?: string
agentSoulConfig?: AgentSoulConfig
clearChatList: boolean
debugConversationId?: string | null
conversationId?: string | null
inputPlaceholder: string
renderEmptyState: (props: AgentChatRuntimeEmptyStateProps) => ReactNode
onClearChatListChange: (clearChatList: boolean) => void
onConversationIdChange?: (conversationId: string) => void
onSaveDraftBeforeRun?: () => Promise<void>
}
@ -420,23 +421,24 @@ export function AgentChatRuntime({
agentName,
agentSoulConfig,
clearChatList,
debugConversationId,
conversationId,
inputPlaceholder,
renderEmptyState,
onClearChatListChange,
onConversationIdChange,
onSaveDraftBeforeRun,
}: AgentChatRuntimeProps) {
const historyQuery = useQuery({
queryKey: ['agent-preview-debug-conversation', agentId, debugConversationId],
queryFn: () => fetchAgentConversationMessages(agentId, debugConversationId!),
enabled: !!debugConversationId,
queryKey: ['agent-chat-conversation-messages', agentId, conversationId],
queryFn: () => fetchAgentConversationMessages(agentId, conversationId!),
enabled: !!conversationId,
})
const initialChatTree = useMemo(
() => getFormattedAgentDebugChatTree(historyQuery.data?.data ?? []),
[historyQuery.data?.data],
)
if (debugConversationId && historyQuery.isPending) {
if (conversationId && historyQuery.isPending) {
return (
<div className="flex h-full items-center justify-center">
<Loading type="app" />
@ -446,7 +448,7 @@ export function AgentChatRuntime({
return (
<AgentPreviewChatSession
key={`${debugConversationId ?? 'new'}-${historyQuery.dataUpdatedAt}`}
key={`${conversationId ?? 'new'}-${historyQuery.dataUpdatedAt}`}
agentId={agentId}
agentIcon={agentIcon}
agentIconBackground={agentIconBackground}
@ -454,11 +456,12 @@ export function AgentChatRuntime({
agentName={agentName}
agentSoulConfig={agentSoulConfig}
clearChatList={clearChatList}
debugConversationId={debugConversationId}
conversationId={conversationId}
initialChatTree={initialChatTree}
inputPlaceholder={inputPlaceholder}
renderEmptyState={renderEmptyState}
onClearChatListChange={onClearChatListChange}
onConversationIdChange={onConversationIdChange}
onSaveDraftBeforeRun={onSaveDraftBeforeRun}
/>
)
@ -472,11 +475,12 @@ function AgentPreviewChatSession({
agentName,
agentSoulConfig,
clearChatList,
debugConversationId,
conversationId,
initialChatTree,
inputPlaceholder,
renderEmptyState,
onClearChatListChange,
onConversationIdChange,
onSaveDraftBeforeRun,
}: {
agentId: string
@ -486,11 +490,12 @@ function AgentPreviewChatSession({
agentName?: string
agentSoulConfig?: AgentSoulConfig
clearChatList: boolean
debugConversationId?: string | null
conversationId?: string | null
initialChatTree: ChatItemInTree[]
inputPlaceholder: string
renderEmptyState: (props: AgentChatRuntimeEmptyStateProps) => ReactNode
onClearChatListChange: (clearChatList: boolean) => void
onConversationIdChange?: (conversationId: string) => void
onSaveDraftBeforeRun?: () => Promise<void>
}) {
const { userProfile } = useAppContext()
@ -533,7 +538,7 @@ function AgentPreviewChatSession({
},
clearChatList,
onClearChatListChange,
debugConversationId ?? undefined,
conversationId ?? undefined,
)
const doSend: OnSend = useCallback(async (message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
@ -562,9 +567,10 @@ function AgentPreviewChatSession({
{
onGetConversationMessages: conversationId => fetchAgentConversationMessages(agentId, conversationId),
onGetSuggestedQuestions: responseItemId => fetchAgentSuggestedQuestions(agentId, responseItemId),
onConversationComplete: onConversationIdChange,
},
)
}, [agentId, chatList, config.model.name, config.model.provider, handleSend, inputs, onSaveDraftBeforeRun, textGenerationModelList])
}, [agentId, chatList, config.model.name, config.model.provider, handleSend, inputs, onConversationIdChange, onSaveDraftBeforeRun, textGenerationModelList])
const doRegenerate = useCallback((chatItem: ChatItem, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)

View File

@ -1,50 +1,29 @@
'use client'
import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/types.gen'
import { cn } from '@langgenius/dify-ui/cn'
import { SegmentedControl, SegmentedControlDivider, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
type AgentConfigureRightPanelMode = 'build' | 'preview'
export function AgentPreviewHeader({
agentId,
mode,
previewEnabled,
isChatFeaturesOpen,
onModeChange,
onToggleChatFeatures,
onOpenVersions,
onRestart,
onRefresh,
refreshDisabled,
}: {
agentId: string
mode: AgentConfigureRightPanelMode
previewEnabled: boolean
isChatFeaturesOpen: boolean
onModeChange: (mode: AgentConfigureRightPanelMode) => void
onToggleChatFeatures: () => void
onOpenVersions: () => void
onRestart: () => void
onRefresh: () => void
refreshDisabled?: boolean
}) {
const { t } = useTranslation('agentV2')
const queryClient = useQueryClient()
const refreshDebugConversationMutation = useMutation(consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions({
onSuccess: ({ debug_conversation_id }) => {
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: agentId } } }),
(agentDetail) => {
if (!agentDetail)
return agentDetail
return {
...agentDetail,
debug_conversation_id,
}
},
)
onRestart()
},
}))
return (
<div className="flex h-12 shrink-0 items-center gap-3 py-2 pr-3 pl-4">
@ -53,7 +32,7 @@ export function AgentPreviewHeader({
value={[mode]}
onValueChange={(value) => {
const nextMode = value[0]
if (nextMode)
if (nextMode && (nextMode !== 'preview' || previewEnabled))
onModeChange(nextMode)
}}
aria-label={t('agentDetail.configure.rightPanel.modeLabel')}
@ -62,7 +41,11 @@ export function AgentPreviewHeader({
<span aria-hidden className="i-ri-hammer-line size-4" />
{t('agentDetail.configure.rightPanel.build')}
</SegmentedControlItem>
<SegmentedControlItem<AgentConfigureRightPanelMode> value="preview" className="uppercase">
<SegmentedControlItem<AgentConfigureRightPanelMode>
value="preview"
disabled={!previewEnabled}
className="uppercase"
>
<span aria-hidden className="i-custom-vender-other-replay-line size-4" />
{t('agentDetail.configure.rightPanel.preview')}
</SegmentedControlItem>
@ -72,12 +55,8 @@ export function AgentPreviewHeader({
<div className="flex shrink-0 items-center gap-1">
<button
type="button"
onClick={() => refreshDebugConversationMutation.mutate({
params: {
agent_id: agentId,
},
})}
disabled={refreshDebugConversationMutation.isPending}
onClick={onRefresh}
disabled={refreshDisabled}
className="flex size-6 items-center justify-center rounded-md p-0.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
aria-label={t('agentDetail.configure.preview.restart')}
>

View File

@ -1,11 +1,13 @@
'use client'
import type { AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentAppDetailWithSite, AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
import { useHydrateAgentSoulConfigDraft } from '@/features/agent-v2/agent-composer/store'
import { consoleQuery } from '@/service/client'
import { AgentOrchestratePanel } from './components/orchestrate'
import { AgentBuildChat } from './components/preview/build-chat'
import { AgentChatFeaturesPanel } from './components/preview/chat-features-panel'
@ -20,6 +22,15 @@ type AgentConfigurePageProps = {
}
type AgentConfigureRightPanelMode = 'build' | 'preview'
type AgentConfigureConversationIds = Record<AgentConfigureRightPanelMode, string | null>
type DebugConversationRefreshInput = {
params: {
agent_id: string
}
body: {
debug_conversation_id: string
}
}
export function AgentConfigurePage({
agentId,
@ -71,6 +82,7 @@ function AgentConfigurePageLoadedContent({
onSelectVersion: (versionId: string | null) => void
}) {
const { t } = useTranslation('agentV2')
const queryClient = useQueryClient()
const [showChatFeatures, setShowChatFeatures] = useState(false)
const [showPreviewVersions, setShowPreviewVersions] = useState(false)
const [clearPreviewChat, setClearPreviewChat] = useState(false)
@ -86,6 +98,57 @@ function AgentConfigurePageLoadedContent({
} = configureData
const agentIconType = agentQuery.data?.icon_type as AgentIconType | null | undefined
const isViewingVersion = !!selectedVersionId
const [conversationIds, setConversationIds] = useState<AgentConfigureConversationIds>({
build: agentQuery.data?.debug_conversation_id ?? null,
preview: null,
})
const rightPanelChatMode: AgentConfigureRightPanelMode = rightPanelMode === 'preview' ? 'build' : rightPanelMode
const refreshDebugConversationMutation = useMutation(consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions({
onSuccess: ({ debug_conversation_id }) => {
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: agentId } } }),
(agentDetail) => {
if (!agentDetail)
return agentDetail
return {
...agentDetail,
debug_conversation_id,
}
},
)
},
}))
const refreshDebugConversation = (conversationId: string) => {
const input: DebugConversationRefreshInput = {
params: {
agent_id: agentId,
},
body: {
debug_conversation_id: conversationId,
},
}
refreshDebugConversationMutation.mutate(
input as unknown as Parameters<typeof refreshDebugConversationMutation.mutate>[0],
)
}
const updateConversationId = (mode: AgentConfigureRightPanelMode, conversationId: string) => {
setConversationIds(current => ({
...current,
[mode]: conversationId,
}))
}
const restartCurrentChat = () => {
if (rightPanelChatMode === 'build')
refreshDebugConversation(conversationIds.build ?? '')
setConversationIds(current => ({
...current,
[rightPanelChatMode]: null,
}))
setClearPreviewChat(true)
}
useHydrateAgentSoulConfigDraft({
agentId,
@ -137,13 +200,14 @@ function AgentConfigurePageLoadedContent({
<div className="flex min-w-105 flex-1 gap-1 overflow-hidden">
<div className="flex min-w-105 flex-1 flex-col overflow-hidden rounded-lg bg-background-gradient-bg-fill-chat-bg-2 shadow-xl shadow-shadow-shadow-5">
<AgentPreviewHeader
agentId={agentId}
mode={rightPanelMode}
mode={rightPanelChatMode}
previewEnabled={false}
isChatFeaturesOpen={showChatFeatures}
onModeChange={setRightPanelMode}
onToggleChatFeatures={() => setShowChatFeatures(open => !open)}
onOpenVersions={() => setShowPreviewVersions(true)}
onRestart={() => setClearPreviewChat(true)}
onRefresh={restartCurrentChat}
refreshDisabled={refreshDebugConversationMutation.isPending}
/>
<div className="min-h-0 flex-1">
@ -155,9 +219,10 @@ function AgentConfigurePageLoadedContent({
agentName={agentQuery.data?.name}
agentSoulConfig={agentSoulConfig}
clearChatList={clearPreviewChat}
debugConversationId={agentQuery.data?.debug_conversation_id}
mode={rightPanelMode}
conversationIds={conversationIds}
mode={rightPanelChatMode}
onClearChatListChange={setClearPreviewChat}
onConversationIdChange={updateConversationId}
onSaveDraftBeforeRun={saveDraft}
/>
</div>
@ -184,25 +249,37 @@ function AgentConfigurePageLoadedContent({
function AgentRightPanelChatWithDraftConfig({
agentSoulConfig,
conversationIds,
mode,
onConversationIdChange,
...props
}: Omit<Parameters<typeof AgentPreviewChat>[0], 'agentSoulConfig'> & {
}: Omit<Parameters<typeof AgentPreviewChat>[0], 'agentSoulConfig' | 'conversationId' | 'onConversationIdChange'> & {
agentSoulConfig?: AgentSoulConfig
conversationIds: AgentConfigureConversationIds
mode: AgentConfigureRightPanelMode
onConversationIdChange: (mode: AgentConfigureRightPanelMode, conversationId: string) => void
}) {
const previewAgentSoulConfig = useAgentPreviewSoulConfig(agentSoulConfig)
const conversationId = conversationIds[mode]
const handleConversationIdChange = (newConversationId: string) => {
onConversationIdChange(mode, newConversationId)
}
return mode === 'build'
? (
<AgentBuildChat
{...props}
conversationId={conversationId}
agentSoulConfig={previewAgentSoulConfig}
onConversationIdChange={handleConversationIdChange}
/>
)
: (
<AgentPreviewChat
{...props}
conversationId={conversationId}
agentSoulConfig={previewAgentSoulConfig}
onConversationIdChange={handleConversationIdChange}
/>
)
}