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({