diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx
index a06c04019da..84de0d50624 100644
--- a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx
+++ b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx
@@ -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: () => (
-
- build
+ AgentBuildChat: (props: {
+ conversationId?: string | null
+ onConversationIdChange?: (conversationId: string) => void
+ }) => (
+
+ {`build:${props.conversationId ?? 'none'}`}
+
),
}))
vi.mock('../components/preview/preview-chat', () => ({
- AgentPreviewChat: () => (
+ AgentPreviewChat: (props: {
+ conversationId?: string | null
+ onConversationIdChange?: (conversationId: string) => void
+ }) => (
- preview
+ {`preview:${props.conversationId ?? 'none'}`}
+
),
}))
@@ -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
}) => (
{props.mode}
-
@@ -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', () => {
,
)
- 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(
+
+
+ ,
+ )
+
+ 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()
})
})
})
diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx
index ee22c74e2cb..ace93799083 100644
--- a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx
@@ -167,7 +167,7 @@ describe('AgentPreviewChat', () => {
})
renderPreviewChat({
- debugConversationId: 'debug-conversation-1',
+ conversationId: 'debug-conversation-1',
})
await waitFor(() => expect(useChatMock).toHaveBeenCalled())
diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/header.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/header.spec.tsx
index a8c7d24f00c..efad6e38d07 100644
--- a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/header.spec.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/header.spec.tsx
@@ -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(
-
-
- ,
+
,
)
-
- 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()
})
})
diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx
index a3011d23c7b..f01066c415e 100644
--- a/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/preview/chat-runtime.tsx
@@ -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
}
@@ -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 (
@@ -446,7 +448,7 @@ export function AgentChatRuntime({
return (
)
@@ -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
}) {
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)
diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx
index 5a1d691143d..4c42afe35f2 100644
--- a/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx
@@ -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(
- consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: agentId } } }),
- (agentDetail) => {
- if (!agentDetail)
- return agentDetail
-
- return {
- ...agentDetail,
- debug_conversation_id,
- }
- },
- )
- onRestart()
- },
- }))
return (
@@ -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({
{t('agentDetail.configure.rightPanel.build')}
-
value="preview" className="uppercase">
+
+ value="preview"
+ disabled={!previewEnabled}
+ className="uppercase"
+ >
{t('agentDetail.configure.rightPanel.preview')}
@@ -72,12 +55,8 @@ export function AgentPreviewHeader({
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')}
>
diff --git a/web/features/agent-v2/agent-detail/configure/page.tsx b/web/features/agent-v2/agent-detail/configure/page.tsx
index eb2ffdcd1db..13c01cb400a 100644
--- a/web/features/agent-v2/agent-detail/configure/page.tsx
+++ b/web/features/agent-v2/agent-detail/configure/page.tsx
@@ -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
+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({
+ 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(
+ 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[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({
setShowChatFeatures(open => !open)}
onOpenVersions={() => setShowPreviewVersions(true)}
- onRestart={() => setClearPreviewChat(true)}
+ onRefresh={restartCurrentChat}
+ refreshDisabled={refreshDebugConversationMutation.isPending}
/>
@@ -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}
/>
@@ -184,25 +249,37 @@ function AgentConfigurePageLoadedContent({
function AgentRightPanelChatWithDraftConfig({
agentSoulConfig,
+ conversationIds,
mode,
+ onConversationIdChange,
...props
-}: Omit[0], 'agentSoulConfig'> & {
+}: Omit[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'
? (
)
: (
)
}