mirror of
https://github.com/langgenius/dify.git
synced 2026-07-27 06:58:30 +08:00
chore: support preview mode if not in community version (#39399)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
db29caff2b
commit
34e2205efa
@ -320,6 +320,106 @@ describe('useChat', () => {
|
||||
})
|
||||
|
||||
describe('handleSend', () => {
|
||||
it('should complete with the conversation id from message_end', async () => {
|
||||
let callbacks: HookCallbacks
|
||||
const onGetConversationMessages = vi.fn().mockResolvedValue({ data: [] })
|
||||
const onConversationComplete = vi.fn()
|
||||
vi.mocked(ssePost).mockImplementation(async (_url, _params, options) => {
|
||||
callbacks = options as HookCallbacks
|
||||
})
|
||||
const { result } = renderHook(() =>
|
||||
useChat(undefined, undefined, undefined, undefined, undefined, undefined, undefined, {
|
||||
isNewAgent: true,
|
||||
}),
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend(
|
||||
'test-url',
|
||||
{ query: 'preview message' },
|
||||
{
|
||||
onGetConversationMessages,
|
||||
onConversationComplete,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
callbacks.onMessageEnd({
|
||||
id: 'preview-message',
|
||||
conversation_id: 'preview-conversation',
|
||||
metadata: {},
|
||||
})
|
||||
await callbacks.onCompleted()
|
||||
})
|
||||
|
||||
expect(onGetConversationMessages).toHaveBeenCalledWith(
|
||||
'preview-conversation',
|
||||
expect.any(Function),
|
||||
)
|
||||
expect(onConversationComplete).toHaveBeenCalledWith('preview-conversation', undefined)
|
||||
})
|
||||
|
||||
it('should send with the latest externally selected conversation', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ conversationId }) =>
|
||||
useChat(undefined, undefined, undefined, undefined, undefined, undefined, conversationId),
|
||||
{
|
||||
initialProps: {
|
||||
conversationId: 'build-conversation' as string | undefined,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
rerender({
|
||||
conversationId: 'preview-conversation',
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend('test-url', { query: 'preview message' }, {})
|
||||
})
|
||||
|
||||
expect(ssePost).toHaveBeenCalledWith(
|
||||
'test-url',
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
conversation_id: 'preview-conversation',
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it('should not reuse a previous conversation when the selected session has no id', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ conversationId }: { conversationId?: string }) =>
|
||||
useChat(undefined, undefined, undefined, undefined, undefined, undefined, conversationId),
|
||||
{
|
||||
initialProps: {
|
||||
conversationId: 'build-conversation' as string | undefined,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
rerender({
|
||||
conversationId: undefined,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleSend('test-url', { query: 'new preview message' }, {})
|
||||
})
|
||||
|
||||
expect(ssePost).toHaveBeenCalledWith(
|
||||
'test-url',
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
conversation_id: '',
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it('should block send if already responding', async () => {
|
||||
const { result } = renderHook(() => useChat())
|
||||
|
||||
|
||||
@ -198,6 +198,7 @@ vi.mock('react-i18next', async () => {
|
||||
})
|
||||
|
||||
type OperationProps = {
|
||||
answerActionPosition?: 'auto' | 'below'
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
@ -854,6 +855,12 @@ describe('Operation', () => {
|
||||
expect(bar.style.left).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should position below when requested even if there is room on the right', () => {
|
||||
renderOperation({ ...baseProps, answerActionPosition: 'below', maxSize: 500 })
|
||||
const bar = screen.getByTestId('operation-bar')
|
||||
expect(bar.style.left).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should calculate width correctly for all features combined', () => {
|
||||
mockContextValue.config = makeChatConfig({
|
||||
text_to_speech: { enabled: true },
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { ChatConfig, ChatItem } from '../../types'
|
||||
import type { HumanInputFormSubmitData } from './human-input-content/type'
|
||||
import type { AnswerActionPosition } from './operation'
|
||||
import type { AppData } from '@/models/share'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
@ -23,6 +24,7 @@ import SuggestedQuestions from './suggested-questions'
|
||||
import WorkflowProcessItem from './workflow-process'
|
||||
|
||||
type AnswerProps = {
|
||||
answerActionPosition?: AnswerActionPosition
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
@ -44,6 +46,7 @@ type AnswerProps = {
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: HumanInputFormSubmitData) => Promise<void>
|
||||
}
|
||||
const Answer: FC<AnswerProps> = ({
|
||||
answerActionPosition,
|
||||
item,
|
||||
question,
|
||||
index,
|
||||
@ -185,6 +188,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
>
|
||||
{!responding && contentIsEmpty && !hasAgentContent && (
|
||||
<Operation
|
||||
answerActionPosition={answerActionPosition}
|
||||
hasWorkflowProcess={!!workflowProcess}
|
||||
maxSize={containerWidth - humanInputFormContainerWidth - 4}
|
||||
contentWidth={humanInputFormContainerWidth}
|
||||
@ -245,6 +249,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
>
|
||||
{!responding && (
|
||||
<Operation
|
||||
answerActionPosition={answerActionPosition}
|
||||
hasWorkflowProcess={!!workflowProcess}
|
||||
maxSize={containerWidth - contentWidth - 4}
|
||||
contentWidth={contentWidth}
|
||||
@ -321,6 +326,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
>
|
||||
{!responding && (
|
||||
<Operation
|
||||
answerActionPosition={answerActionPosition}
|
||||
hasWorkflowProcess={!!workflowProcess}
|
||||
maxSize={containerWidth - contentWidth - 4}
|
||||
contentWidth={contentWidth}
|
||||
|
||||
@ -23,6 +23,7 @@ import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import { useChatContext } from '../context'
|
||||
|
||||
type OperationProps = {
|
||||
answerActionPosition?: AnswerActionPosition
|
||||
item: ChatItem
|
||||
question: string
|
||||
index: number
|
||||
@ -33,6 +34,8 @@ type OperationProps = {
|
||||
noChatInput?: boolean
|
||||
}
|
||||
|
||||
export type AnswerActionPosition = 'auto' | 'below'
|
||||
|
||||
type FeedbackTooltipProps = {
|
||||
content: ReactNode
|
||||
children: ReactElement
|
||||
@ -69,6 +72,7 @@ const FeedbackTooltip = ({ content, children }: FeedbackTooltipProps) => {
|
||||
}
|
||||
|
||||
function Operation({
|
||||
answerActionPosition = 'auto',
|
||||
item,
|
||||
question,
|
||||
index,
|
||||
@ -204,7 +208,10 @@ function Operation({
|
||||
showPromptLog,
|
||||
])
|
||||
|
||||
const positionRight = useMemo(() => operationWidth < maxSize, [operationWidth, maxSize])
|
||||
const positionRight = useMemo(
|
||||
() => answerActionPosition === 'auto' && operationWidth < maxSize,
|
||||
[answerActionPosition, operationWidth, maxSize],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -281,9 +281,9 @@ export const useChat = (
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
initialConversationIdRef.current = initialConversationId ?? ''
|
||||
if (initialConversationId && !conversationIdRef.current)
|
||||
conversationIdRef.current = initialConversationId
|
||||
const nextConversationId = initialConversationId ?? ''
|
||||
initialConversationIdRef.current = nextConversationId
|
||||
conversationIdRef.current = nextConversationId
|
||||
}, [initialConversationId])
|
||||
|
||||
/** Find the target node by bfs and then operate on it */
|
||||
@ -551,6 +551,8 @@ export const useChat = (
|
||||
})
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
if (options.isNewAgent && messageEnd.conversation_id)
|
||||
conversationIdRef.current = messageEnd.conversation_id
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (messageEnd.metadata?.annotation_reply) {
|
||||
responseItem.annotation = {
|
||||
@ -1199,6 +1201,8 @@ export const useChat = (
|
||||
})
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
if (options.isNewAgent && messageEnd.conversation_id)
|
||||
conversationIdRef.current = messageEnd.conversation_id
|
||||
if (messageEnd.metadata?.annotation_reply) {
|
||||
responseItem.id = messageEnd.id
|
||||
responseItem.annotation = {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC, ReactNode } from 'react'
|
||||
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
||||
import type { ChatConfig, ChatItem, Feedback, 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 { SpeechToTextTarget } from '@/app/components/base/voice-input/types'
|
||||
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
@ -22,6 +23,7 @@ import TryToAsk from './try-to-ask'
|
||||
import { useChatLayout } from './use-chat-layout'
|
||||
|
||||
export type ChatProps = {
|
||||
answerActionPosition?: AnswerActionPosition
|
||||
isTryApp?: boolean
|
||||
readonly?: boolean
|
||||
appData?: AppData
|
||||
@ -89,6 +91,7 @@ export type ChatProps = {
|
||||
}
|
||||
|
||||
const Chat: FC<ChatProps> = ({
|
||||
answerActionPosition,
|
||||
isTryApp,
|
||||
readonly = false,
|
||||
appData,
|
||||
@ -214,6 +217,7 @@ const Chat: FC<ChatProps> = ({
|
||||
const isLast = item.id === chatList.at(-1)?.id
|
||||
return (
|
||||
<Answer
|
||||
answerActionPosition={answerActionPosition}
|
||||
appData={appData}
|
||||
key={item.id}
|
||||
item={item}
|
||||
|
||||
@ -147,6 +147,7 @@ export type Metadata = {
|
||||
|
||||
export type MessageEnd = {
|
||||
id: string
|
||||
conversation_id: string
|
||||
metadata: Metadata
|
||||
files?: FileResponse[]
|
||||
}
|
||||
|
||||
@ -213,6 +213,26 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
|
||||
expect(callbacks.onSettled).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should treat unavailable workflow data as a skipped sync instead of an error', async () => {
|
||||
workflowStoreState = {
|
||||
...workflowStoreState,
|
||||
isWorkflowDataLoaded: false,
|
||||
}
|
||||
const callbacks = {
|
||||
onError: vi.fn(),
|
||||
onSettled: vi.fn(),
|
||||
}
|
||||
|
||||
const { result } = renderUseNodesSyncDraft()
|
||||
await act(async () => {
|
||||
await expect(result.current.doSyncWorkflowDraft(false, callbacks)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
expect(mockSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(callbacks.onError).not.toHaveBeenCalled()
|
||||
expect(callbacks.onSettled).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not include source_workflow_id in draft sync payloads', async () => {
|
||||
const { result } = renderUseNodesSyncDraft()
|
||||
|
||||
@ -568,16 +588,22 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
|
||||
mockCollaborationIsConnected.mockReturnValue(true)
|
||||
mockCollaborationGetIsLeader.mockReturnValue(true)
|
||||
mockCollaborationCanPersistLocalGraph.mockReturnValue(false)
|
||||
const callbacks = {
|
||||
onError: vi.fn(),
|
||||
onSettled: vi.fn(),
|
||||
}
|
||||
|
||||
const { result } = renderUseNodesSyncDraft()
|
||||
|
||||
let syncResult: unknown
|
||||
await act(async () => {
|
||||
syncResult = await result.current.doSyncWorkflowDraft(false)
|
||||
syncResult = await result.current.doSyncWorkflowDraft(false, callbacks)
|
||||
})
|
||||
|
||||
expect(syncResult).toBeNull()
|
||||
expect(mockSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(callbacks.onError).not.toHaveBeenCalled()
|
||||
expect(callbacks.onSettled).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should skip keepalive sync on page close when current user is collaboration follower', () => {
|
||||
|
||||
@ -151,14 +151,12 @@ const useNodesSyncDraftBase = (getNodesReadOnly: () => boolean) => {
|
||||
if (getNodesReadOnly()) return null
|
||||
|
||||
if (isCollaborationEnabled && !collaborationManager.canPersistLocalGraph()) {
|
||||
callback?.onError?.()
|
||||
callback?.onSettled?.()
|
||||
return null
|
||||
}
|
||||
|
||||
const baseParams = getPostParams()
|
||||
if (!baseParams) {
|
||||
callback?.onError?.()
|
||||
callback?.onSettled?.()
|
||||
return null
|
||||
}
|
||||
|
||||
@ -629,7 +629,10 @@ describe('Workflow edge event wiring', () => {
|
||||
},
|
||||
)
|
||||
|
||||
const { unmount } = renderSubject({ isCollaborationEnabled: true })
|
||||
const { unmount } = renderSubject({
|
||||
initialStoreState: { isWorkflowDataLoaded: true },
|
||||
isCollaborationEnabled: true,
|
||||
})
|
||||
|
||||
unmount()
|
||||
|
||||
@ -638,10 +641,40 @@ describe('Workflow edge event wiring', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip the unmount save before workflow data has loaded', () => {
|
||||
const { unmount } = renderSubject()
|
||||
|
||||
unmount()
|
||||
|
||||
expect(workflowHookMocks.handleSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(toastErrorMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not save the draft when the workflow rerenders', () => {
|
||||
const { rerender } = renderSubject({
|
||||
initialStoreState: { isWorkflowDataLoaded: true },
|
||||
isCollaborationEnabled: false,
|
||||
})
|
||||
|
||||
rerender(
|
||||
<ReactFlowProvider>
|
||||
<Workflow nodes={baseNodes} edges={baseEdges} isCollaborationEnabled>
|
||||
<ReactFlowEdgeBootstrap nodes={baseNodes} edges={baseEdges} />
|
||||
</Workflow>
|
||||
</ReactFlowProvider>,
|
||||
)
|
||||
|
||||
expect(workflowHookMocks.handleSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(toastErrorMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should skip the unmount save when the current collaborator is not the draft leader', () => {
|
||||
collaborationBridge.canFlushGraphOnPageClose.mockReturnValue(false)
|
||||
|
||||
const { unmount } = renderSubject({ isCollaborationEnabled: true })
|
||||
const { unmount } = renderSubject({
|
||||
initialStoreState: { isWorkflowDataLoaded: true },
|
||||
isCollaborationEnabled: true,
|
||||
})
|
||||
|
||||
unmount()
|
||||
|
||||
|
||||
@ -22,7 +22,17 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useEventListener } from 'ahooks'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { setAutoFreeze } from 'immer'
|
||||
import { Fragment, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
@ -365,22 +375,26 @@ export const Workflow: FC<WorkflowProps> = memo(
|
||||
}
|
||||
}, [])
|
||||
|
||||
const syncWorkflowDraftOnUnmount = useEffectEvent(() => {
|
||||
if (!workflowStore.getState().isWorkflowDataLoaded) return
|
||||
if (isCollaborationEnabled && !collaborationManager.canFlushGraphOnPageClose()) return
|
||||
|
||||
handleSyncWorkflowDraft(true, true, {
|
||||
onError: () => {
|
||||
toast.error(
|
||||
t(($) => $['common.draftSaveFailed'], { ns: 'workflow' }),
|
||||
{
|
||||
timeout: 0,
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isCollaborationEnabled && !collaborationManager.canFlushGraphOnPageClose()) return
|
||||
|
||||
handleSyncWorkflowDraft(true, true, {
|
||||
onError: () => {
|
||||
toast.error(
|
||||
t(($) => $['common.draftSaveFailed'], { ns: 'workflow' }),
|
||||
{
|
||||
timeout: 0,
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
syncWorkflowDraftOnUnmount()
|
||||
}
|
||||
}, [handleSyncWorkflowDraft, isCollaborationEnabled, t])
|
||||
}, [])
|
||||
|
||||
const handlePendingCommentPositionChange = useCallback(
|
||||
(position: NonNullable<WorkflowSliceShape['pendingComment']>) => {
|
||||
|
||||
@ -2,14 +2,16 @@ import type {
|
||||
AgentSoulConfig,
|
||||
WorkflowAgentComposerResponse,
|
||||
} from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ReactNode, Ref } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { WorkflowInlineAgentConfigureWorkspace } from '../agent-orchestrate-panel-content'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
checkoutBuildDraft: vi.fn(),
|
||||
completeBuildConversation: undefined as (() => void) | undefined,
|
||||
deleteBuildDraft: vi.fn(),
|
||||
loadBuildDraft: vi.fn(),
|
||||
applyBuildDraft: vi.fn(),
|
||||
@ -18,10 +20,27 @@ const mocks = vi.hoisted(() => ({
|
||||
saveBuildDraft: vi.fn(),
|
||||
saveAgentSoulConfig: vi.fn(),
|
||||
saveDraft: vi.fn(),
|
||||
stopBuildChat: vi.fn(),
|
||||
uploadAgentSandboxFile: vi.fn(),
|
||||
uploadWorkflowSandboxFile: vi.fn(),
|
||||
}))
|
||||
|
||||
const editionState = vi.hoisted(() => ({
|
||||
isSelfHosted: false,
|
||||
licenseStatus: 'none',
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
get IS_CE_EDITION() {
|
||||
return editionState.isSelfHosted
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const permission = vi.hoisted(() => ({ canManageAgents: true }))
|
||||
|
||||
vi.mock('@/features/agent-v2/permissions', () => ({
|
||||
@ -99,11 +118,12 @@ vi.mock('@/features/agent-v2/agent-detail/configure/components/preview/build-bac
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/agent-detail/configure/components/preview/build-chat', async () => {
|
||||
const { useState } = await import('react')
|
||||
const { useImperativeHandle, useState } = await import('react')
|
||||
|
||||
return {
|
||||
AgentBuildChat: (props: {
|
||||
conversationId?: string | null
|
||||
controllerRef?: Ref<{ stop: () => void }>
|
||||
onConversationComplete?: (conversationId: string, workflowRunId?: string) => void
|
||||
onConversationIdChange?: (conversationId: string) => void
|
||||
onSendInterrupted?: () => void
|
||||
@ -111,6 +131,9 @@ vi.mock('@/features/agent-v2/agent-detail/configure/components/preview/build-cha
|
||||
}) => {
|
||||
const [messageSent, setMessageSent] = useState(false)
|
||||
const [sentPrompt, setSentPrompt] = useState<string | undefined>()
|
||||
useImperativeHandle(props.controllerRef, () => ({ stop: mocks.stopBuildChat }))
|
||||
mocks.completeBuildConversation = () =>
|
||||
props.onConversationComplete?.('build-conversation-new', 'workflow-run-1')
|
||||
|
||||
return (
|
||||
<div role="region" aria-label="build-chat">
|
||||
@ -120,21 +143,19 @@ vi.mock('@/features/agent-v2/agent-detail/configure/components/preview/build-cha
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void props.onSaveDraftBeforeRun?.().then((agentSoulConfig) => {
|
||||
setSentPrompt(agentSoulConfig?.prompt?.system_prompt)
|
||||
setMessageSent(true)
|
||||
props.onConversationIdChange?.('build-conversation-new')
|
||||
})
|
||||
void props.onSaveDraftBeforeRun?.().then(
|
||||
(agentSoulConfig) => {
|
||||
setSentPrompt(agentSoulConfig?.prompt?.system_prompt)
|
||||
setMessageSent(true)
|
||||
props.onConversationIdChange?.('build-conversation-new')
|
||||
},
|
||||
() => undefined,
|
||||
)
|
||||
}}
|
||||
>
|
||||
send build message
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
props.onConversationComplete?.('build-conversation-new', 'workflow-run-1')
|
||||
}
|
||||
>
|
||||
<button type="button" onClick={() => mocks.completeBuildConversation?.()}>
|
||||
complete build conversation
|
||||
</button>
|
||||
<button
|
||||
@ -152,6 +173,30 @@ vi.mock('@/features/agent-v2/agent-detail/configure/components/preview/build-cha
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/features/agent-v2/agent-detail/configure/components/preview/preview-chat', () => ({
|
||||
AgentPreviewChat: (props: {
|
||||
answerActionPosition?: 'auto' | 'below'
|
||||
conversationId?: string | null
|
||||
onConversationIdChange?: (conversationId: string) => void
|
||||
onSaveDraftBeforeRun?: () => Promise<unknown>
|
||||
}) => (
|
||||
<div role="region" aria-label="preview-chat">
|
||||
<span>{`preview:${props.conversationId ?? 'none'}`}</span>
|
||||
<span>{`actionPosition:${props.answerActionPosition ?? 'auto'}`}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void props.onSaveDraftBeforeRun?.().then(() => {
|
||||
props.onConversationIdChange?.('preview-conversation-new')
|
||||
})
|
||||
}}
|
||||
>
|
||||
send preview message
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/agent-v2/agent-soul-config', () => ({
|
||||
useWorkflowInlineAgentConfigureSync: () => ({
|
||||
draftSavedAt: undefined,
|
||||
@ -162,6 +207,14 @@ vi.mock('@/app/components/workflow/nodes/agent-v2/agent-soul-config', () => ({
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleClient: {
|
||||
systemFeatures: {
|
||||
get: () =>
|
||||
Promise.resolve({
|
||||
license: {
|
||||
status: editionState.licenseStatus,
|
||||
},
|
||||
}),
|
||||
},
|
||||
agent: {
|
||||
byAgentId: {
|
||||
sandbox: {
|
||||
@ -194,6 +247,11 @@ vi.mock('@/service/client', () => ({
|
||||
},
|
||||
},
|
||||
consoleQuery: {
|
||||
systemFeatures: {
|
||||
get: {
|
||||
queryKey: () => ['system-features'],
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
byAgentId: {
|
||||
get: {
|
||||
@ -414,9 +472,21 @@ function createInlineComposerState({
|
||||
} as WorkflowAgentComposerResponse
|
||||
}
|
||||
|
||||
function createDeferredPromise<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise
|
||||
})
|
||||
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('WorkflowInlineAgentConfigureWorkspace', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.completeBuildConversation = undefined
|
||||
editionState.isSelfHosted = false
|
||||
editionState.licenseStatus = 'none'
|
||||
permission.canManageAgents = true
|
||||
mocks.loadBuildDraft.mockRejectedValue(new Response(null, { status: 404 }))
|
||||
mocks.checkoutBuildDraft.mockResolvedValue({
|
||||
@ -498,6 +568,274 @@ describe('WorkflowInlineAgentConfigureWorkspace', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
}
|
||||
|
||||
describe('Right panel mode', () => {
|
||||
it('should switch an inline agent between build and preview without creating a build draft from preview', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspace()
|
||||
|
||||
const previewButton = await screen.findByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.preview',
|
||||
})
|
||||
expect(previewButton).toBeEnabled()
|
||||
|
||||
await user.click(previewButton)
|
||||
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent('preview:none')
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent(
|
||||
'actionPosition:below',
|
||||
)
|
||||
expect(screen.queryByRole('region', { name: 'build-chat' })).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'send preview message' }))
|
||||
|
||||
await waitFor(() => expect(mocks.saveDraft).toHaveBeenCalled())
|
||||
expect(mocks.saveBuildDraft).not.toHaveBeenCalled()
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.build',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(screen.getByRole('region', { name: 'build-chat' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('region', { name: 'preview-chat' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should refresh only the inline preview conversation when restarting preview mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspace({
|
||||
inlineComposerState: createInlineComposerState({
|
||||
debugConversationHasMessages: true,
|
||||
debugConversationId: 'inline-build-conversation-1',
|
||||
debugConversationMessageCount: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.preview',
|
||||
}),
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'send preview message' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent(
|
||||
'preview:preview-conversation-new',
|
||||
)
|
||||
})
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.preview.restart',
|
||||
}),
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.refreshDebugConversation).toHaveBeenCalledWith(
|
||||
{
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
body: {
|
||||
draft_type: 'draft',
|
||||
},
|
||||
},
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent('preview:none')
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.build',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent(
|
||||
'build:inline-build-conversation-1',
|
||||
)
|
||||
})
|
||||
|
||||
it('should discard an active inline build session before switching to preview', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspace()
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'send build message' }))
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('sent:yes'),
|
||||
)
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.preview',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByRole('alertdialog', {
|
||||
name: 'agentV2.agentDetail.configure.switchToPreviewConfirm.title',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'build-chat', hidden: true })).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
await waitFor(() => expect(mocks.deleteBuildDraft).toHaveBeenCalled())
|
||||
expect(mocks.stopBuildChat).toHaveBeenCalledTimes(1)
|
||||
const stopBuildChatCallOrder = mocks.stopBuildChat.mock.invocationCallOrder[0]
|
||||
const deleteBuildDraftCallOrder = mocks.deleteBuildDraft.mock.invocationCallOrder[0]
|
||||
if (stopBuildChatCallOrder === undefined || deleteBuildDraftCallOrder === undefined)
|
||||
throw new Error('Expected Build stop and draft deletion calls.')
|
||||
expect(stopBuildChatCallOrder).toBeLessThan(deleteBuildDraftCallOrder)
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('region', { name: 'build-chat' })).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'send preview message' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent(
|
||||
'preview:preview-conversation-new',
|
||||
)
|
||||
})
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.preview.restart',
|
||||
}),
|
||||
).toBeEnabled()
|
||||
})
|
||||
|
||||
it('should wait for an in-flight inline build draft save and ignore its result after switching to preview', async () => {
|
||||
const user = userEvent.setup()
|
||||
const buildDraftSave = createDeferredPromise<{
|
||||
agent_soul: AgentSoulConfig
|
||||
draft: object
|
||||
variant: 'agent_app'
|
||||
}>()
|
||||
mocks.saveBuildDraft.mockReturnValue(buildDraftSave.promise)
|
||||
renderWorkspace()
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'send build message' }))
|
||||
await waitFor(() => expect(mocks.saveBuildDraft).toHaveBeenCalledTimes(1))
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.preview',
|
||||
}),
|
||||
)
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'common.operation.confirm',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mocks.deleteBuildDraft).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
buildDraftSave.resolve({
|
||||
agent_soul: {
|
||||
schema_version: 1,
|
||||
prompt: {
|
||||
system_prompt: 'Late build draft prompt.',
|
||||
},
|
||||
},
|
||||
draft: {},
|
||||
variant: 'agent_app',
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => expect(mocks.deleteBuildDraft).toHaveBeenCalledTimes(1))
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('local composer draft')).toHaveValue('Help with workflow tasks.')
|
||||
})
|
||||
|
||||
it('should ignore a late inline build completion after leaving and re-entering build mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderWorkspace()
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'send build message' }))
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('sent:yes'),
|
||||
)
|
||||
const completeBuildConversation = mocks.completeBuildConversation
|
||||
if (!completeBuildConversation) throw new Error('Expected a Build completion callback.')
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.preview',
|
||||
}),
|
||||
)
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'common.operation.confirm',
|
||||
}),
|
||||
)
|
||||
await screen.findByRole('region', { name: 'preview-chat' })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.build',
|
||||
}),
|
||||
)
|
||||
await screen.findByRole('region', { name: 'build-chat' })
|
||||
const buildDraftLoadCount = mocks.loadBuildDraft.mock.calls.length
|
||||
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
await act(async () => {
|
||||
completeBuildConversation()
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
})
|
||||
expect(mocks.loadBuildDraft).toHaveBeenCalledTimes(buildDraftLoadCount)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('should confirm before discarding an existing inline build draft', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.loadBuildDraft.mockResolvedValue({
|
||||
agent_soul: {
|
||||
schema_version: 1,
|
||||
prompt: {
|
||||
system_prompt: 'Existing build draft prompt.',
|
||||
},
|
||||
},
|
||||
draft: {},
|
||||
variant: 'agent_app',
|
||||
})
|
||||
renderWorkspace()
|
||||
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.preview',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByRole('alertdialog', {
|
||||
name: 'agentV2.agentDetail.configure.switchToPreviewConfirm.title',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
expect(mocks.deleteBuildDraft).not.toHaveBeenCalled()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
|
||||
await waitFor(() => expect(mocks.deleteBuildDraft).toHaveBeenCalledTimes(1))
|
||||
expect(screen.getByRole('region', { name: 'preview-chat' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('region', { name: 'build-chat' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep preview disabled for an inline agent in community edition', async () => {
|
||||
editionState.isSelfHosted = true
|
||||
renderWorkspace()
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.rightPanel.preview',
|
||||
}),
|
||||
).toBeDisabled()
|
||||
expect(screen.getByRole('region', { name: 'build-chat' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('region', { name: 'preview-chat' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Working Directory', () => {
|
||||
it('should show save-to-roster in the configure header menu without rendering the old action bar', async () => {
|
||||
renderWorkspace({
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
AgentComposerBindingResponse,
|
||||
WorkflowAgentComposerResponse,
|
||||
} from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { AgentPreviewChatController } from '@/features/agent-v2/agent-detail/configure/components/preview/chat-conversation'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -19,7 +20,7 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { skipToken, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useAtom, useAtomValue, useStore as useJotaiStore, useSetAtom } from 'jotai'
|
||||
import { ScopeProvider } from 'jotai-scope'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@ -27,6 +28,7 @@ import {
|
||||
useDefaultModel,
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import {
|
||||
agentSoulConfigToFormState,
|
||||
formStateToAgentSoulConfig,
|
||||
@ -37,6 +39,7 @@ import {
|
||||
rebaseAgentComposerDraftAtom,
|
||||
} from '@/features/agent-v2/agent-composer/store'
|
||||
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
|
||||
import { AgentConfigureClearSessionConfirmDialog } from '@/features/agent-v2/agent-detail/configure/components/confirm-clear-session-dialog'
|
||||
import { AgentOrchestratePanel } from '@/features/agent-v2/agent-detail/configure/components/orchestrate'
|
||||
import { AgentBuildDraftBar } from '@/features/agent-v2/agent-detail/configure/components/orchestrate/build-draft-bar'
|
||||
import { AgentBuildPanelBackground } from '@/features/agent-v2/agent-detail/configure/components/preview/build-background'
|
||||
@ -52,17 +55,20 @@ import {
|
||||
} from '@/features/agent-v2/agent-detail/configure/components/workspace'
|
||||
import {
|
||||
agentConfigureConversationIdsAtom,
|
||||
agentConfigureRightPanelChatModeAtom,
|
||||
agentConfigureScopedAtoms,
|
||||
agentConfigureRightPanelModeAtom,
|
||||
agentConfigureSoulSourceOverrideAtom,
|
||||
resetAgentConfigureConversationAtom,
|
||||
setAgentConfigureConversationIdAtom,
|
||||
workflowInlineAgentConfigureScopedAtoms,
|
||||
} from '@/features/agent-v2/agent-detail/configure/state'
|
||||
import {
|
||||
useAgentConfigureBuildDraftActions,
|
||||
useAgentConfigureBuildDraftData,
|
||||
} from '@/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft'
|
||||
import { useAgentConfigureSessionController } from '@/features/agent-v2/agent-detail/configure/use-agent-configure-session-controller'
|
||||
import { useCanManageAgents } from '@/features/agent-v2/permissions'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { LicenseStatus } from '@/features/system-features/constants'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config'
|
||||
@ -202,7 +208,7 @@ export function WorkflowInlineAgentConfigureWorkspace(
|
||||
return (
|
||||
<ScopeProvider
|
||||
key={composerSessionKey}
|
||||
atoms={agentConfigureScopedAtoms}
|
||||
atoms={workflowInlineAgentConfigureScopedAtoms}
|
||||
name="WorkflowInlineAgentConfigure"
|
||||
>
|
||||
<WorkflowInlineAgentConfigureWorkspaceComposerScope
|
||||
@ -226,11 +232,13 @@ function WorkflowInlineAgentConfigureWorkspaceComposerScope({
|
||||
agentSoulConfig: AgentSoulConfig
|
||||
}) {
|
||||
const soulSourceOverride = useAtomValue(agentConfigureSoulSourceOverrideAtom)
|
||||
const rightPanelMode = useAtomValue(agentConfigureRightPanelModeAtom)
|
||||
const setSoulSourceOverride = useSetAtom(agentConfigureSoulSourceOverrideAtom)
|
||||
const buildDraft = useAgentConfigureBuildDraftData({
|
||||
agentId,
|
||||
activeVersionId: activeConfigSnapshot?.id,
|
||||
composerAgentSoulConfig: agentSoulConfig,
|
||||
isBuildMode: rightPanelMode === 'build',
|
||||
isViewingVersion: false,
|
||||
normalAgentSoulConfig: agentSoulConfig,
|
||||
setSoulSourceOverride,
|
||||
@ -296,22 +304,29 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
buildDraft: ReturnType<typeof useAgentConfigureBuildDraftData>
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const { t: tAgent } = useTranslation('agentV2')
|
||||
const queryClient = useQueryClient()
|
||||
const jotaiStore = useJotaiStore()
|
||||
const setBuildDraftSoulSourceOverride = buildDraft.setSoulSourceOverride
|
||||
const { data: systemFeatures } = useQuery(systemFeaturesQueryOptions())
|
||||
const composerState = inlineComposerState
|
||||
const [buildDraftActionsDisabled, setBuildDraftActionsDisabled] = useState(false)
|
||||
const [clearPreviewChat, setClearPreviewChat] = useState(false)
|
||||
const [completedBuildConversationId, setCompletedBuildConversationId] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const [workflowRunId, setWorkflowRunId] = useState<string | null>(null)
|
||||
const rightPanelChatControllerRef = useRef<AgentPreviewChatController>(null)
|
||||
const appId = flowType === FlowType.appFlow ? flowId : undefined
|
||||
const conversationIds = useAtomValue(agentConfigureConversationIdsAtom)
|
||||
const rightPanelChatMode = useAtomValue(agentConfigureRightPanelChatModeAtom)
|
||||
const [rightPanelMode, setRightPanelMode] = useAtom(agentConfigureRightPanelModeAtom)
|
||||
const previewEnabled =
|
||||
!IS_CE_EDITION ||
|
||||
systemFeatures?.license.status === LicenseStatus.ACTIVE ||
|
||||
systemFeatures?.license.status === LicenseStatus.EXPIRING
|
||||
const workingDirectoryPanel = useAgentWorkingDirectoryPanel({
|
||||
agentId,
|
||||
appId,
|
||||
conversationId: conversationIds[rightPanelChatMode],
|
||||
conversationId: conversationIds[rightPanelMode],
|
||||
nodeId,
|
||||
workflowRunId,
|
||||
})
|
||||
@ -412,8 +427,10 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
)
|
||||
const {
|
||||
mutateAsync: refreshDebugConversationRequestAsync,
|
||||
isPending: isRefreshingDebugConversation,
|
||||
isPending: isRefreshingBuildConversation,
|
||||
} = refreshDebugConversationMutation
|
||||
const { mutate: refreshPreviewConversationRequest, isPending: isRefreshingPreviewConversation } =
|
||||
useMutation(consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions())
|
||||
const refreshDebugConversationInput = useCallback(
|
||||
() => ({
|
||||
params: {
|
||||
@ -425,7 +442,21 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
const refreshDebugConversationAsync = useCallback(() => {
|
||||
return refreshDebugConversationRequestAsync(refreshDebugConversationInput())
|
||||
}, [refreshDebugConversationInput, refreshDebugConversationRequestAsync])
|
||||
const resetBuildChatSession = useCallback(async () => {
|
||||
const refreshPreviewConversationInput = useCallback(
|
||||
() => ({
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
body: {
|
||||
draft_type: 'draft' as const,
|
||||
},
|
||||
}),
|
||||
[agentId],
|
||||
)
|
||||
const refreshPreviewConversation = useCallback(() => {
|
||||
refreshPreviewConversationRequest(refreshPreviewConversationInput())
|
||||
}, [refreshPreviewConversationInput, refreshPreviewConversationRequest])
|
||||
const resetBuildChatState = useCallback(async () => {
|
||||
await refreshDebugConversationAsync().catch(() => undefined)
|
||||
setCompletedBuildConversationId(null)
|
||||
setConversationId({ mode: 'build', conversationId: null })
|
||||
@ -441,6 +472,31 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
},
|
||||
[rebaseComposerDraft],
|
||||
)
|
||||
const sessionController = useAgentConfigureSessionController({
|
||||
buildDraftAgentSoulConfig: buildDraft.buildDraftAgentSoulConfig,
|
||||
hasActiveBuildDraft: buildDraft.hasActiveBuildDraft,
|
||||
isBuildDraftActive: buildDraft.isActive,
|
||||
mode: rightPanelMode,
|
||||
normalAgentSoulConfig: agentSoulConfig,
|
||||
onModeChange: setRightPanelMode,
|
||||
})
|
||||
const {
|
||||
buildCallbackGeneration,
|
||||
buildDraftActionsDisabled,
|
||||
changeMode,
|
||||
confirmSwitchToPreview: confirmSessionSwitchToPreview,
|
||||
finishBuildAction,
|
||||
isBuildCallbackCurrent,
|
||||
resetBuildSession: resetSessionController,
|
||||
runBuildPreparation,
|
||||
setShowSwitchToPreviewConfirm,
|
||||
showSwitchToPreviewConfirm,
|
||||
waitForPendingPreviewDraftSave,
|
||||
} = sessionController
|
||||
const resetBuildSession = useCallback(
|
||||
() => resetSessionController(resetBuildChatState),
|
||||
[resetBuildChatState, resetSessionController],
|
||||
)
|
||||
const buildDraftActions = useAgentConfigureBuildDraftActions({
|
||||
agentId,
|
||||
buildDraftAgentSoulConfig: buildDraft.agentSoulConfig,
|
||||
@ -453,8 +509,9 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
agent_soul: agentSoulConfig,
|
||||
},
|
||||
}),
|
||||
resetBuildChatSession,
|
||||
resetBuildChatSession: resetBuildSession,
|
||||
saveDraft: async () => {
|
||||
await waitForPendingPreviewDraftSave()
|
||||
await saveDraft()
|
||||
},
|
||||
setSoulSourceOverride: buildDraft.setSoulSourceOverride,
|
||||
@ -470,7 +527,7 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
const { mutateAsync: saveBuildDraft } = useMutation(
|
||||
consoleQuery.agent.byAgentId.buildDraft.put.mutationOptions(),
|
||||
)
|
||||
const discardBuildDraftMutation = useMutation(
|
||||
const { mutateAsync: discardBuildDraft, isPending: isDiscardingBuildDraft } = useMutation(
|
||||
consoleQuery.agent.byAgentId.buildDraft.delete.mutationOptions(),
|
||||
)
|
||||
const getInlineAgentSoulDraft = useCallback(
|
||||
@ -483,6 +540,7 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
[agentSoulConfig, currentModel, jotaiStore],
|
||||
)
|
||||
const prepareInlineBuildDraftBeforeRun = useCallback(async () => {
|
||||
await waitForPendingPreviewDraftSave()
|
||||
cancelBuildDraftRefresh()
|
||||
const configSnapshot = getInlineAgentSoulDraft()
|
||||
const savedComposerState = await saveDraft()
|
||||
@ -497,15 +555,13 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
agent_soul: preparedAgentSoulConfig,
|
||||
},
|
||||
})
|
||||
|
||||
const savedBuildAgentSoulConfig = buildDraftState.agent_soul ?? preparedAgentSoulConfig
|
||||
queryClient.setQueryData(buildDraftQueryOptions.queryKey, buildDraftState)
|
||||
rebaseComposerDraftFromSoulConfig(savedBuildAgentSoulConfig)
|
||||
buildDraft.setSoulSourceOverride('build-draft')
|
||||
setBuildDraftSoulSourceOverride('build-draft')
|
||||
return savedBuildAgentSoulConfig
|
||||
}, [
|
||||
agentId,
|
||||
buildDraft,
|
||||
buildDraftQueryOptions.queryKey,
|
||||
cancelBuildDraftRefresh,
|
||||
getInlineAgentSoulDraft,
|
||||
@ -513,6 +569,8 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
rebaseComposerDraftFromSoulConfig,
|
||||
saveBuildDraft,
|
||||
saveDraft,
|
||||
setBuildDraftSoulSourceOverride,
|
||||
waitForPendingPreviewDraftSave,
|
||||
])
|
||||
const applyInlineBuildDraft = async () => {
|
||||
cancelBuildDraftRefresh()
|
||||
@ -521,15 +579,13 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
if (!buildDraft.agentSoulConfig) return
|
||||
|
||||
const savedComposerState = await saveAgentSoulConfig(buildDraft.agentSoulConfig)
|
||||
await discardBuildDraftMutation
|
||||
.mutateAsync({
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
})
|
||||
.catch(() => undefined)
|
||||
await resetBuildChatSession().catch(() => undefined)
|
||||
buildDraft.setSoulSourceOverride('draft')
|
||||
await discardBuildDraft({
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
}).catch(() => undefined)
|
||||
await resetBuildSession().catch(() => undefined)
|
||||
setBuildDraftSoulSourceOverride('draft')
|
||||
queryClient.removeQueries({
|
||||
queryKey: buildDraftQueryOptions.queryKey,
|
||||
})
|
||||
@ -543,50 +599,89 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
setIsApplyingInlineBuildDraft(false)
|
||||
}
|
||||
}
|
||||
const discardInlineBuildDraft = async () => {
|
||||
const discardInlineBuildDraft = useCallback(async () => {
|
||||
cancelBuildDraftRefresh()
|
||||
try {
|
||||
await discardBuildDraftMutation.mutateAsync({
|
||||
await discardBuildDraft({
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
})
|
||||
await resetBuildChatSession().catch(() => undefined)
|
||||
buildDraft.setSoulSourceOverride('draft')
|
||||
await resetBuildSession().catch(() => undefined)
|
||||
setBuildDraftSoulSourceOverride('draft')
|
||||
queryClient.removeQueries({
|
||||
queryKey: buildDraftQueryOptions.queryKey,
|
||||
})
|
||||
rebaseComposerDraftFromSoulConfig(agentSoulConfig)
|
||||
toast.success(t(($) => $['api.actionSuccess']))
|
||||
return true
|
||||
} catch {
|
||||
toast.error(t(($) => $['api.actionFailed']))
|
||||
return false
|
||||
}
|
||||
}
|
||||
}, [
|
||||
agentId,
|
||||
agentSoulConfig,
|
||||
buildDraftQueryOptions.queryKey,
|
||||
cancelBuildDraftRefresh,
|
||||
discardBuildDraft,
|
||||
queryClient,
|
||||
rebaseComposerDraftFromSoulConfig,
|
||||
resetBuildSession,
|
||||
setBuildDraftSoulSourceOverride,
|
||||
t,
|
||||
])
|
||||
const stopBuildChat = useCallback(() => {
|
||||
rightPanelChatControllerRef.current?.stop()
|
||||
}, [])
|
||||
const changeRightPanelMode = useCallback(
|
||||
(nextMode: typeof rightPanelMode) =>
|
||||
changeMode(nextMode, {
|
||||
discardBuildDraft: discardInlineBuildDraft,
|
||||
rebaseComposerDraft: rebaseComposerDraftFromSoulConfig,
|
||||
savePreviewDraft: saveDraft,
|
||||
stopBuildChat,
|
||||
}),
|
||||
[
|
||||
changeMode,
|
||||
discardInlineBuildDraft,
|
||||
rebaseComposerDraftFromSoulConfig,
|
||||
saveDraft,
|
||||
stopBuildChat,
|
||||
],
|
||||
)
|
||||
const confirmSwitchToPreview = useCallback(
|
||||
() => confirmSessionSwitchToPreview(discardInlineBuildDraft, stopBuildChat),
|
||||
[confirmSessionSwitchToPreview, discardInlineBuildDraft, stopBuildChat],
|
||||
)
|
||||
const hasRestartCurrentChatTarget =
|
||||
(inlineComposerState?.debug_conversation_has_messages ?? false) || buildDraft.isActive
|
||||
rightPanelMode === 'build'
|
||||
? (inlineComposerState?.debug_conversation_has_messages ?? false) || buildDraft.isActive
|
||||
: !!conversationIds.preview
|
||||
const isRestartCurrentChatDisabled =
|
||||
!hasRestartCurrentChatTarget ||
|
||||
buildDraftActionsDisabled ||
|
||||
isApplyingInlineBuildDraft ||
|
||||
discardBuildDraftMutation.isPending ||
|
||||
isRefreshingDebugConversation
|
||||
isDiscardingBuildDraft ||
|
||||
isRefreshingBuildConversation ||
|
||||
isRefreshingPreviewConversation
|
||||
const buildConversationHasAgentResponse =
|
||||
!!conversationIds.build &&
|
||||
(conversationIds.build === completedBuildConversationId ||
|
||||
(conversationIds.build === inlineComposerState?.debug_conversation_id &&
|
||||
(inlineComposerState?.debug_conversation_has_messages ?? false)))
|
||||
const showWorkingDirectoryAction =
|
||||
rightPanelChatMode === 'build' && buildConversationHasAgentResponse
|
||||
const showWorkingDirectoryAction = rightPanelMode === 'build' && buildConversationHasAgentResponse
|
||||
const restartCurrentChat = () => {
|
||||
if (isRestartCurrentChatDisabled) return
|
||||
|
||||
if (buildDraft.isActive) {
|
||||
if (rightPanelMode === 'build' && buildDraft.isActive) {
|
||||
void discardInlineBuildDraft()
|
||||
return
|
||||
}
|
||||
|
||||
void refreshDebugConversationAsync().catch(() => undefined)
|
||||
resetConversation(rightPanelChatMode)
|
||||
if (rightPanelMode === 'build') void refreshDebugConversationAsync().catch(() => undefined)
|
||||
else refreshPreviewConversation()
|
||||
resetConversation(rightPanelMode)
|
||||
setClearPreviewChat(true)
|
||||
}
|
||||
|
||||
@ -614,7 +709,7 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
changesCount={buildDraft.changesCount}
|
||||
disabled={buildDraftActionsDisabled}
|
||||
isApplying={isApplyingInlineBuildDraft}
|
||||
isDiscarding={discardBuildDraftMutation.isPending}
|
||||
isDiscarding={isDiscardingBuildDraft}
|
||||
onApply={() => {
|
||||
void applyInlineBuildDraft()
|
||||
}}
|
||||
@ -639,13 +734,13 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
}
|
||||
rightPanel={
|
||||
<AgentConfigurePreviewSurface
|
||||
background={<AgentBuildPanelBackground visible />}
|
||||
background={<AgentBuildPanelBackground visible={rightPanelMode === 'build'} />}
|
||||
header={
|
||||
<AgentPreviewHeader
|
||||
mode="build"
|
||||
previewEnabled={false}
|
||||
mode={rightPanelMode}
|
||||
previewEnabled={previewEnabled}
|
||||
isChatFeaturesOpen={false}
|
||||
onModeChange={() => undefined}
|
||||
onModeChange={changeRightPanelMode}
|
||||
onToggleChatFeatures={() => undefined}
|
||||
onOpenWorkingDirectory={workingDirectoryPanel.openWorkingDirectory}
|
||||
onRefresh={restartCurrentChat}
|
||||
@ -667,6 +762,7 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
chat={
|
||||
<AgentConfigureRightPanelChat
|
||||
agentId={agentId}
|
||||
answerActionPosition="below"
|
||||
agentIcon={composerState?.agent?.icon}
|
||||
agentIconBackground={composerState?.agent?.icon_background}
|
||||
agentIconType={
|
||||
@ -677,51 +773,67 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
agentName={composerState?.agent?.name}
|
||||
agentSoulConfig={buildDraft.agentSoulConfig}
|
||||
clearChatList={clearPreviewChat}
|
||||
controllerRef={rightPanelChatControllerRef}
|
||||
conversationIds={conversationIds}
|
||||
draftType="debug_build"
|
||||
mode={rightPanelChatMode}
|
||||
mode={rightPanelMode}
|
||||
onClearChatListChange={setClearPreviewChat}
|
||||
onConversationComplete={(mode, completedConversationId, completedWorkflowRunId) => {
|
||||
if (mode === 'build') {
|
||||
setCompletedBuildConversationId(completedConversationId)
|
||||
setWorkflowRunId(completedWorkflowRunId ?? completedConversationId)
|
||||
invalidateAgentWorkingDirectoryFiles({
|
||||
agentId,
|
||||
appId,
|
||||
conversationId: completedConversationId,
|
||||
nodeId,
|
||||
queryClient,
|
||||
workflowRunId: completedWorkflowRunId ?? completedConversationId,
|
||||
})
|
||||
buildDraftActions.refreshBuildDraftAfterBuildChat(() =>
|
||||
setBuildDraftActionsDisabled(false),
|
||||
)
|
||||
}
|
||||
if (mode !== 'build' || !isBuildCallbackCurrent(buildCallbackGeneration)) return
|
||||
|
||||
setCompletedBuildConversationId(completedConversationId)
|
||||
setWorkflowRunId(completedWorkflowRunId ?? completedConversationId)
|
||||
invalidateAgentWorkingDirectoryFiles({
|
||||
agentId,
|
||||
appId,
|
||||
conversationId: completedConversationId,
|
||||
nodeId,
|
||||
queryClient,
|
||||
workflowRunId: completedWorkflowRunId ?? completedConversationId,
|
||||
})
|
||||
buildDraftActions.refreshBuildDraftAfterBuildChat(() =>
|
||||
finishBuildAction(buildCallbackGeneration),
|
||||
)
|
||||
}}
|
||||
onConversationIdChange={(mode, conversationId) => {
|
||||
if (mode === 'build' && !isBuildCallbackCurrent(buildCallbackGeneration)) return
|
||||
setConversationId({ mode, conversationId })
|
||||
}}
|
||||
onWorkflowRunIdChange={(nextWorkflowRunId) => {
|
||||
if (!isBuildCallbackCurrent(buildCallbackGeneration)) return
|
||||
if (nextWorkflowRunId) setWorkflowRunId(nextWorkflowRunId)
|
||||
}}
|
||||
onSaveDraftBeforeRun={async () => {
|
||||
setBuildDraftActionsDisabled(true)
|
||||
setWorkflowRunId(null)
|
||||
try {
|
||||
return await prepareInlineBuildDraftBeforeRun()
|
||||
} catch (error) {
|
||||
setBuildDraftActionsDisabled(false)
|
||||
throw error
|
||||
}
|
||||
}}
|
||||
onSaveDraftBeforeRun={
|
||||
rightPanelMode === 'build'
|
||||
? () => {
|
||||
setWorkflowRunId(null)
|
||||
return runBuildPreparation({
|
||||
generation: buildCallbackGeneration,
|
||||
markBuildChatStarted: true,
|
||||
prepare: prepareInlineBuildDraftBeforeRun,
|
||||
})
|
||||
}
|
||||
: async () => {
|
||||
await saveDraft()
|
||||
}
|
||||
}
|
||||
onSendInterrupted={() => {
|
||||
setBuildDraftActionsDisabled(false)
|
||||
finishBuildAction(buildCallbackGeneration)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
sidePanels={workingDirectoryPanel.panel}
|
||||
sidePanels={
|
||||
<>
|
||||
{workingDirectoryPanel.panel}
|
||||
<AgentConfigureClearSessionConfirmDialog
|
||||
open={showSwitchToPreviewConfirm}
|
||||
title={tAgent(($) => $['agentDetail.configure.switchToPreviewConfirm.title'])}
|
||||
onOpenChange={setShowSwitchToPreviewConfirm}
|
||||
onConfirm={confirmSwitchToPreview}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -376,6 +376,37 @@ describe('useAgentConfigureSync', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should not save the same draft again when Configure unmounts during an explicit save', async () => {
|
||||
const saveDeferred = createDeferredPromise<{ agent_soul: Record<string, unknown> }>()
|
||||
composerPutMutationFn.mockReturnValueOnce(saveDeferred.promise)
|
||||
const { result, store, unmount } = renderUseAgentConfigureSync()
|
||||
|
||||
act(() => {
|
||||
store.set(agentComposerDraftAtom, {
|
||||
...defaultAgentSoulConfigFormState,
|
||||
prompt: 'Switching prompt',
|
||||
})
|
||||
})
|
||||
|
||||
let saveDraftPromise!: Promise<void>
|
||||
act(() => {
|
||||
saveDraftPromise = result.current.saveDraft()
|
||||
})
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(composerPutMutationFn).toHaveBeenCalledTimes(1)
|
||||
|
||||
unmount()
|
||||
await act(async () => {
|
||||
saveDeferred.resolve({ agent_soul: {} })
|
||||
await saveDraftPromise
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
expect(composerPutMutationFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should include Agent Soul files when autosaving file changes', async () => {
|
||||
const { store } = renderUseAgentConfigureSync()
|
||||
|
||||
|
||||
@ -6,12 +6,15 @@ import type {
|
||||
AgentSoulConfig,
|
||||
} from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { useAgentConfigureData } from '../hooks'
|
||||
import type { AgentConfigureRightPanelMode } from '../state'
|
||||
import type { AgentPreviewChatController } from './preview/chat-conversation'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { ScopeProvider } from 'jotai-scope'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions'
|
||||
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
|
||||
import { rebaseAgentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
|
||||
@ -19,8 +22,6 @@ import { consoleQuery } from '@/service/client'
|
||||
import { useAgentConfigureModelOptions } from '../hooks'
|
||||
import {
|
||||
agentConfigureConversationIdsAtom,
|
||||
agentConfigureRightPanelChatModeAtom,
|
||||
agentConfigureRightPanelModeAtom,
|
||||
agentConfigureShowChatFeaturesAtom,
|
||||
agentConfigureShowPreviewVersionsAtom,
|
||||
agentConfigureSoulSourceOverrideAtom,
|
||||
@ -31,7 +32,9 @@ import {
|
||||
useAgentConfigureBuildDraftActions,
|
||||
useAgentConfigureBuildDraftData,
|
||||
} from '../use-agent-configure-build-draft'
|
||||
import { useAgentConfigureSessionController } from '../use-agent-configure-session-controller'
|
||||
import { useAgentConfigureSync } from '../use-agent-configure-sync'
|
||||
import { AgentConfigureClearSessionConfirmDialog } from './confirm-clear-session-dialog'
|
||||
import { AgentOrchestratePanel } from './orchestrate'
|
||||
import { AgentBuildDraftBar } from './orchestrate/build-draft-bar'
|
||||
import { AgentConfigurePageLoading } from './page-loading'
|
||||
@ -50,34 +53,51 @@ export function AgentConfigureComposerScope({
|
||||
agentId,
|
||||
composerRebaseRevision,
|
||||
configureData,
|
||||
previewEnabled,
|
||||
rightPanelMode,
|
||||
onComposerRebase,
|
||||
onRightPanelModeChange,
|
||||
onSelectVersion,
|
||||
}: {
|
||||
agentId: string
|
||||
composerRebaseRevision: number
|
||||
configureData: ReturnType<typeof useAgentConfigureData>
|
||||
previewEnabled: boolean
|
||||
rightPanelMode: AgentConfigureRightPanelMode
|
||||
onComposerRebase: () => void
|
||||
onRightPanelModeChange: (mode: AgentConfigureRightPanelMode) => void | Promise<unknown>
|
||||
onSelectVersion: (versionId: string | null) => void
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { composerQuery, selectedVersionId, activeVersionId, agentSoulConfig } = configureData
|
||||
const soulSourceOverride = useAtomValue(agentConfigureSoulSourceOverrideAtom)
|
||||
const setSoulSourceOverride = useSetAtom(agentConfigureSoulSourceOverrideAtom)
|
||||
const initializedComposerAgentIdRef = useRef<string | null>(null)
|
||||
const isViewingVersion = !!selectedVersionId
|
||||
const buildDraft = useAgentConfigureBuildDraftData({
|
||||
agentId,
|
||||
activeVersionId,
|
||||
composerAgentSoulConfig: composerQuery.data?.agent_soul,
|
||||
isBuildMode: rightPanelMode === 'build',
|
||||
isViewingVersion,
|
||||
normalAgentSoulConfig: agentSoulConfig,
|
||||
setSoulSourceOverride,
|
||||
soulSourceOverride,
|
||||
})
|
||||
const sessionController = useAgentConfigureSessionController({
|
||||
buildDraftAgentSoulConfig: buildDraft.buildDraftAgentSoulConfig,
|
||||
hasActiveBuildDraft: buildDraft.hasActiveBuildDraft,
|
||||
isBuildDraftActive: buildDraft.isActive,
|
||||
mode: rightPanelMode,
|
||||
normalAgentSoulConfig: agentSoulConfig,
|
||||
onModeChange: onRightPanelModeChange,
|
||||
})
|
||||
|
||||
if (buildDraft.isPending) {
|
||||
if (buildDraft.isPending && initializedComposerAgentIdRef.current !== agentId) {
|
||||
return <AgentConfigurePageLoading label={t(($) => $['agentDetail.sections.configure'])} />
|
||||
}
|
||||
|
||||
initializedComposerAgentIdRef.current = agentId
|
||||
const composerSessionKey = `${agentId}:${activeVersionId ?? selectedVersionId ?? 'draft'}:${composerRebaseRevision}`
|
||||
|
||||
return (
|
||||
@ -87,6 +107,9 @@ export function AgentConfigureComposerScope({
|
||||
composerSessionKey={composerSessionKey}
|
||||
configureData={configureData}
|
||||
isViewingVersion={isViewingVersion}
|
||||
previewEnabled={previewEnabled}
|
||||
rightPanelMode={rightPanelMode}
|
||||
sessionController={sessionController}
|
||||
onComposerRebase={onComposerRebase}
|
||||
onSelectVersion={onSelectVersion}
|
||||
/>
|
||||
@ -99,6 +122,9 @@ function AgentConfigurePageComposerSession({
|
||||
composerSessionKey,
|
||||
configureData,
|
||||
isViewingVersion,
|
||||
previewEnabled,
|
||||
rightPanelMode,
|
||||
sessionController,
|
||||
onComposerRebase,
|
||||
onSelectVersion,
|
||||
}: {
|
||||
@ -107,6 +133,9 @@ function AgentConfigurePageComposerSession({
|
||||
composerSessionKey: string
|
||||
configureData: ReturnType<typeof useAgentConfigureData>
|
||||
isViewingVersion: boolean
|
||||
previewEnabled: boolean
|
||||
rightPanelMode: AgentConfigureRightPanelMode
|
||||
sessionController: ReturnType<typeof useAgentConfigureSessionController>
|
||||
onComposerRebase: () => void
|
||||
onSelectVersion: (versionId: string | null) => void
|
||||
}) {
|
||||
@ -139,8 +168,10 @@ function AgentConfigurePageComposerSession({
|
||||
const {
|
||||
mutate: refreshDebugConversationRequest,
|
||||
mutateAsync: refreshDebugConversationRequestAsync,
|
||||
isPending: isRefreshingDebugConversation,
|
||||
isPending: isRefreshingBuildConversation,
|
||||
} = refreshDebugConversationMutation
|
||||
const { mutate: refreshPreviewConversationRequest, isPending: isRefreshingPreviewConversation } =
|
||||
useMutation(consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions())
|
||||
const refreshDebugConversationInput = useCallback(
|
||||
() => ({
|
||||
params: {
|
||||
@ -155,6 +186,20 @@ function AgentConfigurePageComposerSession({
|
||||
const refreshDebugConversationAsync = useCallback(() => {
|
||||
return refreshDebugConversationRequestAsync(refreshDebugConversationInput())
|
||||
}, [refreshDebugConversationInput, refreshDebugConversationRequestAsync])
|
||||
const refreshPreviewConversationInput = useCallback(
|
||||
() => ({
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
body: {
|
||||
draft_type: 'draft' as const,
|
||||
},
|
||||
}),
|
||||
[agentId],
|
||||
)
|
||||
const refreshPreviewConversation = useCallback(() => {
|
||||
refreshPreviewConversationRequest(refreshPreviewConversationInput())
|
||||
}, [refreshPreviewConversationInput, refreshPreviewConversationRequest])
|
||||
|
||||
return (
|
||||
<ScopeProvider
|
||||
@ -179,11 +224,17 @@ function AgentConfigurePageComposerSession({
|
||||
agentIconType={agentIconType}
|
||||
buildDraft={buildDraft}
|
||||
configureData={configureData}
|
||||
isRefreshingDebugConversation={isRefreshingDebugConversation}
|
||||
isRefreshingDebugConversation={
|
||||
isRefreshingBuildConversation || isRefreshingPreviewConversation
|
||||
}
|
||||
isViewingVersion={isViewingVersion}
|
||||
previewEnabled={previewEnabled}
|
||||
rightPanelMode={rightPanelMode}
|
||||
sessionController={sessionController}
|
||||
onComposerRebase={onComposerRebase}
|
||||
onRefreshDebugConversation={refreshDebugConversation}
|
||||
onRefreshDebugConversationAsync={refreshDebugConversationAsync}
|
||||
onRefreshPreviewConversation={refreshPreviewConversation}
|
||||
onSelectVersion={onSelectVersion}
|
||||
/>
|
||||
</AgentComposerProvider>
|
||||
@ -198,9 +249,13 @@ function AgentConfigurePageComposerContent({
|
||||
configureData,
|
||||
isRefreshingDebugConversation,
|
||||
isViewingVersion,
|
||||
previewEnabled,
|
||||
rightPanelMode,
|
||||
sessionController,
|
||||
onComposerRebase,
|
||||
onRefreshDebugConversation,
|
||||
onRefreshDebugConversationAsync,
|
||||
onRefreshPreviewConversation,
|
||||
onSelectVersion,
|
||||
}: {
|
||||
agentId: string
|
||||
@ -209,9 +264,13 @@ function AgentConfigurePageComposerContent({
|
||||
configureData: ReturnType<typeof useAgentConfigureData>
|
||||
isRefreshingDebugConversation: boolean
|
||||
isViewingVersion: boolean
|
||||
previewEnabled: boolean
|
||||
rightPanelMode: AgentConfigureRightPanelMode
|
||||
sessionController: ReturnType<typeof useAgentConfigureSessionController>
|
||||
onComposerRebase: () => void
|
||||
onRefreshDebugConversation: () => void
|
||||
onRefreshDebugConversationAsync: () => Promise<unknown>
|
||||
onRefreshPreviewConversation: () => void
|
||||
onSelectVersion: (versionId: string | null) => void
|
||||
}) {
|
||||
const {
|
||||
@ -223,14 +282,15 @@ function AgentConfigurePageComposerContent({
|
||||
activeConfigSnapshot,
|
||||
agentSoulConfig,
|
||||
} = configureData
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [buildDraftActionsDisabled, setBuildDraftActionsDisabled] = useState(false)
|
||||
const [clearPreviewChat, setClearPreviewChat] = useState(false)
|
||||
const [completedBuildConversationId, setCompletedBuildConversationId] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const rightPanelChatControllerRef = useRef<AgentPreviewChatController>(null)
|
||||
const conversationIds = useAtomValue(agentConfigureConversationIdsAtom)
|
||||
const rightPanelChatMode = useAtomValue(agentConfigureRightPanelChatModeAtom)
|
||||
const rightPanelChatMode = rightPanelMode
|
||||
const workingDirectoryPanel = useAgentWorkingDirectoryPanel({
|
||||
agentId,
|
||||
conversationId: conversationIds[rightPanelChatMode],
|
||||
@ -239,13 +299,12 @@ function AgentConfigurePageComposerContent({
|
||||
const showPreviewVersions = useAtomValue(agentConfigureShowPreviewVersionsAtom)
|
||||
const resetConversation = useSetAtom(resetAgentConfigureConversationAtom)
|
||||
const setConversationId = useSetAtom(setAgentConfigureConversationIdAtom)
|
||||
const setRightPanelMode = useSetAtom(agentConfigureRightPanelModeAtom)
|
||||
const setShowChatFeatures = useSetAtom(agentConfigureShowChatFeaturesAtom)
|
||||
const setShowPreviewVersions = useSetAtom(agentConfigureShowPreviewVersionsAtom)
|
||||
const rebaseComposerDraft = useSetAtom(rebaseAgentComposerDraftAtom)
|
||||
const queryClient = useQueryClient()
|
||||
const showBuildDraftBar = buildDraft.isActive
|
||||
const resetBuildChatSession = useCallback(async () => {
|
||||
const resetBuildChatState = useCallback(async () => {
|
||||
try {
|
||||
await onRefreshDebugConversationAsync()
|
||||
} finally {
|
||||
@ -271,6 +330,29 @@ function AgentConfigurePageComposerContent({
|
||||
currentModel,
|
||||
enabled: composerQuery.isSuccess && !selectedVersionId && !buildDraft.isActive,
|
||||
})
|
||||
const {
|
||||
buildCallbackGeneration,
|
||||
buildDraftActionsDisabled,
|
||||
changeMode,
|
||||
confirmSwitchToPreview: confirmSessionSwitchToPreview,
|
||||
finishBuildAction,
|
||||
isEnteringBuildMode,
|
||||
isBuildCallbackCurrent,
|
||||
resetBuildSession: resetSessionController,
|
||||
resetBuildSessionState,
|
||||
runBuildPreparation,
|
||||
setShowSwitchToPreviewConfirm,
|
||||
showSwitchToPreviewConfirm,
|
||||
waitForPendingPreviewDraftSave,
|
||||
} = sessionController
|
||||
const resetBuildSession = useCallback(
|
||||
() => resetSessionController(resetBuildChatState),
|
||||
[resetBuildChatState, resetSessionController],
|
||||
)
|
||||
const saveDraftBeforeBuildRun = useCallback(async () => {
|
||||
await waitForPendingPreviewDraftSave()
|
||||
await saveDraft()
|
||||
}, [saveDraft, waitForPendingPreviewDraftSave])
|
||||
const buildDraftActions = useAgentConfigureBuildDraftActions({
|
||||
agentId,
|
||||
buildDraftAgentSoulConfig: buildDraft.agentSoulConfig,
|
||||
@ -279,11 +361,36 @@ function AgentConfigurePageComposerContent({
|
||||
rebaseComposerDraft: rebaseComposerDraftFromSoulConfig,
|
||||
refetchBuildDraft: buildDraft.refetch,
|
||||
refetchComposer: composerQuery.refetch,
|
||||
resetBuildChatSession,
|
||||
saveDraft,
|
||||
resetBuildChatSession: resetBuildSession,
|
||||
saveDraft: saveDraftBeforeBuildRun,
|
||||
onComposerRebased: onComposerRebase,
|
||||
setSoulSourceOverride: buildDraft.setSoulSourceOverride,
|
||||
})
|
||||
const stopBuildChat = useCallback(() => {
|
||||
rightPanelChatControllerRef.current?.stop()
|
||||
}, [])
|
||||
const changeRightPanelMode = useCallback(
|
||||
(nextMode: AgentConfigureRightPanelMode) =>
|
||||
changeMode(nextMode, {
|
||||
discardBuildDraft: buildDraftActions.discardBuildDraft,
|
||||
rebaseComposerDraft: rebaseComposerDraftFromSoulConfig,
|
||||
savePreviewDraft: saveDraft,
|
||||
startFreshBuildSession: buildDraftActions.startFreshBuildSession,
|
||||
stopBuildChat,
|
||||
}),
|
||||
[
|
||||
buildDraftActions.discardBuildDraft,
|
||||
buildDraftActions.startFreshBuildSession,
|
||||
changeMode,
|
||||
rebaseComposerDraftFromSoulConfig,
|
||||
saveDraft,
|
||||
stopBuildChat,
|
||||
],
|
||||
)
|
||||
const confirmSwitchToPreview = useCallback(
|
||||
() => confirmSessionSwitchToPreview(buildDraftActions.discardBuildDraft, stopBuildChat),
|
||||
[buildDraftActions.discardBuildDraft, confirmSessionSwitchToPreview, stopBuildChat],
|
||||
)
|
||||
const selectVersion = useCallback(
|
||||
(versionId: string | null) => {
|
||||
onSelectVersion(versionId)
|
||||
@ -297,10 +404,15 @@ function AgentConfigurePageComposerContent({
|
||||
const isRestartCurrentChatDisabled =
|
||||
!hasRestartCurrentChatTarget ||
|
||||
buildDraftActionsDisabled ||
|
||||
isEnteringBuildMode ||
|
||||
isRefreshingDebugConversation ||
|
||||
buildDraftActions.isApplyingBuildDraft ||
|
||||
buildDraftActions.isDiscardingBuildDraft
|
||||
const isChatFeaturesReadOnly = (isViewingVersion && versionQuery.isPending) || buildDraft.isActive
|
||||
const isChatFeaturesReadOnly =
|
||||
isEnteringBuildMode ||
|
||||
buildDraftActionsDisabled ||
|
||||
(isViewingVersion && versionQuery.isPending) ||
|
||||
buildDraft.isActive
|
||||
const buildConversationHasAgentResponse =
|
||||
!!conversationIds.build &&
|
||||
(conversationIds.build === completedBuildConversationId ||
|
||||
@ -316,7 +428,12 @@ function AgentConfigurePageComposerContent({
|
||||
return
|
||||
}
|
||||
|
||||
if (rightPanelChatMode === 'build') onRefreshDebugConversation()
|
||||
if (rightPanelChatMode === 'build') {
|
||||
resetBuildSessionState()
|
||||
onRefreshDebugConversation()
|
||||
} else {
|
||||
onRefreshPreviewConversation()
|
||||
}
|
||||
|
||||
resetConversation(rightPanelChatMode)
|
||||
setClearPreviewChat(true)
|
||||
@ -324,7 +441,7 @@ function AgentConfigurePageComposerContent({
|
||||
|
||||
return (
|
||||
<AgentConfigureWorkspace
|
||||
aria-busy={agentQuery.isFetching}
|
||||
aria-busy={agentQuery.isFetching || isEnteringBuildMode}
|
||||
leftPanel={
|
||||
<AgentOrchestratePanel
|
||||
agentId={agentId}
|
||||
@ -336,11 +453,16 @@ function AgentConfigurePageComposerContent({
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
draftSavedAt={draftSavedAt}
|
||||
isPublishing={isPublishing}
|
||||
readOnly={isViewingVersion || buildDraft.isActive}
|
||||
readOnly={
|
||||
isViewingVersion ||
|
||||
buildDraft.isActive ||
|
||||
buildDraftActionsDisabled ||
|
||||
isEnteringBuildMode
|
||||
}
|
||||
selectedVersionSnapshot={isViewingVersion ? activeConfigSnapshot : undefined}
|
||||
isBuildDraftActive={buildDraft.isActive}
|
||||
buildDraftChangedKeys={buildDraft.changedKeys}
|
||||
showPublishBar={!buildDraft.isActive}
|
||||
showPublishBar={!buildDraft.isActive && !isEnteringBuildMode}
|
||||
workflowReferencesEnabled={agentQuery.isSuccess}
|
||||
bottomAction={
|
||||
showBuildDraftBar ? (
|
||||
@ -378,9 +500,9 @@ function AgentConfigurePageComposerContent({
|
||||
header={
|
||||
<AgentPreviewHeader
|
||||
mode={rightPanelChatMode}
|
||||
previewEnabled={false}
|
||||
previewEnabled={previewEnabled}
|
||||
isChatFeaturesOpen={showChatFeatures}
|
||||
onModeChange={setRightPanelMode}
|
||||
onModeChange={changeRightPanelMode}
|
||||
onToggleChatFeatures={() => setShowChatFeatures((open) => !open)}
|
||||
onOpenWorkingDirectory={() => {
|
||||
setShowPreviewVersions(false)
|
||||
@ -392,20 +514,27 @@ function AgentConfigurePageComposerContent({
|
||||
/>
|
||||
}
|
||||
chat={
|
||||
<AgentConfigureRightPanelChat
|
||||
agentId={agentId}
|
||||
agentIcon={agentQuery.data?.icon}
|
||||
agentIconBackground={agentQuery.data?.icon_background}
|
||||
agentIconType={agentIconType}
|
||||
agentName={agentQuery.data?.name}
|
||||
agentSoulConfig={buildDraft.agentSoulConfig}
|
||||
clearChatList={clearPreviewChat}
|
||||
conversationIds={conversationIds}
|
||||
draftType={rightPanelChatMode === 'build' ? 'debug_build' : undefined}
|
||||
mode={rightPanelChatMode}
|
||||
onClearChatListChange={setClearPreviewChat}
|
||||
onConversationComplete={(mode, completedConversationId) => {
|
||||
if (mode === 'build') {
|
||||
buildDraft.isPending || isEnteringBuildMode ? (
|
||||
<Loading type="app" />
|
||||
) : (
|
||||
<AgentConfigureRightPanelChat
|
||||
agentId={agentId}
|
||||
agentIcon={agentQuery.data?.icon}
|
||||
agentIconBackground={agentQuery.data?.icon_background}
|
||||
agentIconType={agentIconType}
|
||||
agentName={agentQuery.data?.name}
|
||||
agentSoulConfig={buildDraft.agentSoulConfig}
|
||||
clearChatList={clearPreviewChat}
|
||||
controllerRef={rightPanelChatControllerRef}
|
||||
conversationIds={conversationIds}
|
||||
mode={rightPanelChatMode}
|
||||
speechToTextDraftType={
|
||||
rightPanelChatMode === 'build' && buildDraft.isActive ? 'debug_build' : 'draft'
|
||||
}
|
||||
onClearChatListChange={setClearPreviewChat}
|
||||
onConversationComplete={(mode, completedConversationId) => {
|
||||
if (mode !== 'build' || !isBuildCallbackCurrent(buildCallbackGeneration)) return
|
||||
|
||||
setCompletedBuildConversationId(completedConversationId)
|
||||
invalidateAgentWorkingDirectoryFiles({
|
||||
agentId,
|
||||
@ -413,40 +542,37 @@ function AgentConfigurePageComposerContent({
|
||||
queryClient,
|
||||
})
|
||||
buildDraftActions.refreshBuildDraftAfterBuildChat(() =>
|
||||
setBuildDraftActionsDisabled(false),
|
||||
finishBuildAction(buildCallbackGeneration),
|
||||
)
|
||||
}}
|
||||
onConversationIdChange={(mode, conversationId) => {
|
||||
if (mode === 'build' && !isBuildCallbackCurrent(buildCallbackGeneration)) return
|
||||
setConversationId({ mode, conversationId })
|
||||
}}
|
||||
onBeforeSpeechToText={
|
||||
rightPanelChatMode === 'build' ? saveDraftBeforeBuildRun : saveDraft
|
||||
}
|
||||
}}
|
||||
onConversationIdChange={(mode, conversationId) => {
|
||||
setConversationId({ mode, conversationId })
|
||||
}}
|
||||
onBeforeSpeechToText={
|
||||
rightPanelChatMode === 'build'
|
||||
? buildDraftActions.prepareBuildDraftBeforeRun
|
||||
: saveDraft
|
||||
}
|
||||
onSaveDraftBeforeRun={
|
||||
rightPanelChatMode === 'build'
|
||||
? async () => {
|
||||
if (!currentModel?.provider || !currentModel.model) {
|
||||
toast.error(tCommon(($) => $['modelProvider.selectModel']))
|
||||
throw new Error('Agent model is required.')
|
||||
}
|
||||
onSaveDraftBeforeRun={
|
||||
rightPanelChatMode === 'build'
|
||||
? async () => {
|
||||
if (!currentModel?.provider || !currentModel.model) {
|
||||
toast.error(tCommon(($) => $['modelProvider.selectModel']))
|
||||
throw new Error('Agent model is required.')
|
||||
}
|
||||
|
||||
setBuildDraftActionsDisabled(true)
|
||||
try {
|
||||
return await buildDraftActions.prepareBuildDraftBeforeRun()
|
||||
} catch (error) {
|
||||
setBuildDraftActionsDisabled(false)
|
||||
throw error
|
||||
return runBuildPreparation({
|
||||
generation: buildCallbackGeneration,
|
||||
markBuildChatStarted: true,
|
||||
prepare: buildDraftActions.prepareBuildDraftBeforeRun,
|
||||
})
|
||||
}
|
||||
}
|
||||
: saveDraft
|
||||
}
|
||||
onSendInterrupted={() => {
|
||||
if (rightPanelChatMode === 'build') setBuildDraftActionsDisabled(false)
|
||||
}}
|
||||
/>
|
||||
: saveDraft
|
||||
}
|
||||
onSendInterrupted={() => {
|
||||
if (rightPanelChatMode === 'build') finishBuildAction(buildCallbackGeneration)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
@ -467,6 +593,12 @@ function AgentConfigurePageComposerContent({
|
||||
disabled={isChatFeaturesReadOnly}
|
||||
onClose={() => setShowChatFeatures(false)}
|
||||
/>
|
||||
<AgentConfigureClearSessionConfirmDialog
|
||||
open={showSwitchToPreviewConfirm}
|
||||
title={t(($) => $['agentDetail.configure.switchToPreviewConfirm.title'])}
|
||||
onOpenChange={setShowSwitchToPreviewConfirm}
|
||||
onConfirm={confirmSwitchToPreview}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactElement } from 'react'
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@ -17,38 +17,64 @@ import { useTranslation } from 'react-i18next'
|
||||
export function AgentConfigureClearSessionConfirmDialog({
|
||||
children,
|
||||
confirmDisabled = false,
|
||||
open: controlledOpen,
|
||||
title,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: {
|
||||
children: ReactElement
|
||||
children?: ReactElement
|
||||
confirmDisabled?: boolean
|
||||
onConfirm: () => void
|
||||
open?: boolean
|
||||
title?: ReactNode
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onConfirm: () => boolean | void | Promise<boolean | void>
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false)
|
||||
const [isConfirming, setIsConfirming] = useState(false)
|
||||
const open = controlledOpen ?? uncontrolledOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (controlledOpen === undefined) setUncontrolledOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!isConfirming) setOpen(nextOpen)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm()
|
||||
setOpen(false)
|
||||
const handleConfirm = async () => {
|
||||
setIsConfirming(true)
|
||||
try {
|
||||
const shouldClose = await onConfirm()
|
||||
if (shouldClose !== false) setOpen(false)
|
||||
} catch {
|
||||
// Keep the dialog open so the user can retry the failed action.
|
||||
} finally {
|
||||
setIsConfirming(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger render={children} />
|
||||
<AlertDialogContent className="w-100">
|
||||
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
||||
{children && <AlertDialogTrigger render={children} />}
|
||||
<AlertDialogContent backdropProps={{ forceRender: true }} className="w-100">
|
||||
<div className="flex flex-col gap-1 p-6 pb-0">
|
||||
<AlertDialogTitle className="title-md-semi-bold text-text-primary">
|
||||
{t(($) => $['agentDetail.configure.clearSessionConfirm.title'])}
|
||||
{title ?? t(($) => $['agentDetail.configure.clearSessionConfirm.title'])}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['agentDetail.configure.clearSessionConfirm.description'])}
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions className="pt-6">
|
||||
<AlertDialogCancelButton disabled={confirmDisabled}>
|
||||
<AlertDialogCancelButton disabled={confirmDisabled || isConfirming}>
|
||||
{tCommon(($) => $['operation.cancel'])}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton disabled={confirmDisabled} onClick={handleConfirm}>
|
||||
<AlertDialogConfirmButton
|
||||
disabled={confirmDisabled || isConfirming}
|
||||
loading={isConfirming}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{tCommon(($) => $['operation.confirm'])}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
import type { AgentChatMessageSender } from '../chat-conversation'
|
||||
import type { AgentChatRuntimeProps } from '../chat-runtime'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { render } from '@/test/console/render'
|
||||
import { AgentBuildChat } from '../build-chat'
|
||||
import { sendBuildChatMessage } from '../build-chat-request'
|
||||
import { AgentPreviewChat } from '../preview-chat'
|
||||
import { sendPreviewChatMessage } from '../preview-chat-request'
|
||||
|
||||
const runtimePropsMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../chat-runtime', () => ({
|
||||
AgentChatRuntime: (
|
||||
props: Pick<AgentChatRuntimeProps, 'draftType'> & { sendMessage: AgentChatMessageSender },
|
||||
) => {
|
||||
runtimePropsMock(props)
|
||||
return null
|
||||
},
|
||||
}))
|
||||
|
||||
const commonProps = {
|
||||
agentId: 'agent-1',
|
||||
clearChatList: false,
|
||||
onClearChatListChange: vi.fn(),
|
||||
}
|
||||
|
||||
describe('Agent chat mode request routing', () => {
|
||||
beforeEach(() => {
|
||||
runtimePropsMock.mockClear()
|
||||
})
|
||||
|
||||
it('should wire Build chat to the Build request implementation', () => {
|
||||
render(<AgentBuildChat {...commonProps} />)
|
||||
|
||||
expect(runtimePropsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
draftType: 'debug_build',
|
||||
sendMessage: sendBuildChatMessage,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should wire Preview chat to the Preview request implementation', () => {
|
||||
render(<AgentPreviewChat {...commonProps} />)
|
||||
|
||||
expect(runtimePropsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sendMessage: sendPreviewChatMessage,
|
||||
}),
|
||||
)
|
||||
expect(runtimePropsMock.mock.calls.at(-1)?.[0]).not.toHaveProperty('draftType')
|
||||
})
|
||||
|
||||
it('should show only the agent name in the Preview empty-state title', () => {
|
||||
render(<AgentPreviewChat {...commonProps} agentName="Research Agent" />)
|
||||
|
||||
const renderEmptyState = runtimePropsMock.mock.calls.at(-1)?.[0].renderEmptyState
|
||||
render(
|
||||
renderEmptyState({
|
||||
agentName: 'Research Agent',
|
||||
hasInstructions: true,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(screen.getByText('Research Agent')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Preview Research Agent')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,11 @@
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import type { AgentPreviewChatController } from '../chat-conversation'
|
||||
import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { createStore, Provider as JotaiProvider } from 'jotai'
|
||||
import { useState } from 'react'
|
||||
import { createRef, useState } from 'react'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
|
||||
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
|
||||
@ -12,7 +13,9 @@ import { consoleQuery } from '@/service/client'
|
||||
import { render } from '@/test/console/render'
|
||||
import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { sendBuildChatMessage } from '../build-chat-request'
|
||||
import { AgentChatRuntime } from '../chat-runtime'
|
||||
import { sendPreviewChatMessage } from '../preview-chat-request'
|
||||
|
||||
const useChatMock = vi.hoisted(() => vi.fn())
|
||||
const handleSendMock = vi.hoisted(() => vi.fn())
|
||||
@ -25,6 +28,18 @@ const sendResultRef = vi.hoisted(() => ({
|
||||
const chatMessagesGetMock = vi.hoisted(() => vi.fn())
|
||||
const suggestedQuestionsGetMock = vi.hoisted(() => vi.fn())
|
||||
const stopPostMock = vi.hoisted(() => vi.fn())
|
||||
const editionState = vi.hoisted(() => ({ isCommunity: true }))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
get IS_CE_EDITION() {
|
||||
return editionState.isCommunity
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/next/dynamic', async () => {
|
||||
const { useState } = await import('react')
|
||||
@ -32,6 +47,7 @@ vi.mock('@/next/dynamic', async () => {
|
||||
return {
|
||||
default: () =>
|
||||
function MockChat(props: {
|
||||
answerActionPosition?: 'auto' | 'below'
|
||||
onSend: (message: string) => unknown
|
||||
onStopResponding: () => void
|
||||
sendButtonLabel?: string
|
||||
@ -62,6 +78,7 @@ vi.mock('@/next/dynamic', async () => {
|
||||
aria-label="chat"
|
||||
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-footer-notice={props.footerNotice ?? ''}
|
||||
data-no-chat-input={String(!!props.noChatInput)}
|
||||
@ -102,9 +119,11 @@ vi.mock('@/next/dynamic', async () => {
|
||||
vi.mock('@/app/components/base/chat/chat/chat-input-area', () => ({
|
||||
default: function MockChatInputArea({
|
||||
footerNotice,
|
||||
footerNoticeTooltip,
|
||||
speechToTextTarget,
|
||||
}: {
|
||||
footerNotice?: ReactNode
|
||||
footerNoticeTooltip?: ReactNode
|
||||
speechToTextTarget?: SpeechToTextTarget
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
@ -126,6 +145,11 @@ vi.mock('@/app/components/base/chat/chat/chat-input-area', () => ({
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
/>
|
||||
{footerNotice}
|
||||
{footerNoticeTooltip !== undefined && footerNoticeTooltip !== null && (
|
||||
<button type="button" aria-label="sandbox notice info">
|
||||
{footerNoticeTooltip}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@ -250,6 +274,7 @@ function renderPreviewChat(props?: Partial<ComponentProps<typeof AgentChatRuntim
|
||||
clearChatList={false}
|
||||
inputPlaceholder="Message agent"
|
||||
renderEmptyState={() => null}
|
||||
sendMessage={sendPreviewChatMessage}
|
||||
onClearChatListChange={vi.fn()}
|
||||
{...props}
|
||||
/>
|
||||
@ -269,6 +294,7 @@ function RuntimeConversationHarness() {
|
||||
conversationId={conversationId}
|
||||
inputPlaceholder="Message agent"
|
||||
renderEmptyState={() => null}
|
||||
sendMessage={sendPreviewChatMessage}
|
||||
onClearChatListChange={vi.fn()}
|
||||
onConversationIdChange={setConversationId}
|
||||
/>
|
||||
@ -284,6 +310,7 @@ function RuntimeClearCommandHarness({ inputPlaceholder }: { inputPlaceholder: st
|
||||
clearChatList={clearChatList}
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
renderEmptyState={() => null}
|
||||
sendMessage={sendPreviewChatMessage}
|
||||
onClearChatListChange={setClearChatList}
|
||||
/>
|
||||
)
|
||||
@ -346,15 +373,58 @@ function renderPreviewChatWithClearCommandHarness() {
|
||||
|
||||
describe('AgentPreviewChat', () => {
|
||||
beforeEach(() => {
|
||||
editionState.isCommunity = true
|
||||
useChatMock.mockClear()
|
||||
handleSendMock.mockClear()
|
||||
chatMessagesGetMock.mockResolvedValue({ data: [] })
|
||||
suggestedQuestionsGetMock.mockResolvedValue({ data: [] })
|
||||
stopPostMock.mockClear()
|
||||
stopPostMock.mockResolvedValue({ result: 'success' })
|
||||
stopCallbackRef.current = undefined
|
||||
sendResultRef.current = undefined
|
||||
})
|
||||
|
||||
it('should keep build and preview chat requests independently replaceable', () => {
|
||||
const buildHandleSend = vi.fn()
|
||||
const previewHandleSend = vi.fn()
|
||||
const callbacks = {
|
||||
onSendSettled: vi.fn(),
|
||||
}
|
||||
const buildData = {
|
||||
query: 'Build an agent',
|
||||
draft_type: 'debug_build',
|
||||
}
|
||||
const previewData = {
|
||||
query: 'Preview the agent',
|
||||
}
|
||||
|
||||
expect(sendBuildChatMessage).not.toBe(sendPreviewChatMessage)
|
||||
|
||||
sendBuildChatMessage({
|
||||
agentId: 'agent-1',
|
||||
callbacks,
|
||||
data: buildData,
|
||||
handleSend: buildHandleSend,
|
||||
})
|
||||
sendPreviewChatMessage({
|
||||
agentId: 'agent-1',
|
||||
callbacks,
|
||||
data: previewData,
|
||||
handleSend: previewHandleSend,
|
||||
})
|
||||
|
||||
expect(buildHandleSend).toHaveBeenCalledWith(
|
||||
'agent/agent-1/chat-messages',
|
||||
buildData,
|
||||
callbacks,
|
||||
)
|
||||
expect(previewHandleSend).toHaveBeenCalledWith(
|
||||
'agent/agent-1/chat-messages',
|
||||
previewData,
|
||||
callbacks,
|
||||
)
|
||||
})
|
||||
|
||||
it('should bind Agent preview voice input to the normal Agent draft', () => {
|
||||
renderPreviewChat({
|
||||
renderEmptyState: () => null,
|
||||
@ -394,9 +464,30 @@ describe('AgentPreviewChat', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should bind pre-checkout Build voice input to the normal Agent draft', () => {
|
||||
renderPreviewChat({
|
||||
draftType: 'debug_build',
|
||||
speechToTextDraftType: 'draft',
|
||||
renderEmptyState: () => null,
|
||||
})
|
||||
|
||||
expect(screen.getByRole('group', { name: 'voice input' })).toHaveAttribute(
|
||||
'data-speech-draft-type',
|
||||
'draft',
|
||||
)
|
||||
expect(screen.getByRole('region', { name: 'chat' })).toHaveAttribute(
|
||||
'data-speech-draft-type',
|
||||
'draft',
|
||||
)
|
||||
})
|
||||
|
||||
it('should keep answer regeneration available when the chat input is external', () => {
|
||||
renderPreviewChat()
|
||||
|
||||
expect(screen.getByRole('region', { name: 'chat' })).toHaveAttribute(
|
||||
'data-answer-action-position',
|
||||
'auto',
|
||||
)
|
||||
expect(screen.getByRole('region', { name: 'chat' })).toHaveAttribute(
|
||||
'data-no-chat-input',
|
||||
'true',
|
||||
@ -620,6 +711,7 @@ describe('AgentPreviewChat', () => {
|
||||
expect(handleSendMock).toHaveBeenCalledWith(
|
||||
'agent/agent-1/chat-messages',
|
||||
expect.not.objectContaining({
|
||||
draft_type: expect.anything(),
|
||||
model_config: expect.anything(),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@ -1015,6 +1107,23 @@ describe('AgentPreviewChat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should stop the active SSE through the runtime controller', async () => {
|
||||
const controllerRef = createRef<AgentPreviewChatController>()
|
||||
renderPreviewChat({ controllerRef })
|
||||
|
||||
await waitFor(() => expect(controllerRef.current).not.toBeNull())
|
||||
act(() => {
|
||||
controllerRef.current?.stop()
|
||||
})
|
||||
|
||||
expect(stopPostMock).toHaveBeenCalledWith({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
task_id: 'task-1',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify the owner once when a stopped send later settles with an error', async () => {
|
||||
const onSendInterrupted = vi.fn()
|
||||
renderPreviewChat({
|
||||
@ -1050,6 +1159,7 @@ describe('AgentPreviewChat', () => {
|
||||
it('should send build chat with the debug build draft type', async () => {
|
||||
renderPreviewChat({
|
||||
draftType: 'debug_build',
|
||||
sendMessage: sendBuildChatMessage,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'send' }))
|
||||
@ -1085,6 +1195,7 @@ describe('AgentPreviewChat', () => {
|
||||
expect(
|
||||
screen.getByText('agentV2.agentDetail.configure.preview.sandboxNotice'),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'sandbox notice info' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'send' }))
|
||||
|
||||
@ -1095,6 +1206,19 @@ describe('AgentPreviewChat', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide only the sandbox notice infotip outside community edition', () => {
|
||||
editionState.isCommunity = false
|
||||
|
||||
renderPreviewChat({
|
||||
renderEmptyState: () => null,
|
||||
})
|
||||
|
||||
expect(
|
||||
screen.getByText('agentV2.agentDetail.configure.preview.sandboxNotice'),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'sandbox notice info' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should send build chat inputs from the prepared build draft snapshot', async () => {
|
||||
const saveDraftBeforeRun = vi.fn().mockResolvedValue({
|
||||
app_variables: [
|
||||
@ -1125,6 +1249,7 @@ describe('AgentPreviewChat', () => {
|
||||
],
|
||||
},
|
||||
draftType: 'debug_build',
|
||||
sendMessage: sendBuildChatMessage,
|
||||
onSaveDraftBeforeRun: saveDraftBeforeRun,
|
||||
})
|
||||
|
||||
|
||||
@ -62,10 +62,23 @@ describe('AgentPreviewHeader', () => {
|
||||
expect(onRefresh).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should emit refresh without confirmation in preview mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onRefresh = vi.fn()
|
||||
renderHeader({ mode: 'preview', onRefresh })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'agentV2.agentDetail.configure.preview.restart' }),
|
||||
)
|
||||
|
||||
expect(onRefresh).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not emit refresh when the restart button is disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onRefresh = vi.fn()
|
||||
renderHeader({ mode: 'build', onRefresh, refreshDisabled: true })
|
||||
renderHeader({ mode: 'preview', onRefresh, refreshDisabled: true })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'agentV2.agentDetail.configure.preview.restart' }),
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import type { AgentChatMessageRequest } from './chat-conversation'
|
||||
|
||||
export function sendBuildChatMessage({
|
||||
agentId,
|
||||
callbacks,
|
||||
data,
|
||||
handleSend,
|
||||
}: AgentChatMessageRequest) {
|
||||
return handleSend(`agent/${agentId}/chat-messages`, data, callbacks)
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
import type { AgentChatRuntimeProps } from './chat-runtime'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CommunityEditionTip } from '../community-edition-tip'
|
||||
import { sendBuildChatMessage } from './build-chat-request'
|
||||
import { AgentChatRuntime } from './chat-runtime'
|
||||
|
||||
const buildIconGridCellOpacities = [
|
||||
@ -23,7 +24,7 @@ const buildIconGridCells = buildIconGridCellOpacities.map((opacity, index) => ({
|
||||
|
||||
type AgentBuildChatProps = Omit<
|
||||
AgentChatRuntimeProps,
|
||||
'inputPlaceholder' | 'renderEmptyState' | 'sendButtonLabel'
|
||||
'draftType' | 'inputPlaceholder' | 'renderEmptyState' | 'sendButtonLabel' | 'sendMessage'
|
||||
>
|
||||
|
||||
function AgentBuildChatEmptyState() {
|
||||
@ -72,9 +73,11 @@ export function AgentBuildChat(props: AgentBuildChatProps) {
|
||||
return (
|
||||
<AgentChatRuntime
|
||||
{...props}
|
||||
draftType="debug_build"
|
||||
inputPlaceholder={t(($) => $['agentDetail.configure.build.inputPlaceholder'])}
|
||||
inputAutoFocus={false}
|
||||
sendButtonLabel={t(($) => $['agentDetail.configure.build.startBuild'])}
|
||||
sendMessage={sendBuildChatMessage}
|
||||
renderEmptyState={() => <AgentBuildChatEmptyState />}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { Ref } from 'react'
|
||||
import type { AgentPreviewChatConfig } from './chat-config'
|
||||
import type { AnswerActionPosition } from '@/app/components/base/chat/chat/answer/operation'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
@ -44,6 +45,19 @@ const fetchAgentSuggestedQuestions = (agentId: string, messageId: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
type AgentChatHandleSend = ReturnType<typeof useChat>['handleSend']
|
||||
|
||||
export type AgentChatMessageRequest = {
|
||||
agentId: string
|
||||
callbacks: Parameters<AgentChatHandleSend>[2]
|
||||
data: Parameters<AgentChatHandleSend>[1]
|
||||
handleSend: AgentChatHandleSend
|
||||
}
|
||||
|
||||
export type AgentChatMessageSender = (
|
||||
request: AgentChatMessageRequest,
|
||||
) => ReturnType<AgentChatHandleSend>
|
||||
|
||||
export type AgentPreviewChatRuntimeState = {
|
||||
isEmptyChat: boolean
|
||||
isResponding: boolean
|
||||
@ -52,11 +66,13 @@ export type AgentPreviewChatRuntimeState = {
|
||||
|
||||
export type AgentPreviewChatController = {
|
||||
send: OnSend
|
||||
stop: () => void
|
||||
}
|
||||
|
||||
export function AgentPreviewChatConversation({
|
||||
ref,
|
||||
agentId,
|
||||
answerActionPosition,
|
||||
agentSoulConfig,
|
||||
clearChatList,
|
||||
config,
|
||||
@ -67,6 +83,7 @@ export function AgentPreviewChatConversation({
|
||||
inputs,
|
||||
inputsForm,
|
||||
sendButtonLabel,
|
||||
sendMessage,
|
||||
speechToTextTarget,
|
||||
onBeforeSpeechToText,
|
||||
onClearChatListChange,
|
||||
@ -79,6 +96,7 @@ export function AgentPreviewChatConversation({
|
||||
}: {
|
||||
ref: Ref<AgentPreviewChatController>
|
||||
agentId: string
|
||||
answerActionPosition?: AnswerActionPosition
|
||||
agentSoulConfig?: AgentSoulConfig
|
||||
clearChatList: boolean
|
||||
config: AgentPreviewChatConfig
|
||||
@ -89,6 +107,7 @@ export function AgentPreviewChatConversation({
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
sendButtonLabel?: string
|
||||
sendMessage: AgentChatMessageSender
|
||||
speechToTextTarget: SpeechToTextTarget
|
||||
onBeforeSpeechToText?: () => Promise<unknown>
|
||||
onClearChatListChange: (clearChatList: boolean) => void
|
||||
@ -178,44 +197,49 @@ export function AgentPreviewChatConversation({
|
||||
|
||||
if (files?.length && supportVision) data.files = files
|
||||
|
||||
handleSend(`agent/${agentId}/chat-messages`, data as Parameters<typeof handleSend>[1], {
|
||||
onGetConversationMessages: async (conversationId) => {
|
||||
return queryClient.fetchQuery({
|
||||
...consoleQuery.agent.byAgentId.chatMessages.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
sendMessage({
|
||||
agentId,
|
||||
data: data as Parameters<typeof handleSend>[1],
|
||||
handleSend,
|
||||
callbacks: {
|
||||
onGetConversationMessages: async (conversationId) => {
|
||||
return queryClient.fetchQuery({
|
||||
...consoleQuery.agent.byAgentId.chatMessages.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
query: {
|
||||
conversation_id: conversationId,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
conversation_id: conversationId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
staleTime: 0,
|
||||
})
|
||||
},
|
||||
onGetSuggestedQuestions: (responseItemId) =>
|
||||
fetchAgentSuggestedQuestions(agentId, responseItemId),
|
||||
onUnhandledEvent: (event) => {
|
||||
if (event.event !== 'error' || typeof event.message !== 'string') return
|
||||
}),
|
||||
staleTime: 0,
|
||||
})
|
||||
},
|
||||
onGetSuggestedQuestions: (responseItemId) =>
|
||||
fetchAgentSuggestedQuestions(agentId, responseItemId),
|
||||
onUnhandledEvent: (event) => {
|
||||
if (event.event !== 'error' || typeof event.message !== 'string') return
|
||||
|
||||
return {
|
||||
conversationId:
|
||||
typeof event.conversation_id === 'string' ? event.conversation_id : undefined,
|
||||
messageId: typeof event.message_id === 'string' ? event.message_id : undefined,
|
||||
errorMessage: event.message,
|
||||
errorCode: typeof event.code === 'string' ? event.code : undefined,
|
||||
}
|
||||
},
|
||||
onConversationComplete: (completedConversationId, workflowRunId) => {
|
||||
if (completedConversationId && completedConversationId !== conversationId)
|
||||
onCurrentSessionConversationIdChange(completedConversationId)
|
||||
onConversationIdChange?.(completedConversationId)
|
||||
onConversationComplete?.(completedConversationId, workflowRunId)
|
||||
},
|
||||
onSendSettled: (hasError) => {
|
||||
setIsSendPending(false)
|
||||
if (hasError) notifySendInterrupted()
|
||||
return {
|
||||
conversationId:
|
||||
typeof event.conversation_id === 'string' ? event.conversation_id : undefined,
|
||||
messageId: typeof event.message_id === 'string' ? event.message_id : undefined,
|
||||
errorMessage: event.message,
|
||||
errorCode: typeof event.code === 'string' ? event.code : undefined,
|
||||
}
|
||||
},
|
||||
onConversationComplete: (completedConversationId, workflowRunId) => {
|
||||
if (completedConversationId && completedConversationId !== conversationId)
|
||||
onCurrentSessionConversationIdChange(completedConversationId)
|
||||
onConversationIdChange?.(completedConversationId)
|
||||
onConversationComplete?.(completedConversationId, workflowRunId)
|
||||
},
|
||||
onSendSettled: (hasError) => {
|
||||
setIsSendPending(false)
|
||||
if (hasError) notifySendInterrupted()
|
||||
},
|
||||
},
|
||||
})
|
||||
sendStarted = true
|
||||
@ -241,6 +265,7 @@ export function AgentPreviewChatConversation({
|
||||
onCurrentSessionConversationIdChange,
|
||||
onSaveDraftBeforeRun,
|
||||
queryClient,
|
||||
sendMessage,
|
||||
textGenerationModelList,
|
||||
],
|
||||
)
|
||||
@ -269,7 +294,10 @@ export function AgentPreviewChatConversation({
|
||||
)
|
||||
const isEmptyChat = chatList.length === 0
|
||||
const sendButtonLoading = isEmptyChat && !!sendButtonLabel && (isSendPending || isResponding)
|
||||
useImperativeHandle(ref, () => ({ send: doSend }), [doSend])
|
||||
useImperativeHandle(ref, () => ({ send: doSend, stop: doStopResponding }), [
|
||||
doSend,
|
||||
doStopResponding,
|
||||
])
|
||||
useLayoutEffect(() => {
|
||||
onRuntimeStateChange({
|
||||
isEmptyChat,
|
||||
@ -280,6 +308,7 @@ export function AgentPreviewChatConversation({
|
||||
|
||||
return (
|
||||
<Chat
|
||||
answerActionPosition={answerActionPosition}
|
||||
config={config}
|
||||
speechToTextTarget={speechToTextTarget}
|
||||
onBeforeSpeechToText={onBeforeSpeechToText}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ReactNode, Ref } from 'react'
|
||||
import type { AgentChatMessageSender, AgentPreviewChatController } from './chat-conversation'
|
||||
import type { AnswerActionPosition } from '@/app/components/base/chat/chat/answer/operation'
|
||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@ -19,18 +21,22 @@ export type AgentChatRuntimeEmptyStateProps = {
|
||||
|
||||
export type AgentChatRuntimeProps = {
|
||||
agentId: string
|
||||
answerActionPosition?: AnswerActionPosition
|
||||
agentIcon?: string | null
|
||||
agentIconBackground?: string | null
|
||||
agentIconType?: AgentIconType | null
|
||||
agentName?: string
|
||||
agentSoulConfig?: AgentSoulConfig
|
||||
clearChatList: boolean
|
||||
controllerRef?: Ref<AgentPreviewChatController>
|
||||
conversationId?: string | null
|
||||
draftType?: 'debug_build'
|
||||
speechToTextDraftType?: 'draft' | 'debug_build'
|
||||
inputPlaceholder: string
|
||||
inputAutoFocus?: boolean
|
||||
sendButtonLabel?: string
|
||||
renderEmptyState: (props: AgentChatRuntimeEmptyStateProps) => ReactNode
|
||||
sendMessage: AgentChatMessageSender
|
||||
onClearChatListChange: (clearChatList: boolean) => void
|
||||
onConversationComplete?: (conversationId: string, workflowRunId?: string) => void
|
||||
onConversationIdChange?: (conversationId: string) => void
|
||||
@ -42,18 +48,22 @@ export type AgentChatRuntimeProps = {
|
||||
|
||||
export function AgentChatRuntime({
|
||||
agentId,
|
||||
answerActionPosition,
|
||||
agentIcon,
|
||||
agentIconBackground,
|
||||
agentIconType,
|
||||
agentName,
|
||||
agentSoulConfig,
|
||||
clearChatList,
|
||||
controllerRef,
|
||||
conversationId,
|
||||
draftType,
|
||||
speechToTextDraftType,
|
||||
inputPlaceholder,
|
||||
inputAutoFocus,
|
||||
sendButtonLabel,
|
||||
renderEmptyState,
|
||||
sendMessage,
|
||||
onClearChatListChange,
|
||||
onConversationComplete,
|
||||
onConversationIdChange,
|
||||
@ -117,19 +127,23 @@ export function AgentChatRuntime({
|
||||
key={inputSessionKey}
|
||||
conversationSessionKey={conversationSessionKey}
|
||||
agentId={agentId}
|
||||
answerActionPosition={answerActionPosition}
|
||||
agentIcon={agentIcon}
|
||||
agentIconBackground={agentIconBackground}
|
||||
agentIconType={agentIconType}
|
||||
agentName={agentName}
|
||||
agentSoulConfig={agentSoulConfig}
|
||||
clearChatList={clearChatList}
|
||||
controllerRef={controllerRef}
|
||||
conversationId={conversationId}
|
||||
draftType={draftType}
|
||||
speechToTextDraftType={speechToTextDraftType}
|
||||
initialChatTree={initialChatTree}
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
inputAutoFocus={inputAutoFocus}
|
||||
sendButtonLabel={sendButtonLabel}
|
||||
renderEmptyState={renderEmptyState}
|
||||
sendMessage={sendMessage}
|
||||
onClearChatListChange={handleClearChatListChange}
|
||||
onConversationComplete={onConversationComplete}
|
||||
onConversationIdChange={onConversationIdChange}
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { AgentPreviewChatController, AgentPreviewChatRuntimeState } from './chat-conversation'
|
||||
import type { AgentChatRuntimeEmptyStateProps } from './chat-runtime'
|
||||
import type { ReactNode, Ref } from 'react'
|
||||
import type {
|
||||
AgentChatMessageSender,
|
||||
AgentPreviewChatController,
|
||||
AgentPreviewChatRuntimeState,
|
||||
} from './chat-conversation'
|
||||
import type { AgentChatRuntimeEmptyStateProps, AgentChatRuntimeProps } from './chat-runtime'
|
||||
import type { ChatItem, ChatItemInTree, OnSend } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { SpeechToTextTarget } from '@/app/components/base/voice-input/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ChatInputArea from '@/app/components/base/chat/chat/chat-input-area'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
|
||||
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
|
||||
import { buildChatConfig, getAgentSoulInputs, getAgentSoulInputsForm } from './chat-config'
|
||||
@ -20,19 +25,23 @@ import { AgentPreviewChatConversation } from './chat-conversation'
|
||||
export function AgentPreviewChatSession({
|
||||
conversationSessionKey,
|
||||
agentId,
|
||||
answerActionPosition,
|
||||
agentIcon,
|
||||
agentIconBackground,
|
||||
agentIconType,
|
||||
agentName,
|
||||
agentSoulConfig,
|
||||
clearChatList,
|
||||
controllerRef,
|
||||
conversationId,
|
||||
draftType,
|
||||
speechToTextDraftType,
|
||||
initialChatTree,
|
||||
inputPlaceholder,
|
||||
inputAutoFocus,
|
||||
sendButtonLabel,
|
||||
renderEmptyState,
|
||||
sendMessage,
|
||||
onClearChatListChange,
|
||||
onConversationComplete,
|
||||
onConversationIdChange,
|
||||
@ -43,19 +52,23 @@ export function AgentPreviewChatSession({
|
||||
}: {
|
||||
conversationSessionKey: string
|
||||
agentId: string
|
||||
answerActionPosition?: AgentChatRuntimeProps['answerActionPosition']
|
||||
agentIcon?: string | null
|
||||
agentIconBackground?: string | null
|
||||
agentIconType?: AgentIconType | null
|
||||
agentName?: string
|
||||
agentSoulConfig?: AgentSoulConfig
|
||||
clearChatList: boolean
|
||||
controllerRef?: Ref<AgentPreviewChatController>
|
||||
conversationId?: string | null
|
||||
draftType?: 'debug_build'
|
||||
speechToTextDraftType?: 'draft' | 'debug_build'
|
||||
initialChatTree: ChatItemInTree[]
|
||||
inputPlaceholder: string
|
||||
inputAutoFocus?: boolean
|
||||
sendButtonLabel?: string
|
||||
renderEmptyState: (props: AgentChatRuntimeEmptyStateProps) => ReactNode
|
||||
sendMessage: AgentChatMessageSender
|
||||
onClearChatListChange: (clearChatList: boolean) => void
|
||||
onConversationComplete?: (conversationId: string, workflowRunId?: string) => void
|
||||
onConversationIdChange?: (conversationId: string) => void
|
||||
@ -105,6 +118,14 @@ export function AgentPreviewChatSession({
|
||||
) => conversationRef.current?.send(message, files, isRegenerate, parentAnswer),
|
||||
[],
|
||||
)
|
||||
useImperativeHandle(
|
||||
controllerRef,
|
||||
() => ({
|
||||
send: handleInputSend,
|
||||
stop: () => conversationRef.current?.stop(),
|
||||
}),
|
||||
[handleInputSend],
|
||||
)
|
||||
const { isEmptyChat, isResponding, isSendPending } = runtimeState
|
||||
const hasInstructions = !!config.pre_prompt.trim()
|
||||
const sendButtonLoading = isEmptyChat && !!sendButtonLabel && (isSendPending || isResponding)
|
||||
@ -114,7 +135,7 @@ export function AgentPreviewChatSession({
|
||||
const speechToTextTarget: SpeechToTextTarget = {
|
||||
type: 'agent',
|
||||
agentId,
|
||||
draftType: draftType ?? 'draft',
|
||||
draftType: speechToTextDraftType ?? draftType ?? 'draft',
|
||||
}
|
||||
const chatInputNode = (
|
||||
<ChatInputArea
|
||||
@ -136,7 +157,7 @@ export function AgentPreviewChatSession({
|
||||
isResponding={isEmptyChat ? undefined : isResponding}
|
||||
sendButtonLabel={isEmptyChat ? sendButtonLabel : undefined}
|
||||
footerNotice={showSandboxNotice ? sandboxNotice : undefined}
|
||||
footerNoticeTooltip={showSandboxNotice ? sandboxNoticeTooltip : undefined}
|
||||
footerNoticeTooltip={showSandboxNotice && IS_CE_EDITION ? sandboxNoticeTooltip : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -147,6 +168,7 @@ export function AgentPreviewChatSession({
|
||||
key={conversationSessionKey}
|
||||
ref={conversationRef}
|
||||
agentId={agentId}
|
||||
answerActionPosition={answerActionPosition}
|
||||
agentSoulConfig={agentSoulConfig}
|
||||
clearChatList={clearChatList}
|
||||
config={config}
|
||||
@ -157,6 +179,7 @@ export function AgentPreviewChatSession({
|
||||
inputs={inputs}
|
||||
inputsForm={inputsForm}
|
||||
sendButtonLabel={sendButtonLabel}
|
||||
sendMessage={sendMessage}
|
||||
speechToTextTarget={speechToTextTarget}
|
||||
onBeforeSpeechToText={onBeforeSpeechToText}
|
||||
onClearChatListChange={onClearChatListChange}
|
||||
|
||||
@ -139,6 +139,17 @@ export function AgentPreviewHeader({
|
||||
const previewDisabledTip = t(($) => $['agentDetail.configure.rightPanel.previewDisabledTip'])
|
||||
const learnMoreLabel = t(($) => $['agentDetail.configure.rightPanel.learnMore'])
|
||||
const modeTip = `${buildLabel}. ${buildTipBody} ${learnMoreLabel} ${previewLabel}. ${previewTipBody}`
|
||||
const restartButton = (
|
||||
<button
|
||||
type="button"
|
||||
disabled={refreshDisabled}
|
||||
onClick={mode === 'preview' ? onRefresh : undefined}
|
||||
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'])}
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-other-replay-line size-4" />
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative z-1 flex h-12 shrink-0 items-center justify-between gap-3 px-4 py-2">
|
||||
@ -191,16 +202,13 @@ export function AgentPreviewHeader({
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentConfigureClearSessionConfirmDialog onConfirm={onRefresh}>
|
||||
<button
|
||||
type="button"
|
||||
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'])}
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-other-replay-line size-4" />
|
||||
</button>
|
||||
</AgentConfigureClearSessionConfirmDialog>
|
||||
{mode === 'preview' ? (
|
||||
restartButton
|
||||
) : (
|
||||
<AgentConfigureClearSessionConfirmDialog onConfirm={onRefresh}>
|
||||
{restartButton}
|
||||
</AgentConfigureClearSessionConfirmDialog>
|
||||
)}
|
||||
{mode === 'build' && showWorkingDirectoryAction && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import type { AgentChatMessageRequest } from './chat-conversation'
|
||||
|
||||
export function sendPreviewChatMessage({
|
||||
agentId,
|
||||
callbacks,
|
||||
data,
|
||||
handleSend,
|
||||
}: AgentChatMessageRequest) {
|
||||
return handleSend(`agent/${agentId}/chat-messages`, data, callbacks)
|
||||
}
|
||||
@ -4,8 +4,12 @@ import type { AgentChatRuntimeEmptyStateProps, AgentChatRuntimeProps } from './c
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { AgentChatRuntime } from './chat-runtime'
|
||||
import { sendPreviewChatMessage } from './preview-chat-request'
|
||||
|
||||
type AgentPreviewChatProps = Omit<AgentChatRuntimeProps, 'inputPlaceholder' | 'renderEmptyState'>
|
||||
type AgentPreviewChatProps = Omit<
|
||||
AgentChatRuntimeProps,
|
||||
'draftType' | 'inputPlaceholder' | 'renderEmptyState' | 'sendMessage'
|
||||
>
|
||||
|
||||
function AgentPreviewChatEmptyState({
|
||||
agentIcon,
|
||||
@ -30,9 +34,7 @@ function AgentPreviewChatEmptyState({
|
||||
className="bg-background-default"
|
||||
/>
|
||||
<div className="mt-3 max-w-full truncate system-md-medium text-text-secondary">
|
||||
{t(($) => $['agentDetail.configure.preview.empty.title'], {
|
||||
name: agentName || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName']),
|
||||
})}
|
||||
{agentName || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName'])}
|
||||
</div>
|
||||
<p className="mt-1 max-w-full body-md-regular text-text-tertiary">
|
||||
{t(($) => $['agentDetail.configure.preview.empty.description'])}
|
||||
@ -57,6 +59,7 @@ export function AgentPreviewChat(props: AgentPreviewChatProps) {
|
||||
inputPlaceholder={t(($) => $['agentDetail.configure.preview.inputPlaceholder'], {
|
||||
name: agentName,
|
||||
})}
|
||||
sendMessage={sendPreviewChatMessage}
|
||||
renderEmptyState={(emptyStateProps) => <AgentPreviewChatEmptyState {...emptyStateProps} />}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentConfigureRightPanelMode } from './state'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { ScopeProvider } from 'jotai-scope'
|
||||
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
||||
import { Suspense, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { LicenseStatus } from '@/features/system-features/constants'
|
||||
import { AgentConfigureComposerScope } from './components/composer-session'
|
||||
import { AgentConfigurePageLoading } from './components/page-loading'
|
||||
import { useAgentConfigureData } from './hooks'
|
||||
import {
|
||||
AGENT_CONFIGURE_RIGHT_PANEL_MODES,
|
||||
agentConfigureComposerRebaseRevisionAtom,
|
||||
agentConfigureScopedAtoms,
|
||||
agentConfigureSelectedVersionIdAtom,
|
||||
@ -14,28 +22,62 @@ import {
|
||||
rebaseAgentConfigureComposerAtom,
|
||||
} from './state'
|
||||
|
||||
const agentConfigureModeQueryParser = parseAsStringLiteral(
|
||||
AGENT_CONFIGURE_RIGHT_PANEL_MODES,
|
||||
).withOptions({ history: 'replace' })
|
||||
|
||||
type AgentConfigurePageProps = {
|
||||
agentId: string
|
||||
}
|
||||
|
||||
export function AgentConfigurePage({ agentId }: AgentConfigurePageProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const loadingLabel = t(($) => $['agentDetail.sections.configure'])
|
||||
|
||||
return (
|
||||
<ScopeProvider key={agentId} atoms={agentConfigureScopedAtoms} name="AgentConfigure">
|
||||
<AgentConfigurePageContent agentId={agentId} />
|
||||
</ScopeProvider>
|
||||
<Suspense fallback={<AgentConfigurePageLoading label={loadingLabel} />}>
|
||||
<ScopeProvider key={agentId} atoms={agentConfigureScopedAtoms} name="AgentConfigure">
|
||||
<AgentConfigurePageContent agentId={agentId} loadingLabel={loadingLabel} />
|
||||
</ScopeProvider>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentConfigurePageContent({ agentId }: AgentConfigurePageProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
function AgentConfigurePageContent({
|
||||
agentId,
|
||||
loadingLabel,
|
||||
}: AgentConfigurePageProps & { loadingLabel: string }) {
|
||||
const [modeInUrl, setModeInUrl] = useQueryState('mode', agentConfigureModeQueryParser)
|
||||
const selectedVersionId = useAtomValue(agentConfigureSelectedVersionIdAtom)
|
||||
const composerRebaseRevision = useAtomValue(agentConfigureComposerRebaseRevisionAtom)
|
||||
const rebaseComposer = useSetAtom(rebaseAgentConfigureComposerAtom)
|
||||
const selectVersion = useSetAtom(agentConfigureSelectVersionAtom)
|
||||
const configureData = useAgentConfigureData(agentId, selectedVersionId)
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const previewEnabled =
|
||||
!IS_CE_EDITION ||
|
||||
systemFeatures.license.status === LicenseStatus.ACTIVE ||
|
||||
systemFeatures.license.status === LicenseStatus.EXPIRING
|
||||
const requestedMode = modeInUrl ?? 'build'
|
||||
const rightPanelMode = previewEnabled ? requestedMode : 'build'
|
||||
const changeRightPanelMode = useCallback(
|
||||
(nextMode: AgentConfigureRightPanelMode) => {
|
||||
if (nextMode === 'preview' && !previewEnabled) return
|
||||
|
||||
return setModeInUrl(nextMode)
|
||||
},
|
||||
[previewEnabled, setModeInUrl],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (modeInUrl === rightPanelMode) return
|
||||
|
||||
// oxlint-disable-next-line eslint-react/set-state-in-effect -- The URL is external state and must mirror the effective mode after parsing and license gating.
|
||||
void setModeInUrl(rightPanelMode, { history: 'replace' })
|
||||
}, [modeInUrl, rightPanelMode, setModeInUrl])
|
||||
|
||||
if (configureData.isPending) {
|
||||
return <AgentConfigurePageLoading label={t(($) => $['agentDetail.sections.configure'])} />
|
||||
return <AgentConfigurePageLoading label={loadingLabel} />
|
||||
}
|
||||
|
||||
return (
|
||||
@ -43,7 +85,10 @@ function AgentConfigurePageContent({ agentId }: AgentConfigurePageProps) {
|
||||
agentId={agentId}
|
||||
composerRebaseRevision={composerRebaseRevision}
|
||||
configureData={configureData}
|
||||
previewEnabled={previewEnabled}
|
||||
rightPanelMode={rightPanelMode}
|
||||
onComposerRebase={rebaseComposer}
|
||||
onRightPanelModeChange={changeRightPanelMode}
|
||||
onSelectVersion={selectVersion}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { atom } from 'jotai'
|
||||
|
||||
export type AgentConfigureRightPanelMode = 'build' | 'preview'
|
||||
export const AGENT_CONFIGURE_RIGHT_PANEL_MODES = ['build', 'preview'] as const
|
||||
|
||||
export type AgentConfigureRightPanelMode = (typeof AGENT_CONFIGURE_RIGHT_PANEL_MODES)[number]
|
||||
export type AgentConfigureConversationIds = Record<AgentConfigureRightPanelMode, string | null>
|
||||
export type AgentConfigureSoulSource = 'draft' | 'build-draft' | 'view-version'
|
||||
|
||||
@ -16,12 +18,6 @@ export const agentConfigureConversationIdsAtom = atom<AgentConfigureConversation
|
||||
preview: null,
|
||||
})
|
||||
|
||||
export const agentConfigureRightPanelChatModeAtom = atom((get): AgentConfigureRightPanelMode => {
|
||||
const mode = get(agentConfigureRightPanelModeAtom)
|
||||
|
||||
return mode === 'preview' ? 'build' : mode
|
||||
})
|
||||
|
||||
export const agentConfigureSelectVersionAtom = atom(null, (_get, set, versionId: string | null) => {
|
||||
set(agentConfigureSoulSourceOverrideAtom, versionId ? 'view-version' : null)
|
||||
set(agentConfigureSelectedVersionIdAtom, versionId)
|
||||
@ -67,5 +63,9 @@ export const agentConfigureScopedAtoms = [
|
||||
agentConfigureSoulSourceOverrideAtom,
|
||||
agentConfigureShowChatFeaturesAtom,
|
||||
agentConfigureShowPreviewVersionsAtom,
|
||||
] as const
|
||||
|
||||
export const workflowInlineAgentConfigureScopedAtoms = [
|
||||
...agentConfigureScopedAtoms,
|
||||
agentConfigureRightPanelModeAtom,
|
||||
] as const
|
||||
|
||||
@ -35,38 +35,55 @@ export function usePrepareAgentBuildDraftBeforeRun({
|
||||
const { mutateAsync: checkoutBuildDraft, isPending: isCheckingOutBuildDraft } =
|
||||
checkoutBuildDraftMutation
|
||||
|
||||
const checkoutBuildDraftFromNormalDraft = useCallback(
|
||||
async (force: boolean) => {
|
||||
if (!agentId) return
|
||||
|
||||
const buildDraft = await checkoutBuildDraft({
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
body: {
|
||||
force,
|
||||
},
|
||||
})
|
||||
queryClient.setQueryData(buildDraftQueryOptions.queryKey, buildDraft)
|
||||
rebaseComposerDraft?.(buildDraft.agent_soul as AgentSoulConfig | undefined)
|
||||
setSoulSourceOverride?.('build-draft')
|
||||
return buildDraft.agent_soul as AgentSoulConfig | undefined
|
||||
},
|
||||
[
|
||||
agentId,
|
||||
buildDraftQueryOptions.queryKey,
|
||||
checkoutBuildDraft,
|
||||
queryClient,
|
||||
rebaseComposerDraft,
|
||||
setSoulSourceOverride,
|
||||
],
|
||||
)
|
||||
|
||||
const prepareBuildDraftBeforeRun = useCallback(async () => {
|
||||
if (!agentId) return
|
||||
|
||||
if (isBuildDraftActive) return buildDraftAgentSoulConfig
|
||||
|
||||
await saveDraft()
|
||||
|
||||
const buildDraft = await checkoutBuildDraft({
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
body: {
|
||||
force: false,
|
||||
},
|
||||
})
|
||||
queryClient.setQueryData(buildDraftQueryOptions.queryKey, buildDraft)
|
||||
rebaseComposerDraft?.(buildDraft.agent_soul as AgentSoulConfig | undefined)
|
||||
setSoulSourceOverride?.('build-draft')
|
||||
return buildDraft.agent_soul as AgentSoulConfig | undefined
|
||||
if (isBuildDraftActive) return buildDraftAgentSoulConfig
|
||||
|
||||
return checkoutBuildDraftFromNormalDraft(false)
|
||||
}, [
|
||||
agentId,
|
||||
buildDraftAgentSoulConfig,
|
||||
buildDraftQueryOptions.queryKey,
|
||||
checkoutBuildDraft,
|
||||
checkoutBuildDraftFromNormalDraft,
|
||||
isBuildDraftActive,
|
||||
queryClient,
|
||||
rebaseComposerDraft,
|
||||
saveDraft,
|
||||
setSoulSourceOverride,
|
||||
])
|
||||
const forceCheckoutBuildDraft = useCallback(
|
||||
() => checkoutBuildDraftFromNormalDraft(true),
|
||||
[checkoutBuildDraftFromNormalDraft],
|
||||
)
|
||||
|
||||
return {
|
||||
forceCheckoutBuildDraft,
|
||||
isCheckingOutBuildDraft,
|
||||
prepareBuildDraftBeforeRun,
|
||||
}
|
||||
|
||||
@ -141,6 +141,7 @@ export function useAgentConfigureBuildDraftData({
|
||||
agentId,
|
||||
activeVersionId,
|
||||
composerAgentSoulConfig,
|
||||
isBuildMode,
|
||||
isViewingVersion,
|
||||
normalAgentSoulConfig,
|
||||
setSoulSourceOverride,
|
||||
@ -149,6 +150,7 @@ export function useAgentConfigureBuildDraftData({
|
||||
agentId: string
|
||||
activeVersionId: string | null | undefined
|
||||
composerAgentSoulConfig?: AgentSoulConfig
|
||||
isBuildMode: boolean
|
||||
isViewingVersion: boolean
|
||||
normalAgentSoulConfig?: AgentSoulConfig
|
||||
setSoulSourceOverride: (source: AgentConfigureSoulSource | null) => void
|
||||
@ -178,7 +180,10 @@ export function useAgentConfigureBuildDraftData({
|
||||
const buildDraftQuery = useQuery({
|
||||
...buildDraftQueryOptions,
|
||||
enabled:
|
||||
!isViewingVersion && soulSourceOverride !== 'draft' && soulSourceOverride !== 'view-version',
|
||||
isBuildMode &&
|
||||
!isViewingVersion &&
|
||||
soulSourceOverride !== 'draft' &&
|
||||
soulSourceOverride !== 'view-version',
|
||||
queryFn: async (context) => {
|
||||
try {
|
||||
const queryOptions = shouldSilenceBuildDraftCheckRef.current
|
||||
@ -205,10 +210,12 @@ export function useAgentConfigureBuildDraftData({
|
||||
refetch: refetchBuildDraft,
|
||||
} = buildDraftQuery
|
||||
const buildDraftNotFound = isNotFoundResponse(buildDraftError)
|
||||
const soulSource: AgentConfigureSoulSource = isViewingVersion
|
||||
const resolvedSoulSource: AgentConfigureSoulSource = isViewingVersion
|
||||
? 'view-version'
|
||||
: (soulSourceOverride ??
|
||||
(!buildDraftNotFound && !!buildDraftData && !isBuildDraftError ? 'build-draft' : 'draft'))
|
||||
const hasActiveBuildDraft = resolvedSoulSource === 'build-draft'
|
||||
const soulSource = !isBuildMode && hasActiveBuildDraft ? 'draft' : resolvedSoulSource
|
||||
const isBuildDraftActive = soulSource === 'build-draft'
|
||||
const buildDraftAgentSoulConfig = buildDraftData?.agent_soul as AgentSoulConfig | undefined
|
||||
const visibleAgentSoulConfig = isBuildDraftActive
|
||||
@ -244,11 +251,14 @@ export function useAgentConfigureBuildDraftData({
|
||||
? `build-draft:${buildDraftDataUpdatedAt}`
|
||||
: activeVersionId,
|
||||
agentSoulConfig: visibleAgentSoulConfig,
|
||||
buildDraftAgentSoulConfig,
|
||||
changedKeys: buildDraftChangeSummary.changedKeys,
|
||||
changeSummary: buildDraftChangeSummary,
|
||||
changesCount: buildDraftChangeSummary.changesCount,
|
||||
hasActiveBuildDraft,
|
||||
isActive: isBuildDraftActive,
|
||||
isPending:
|
||||
isBuildMode &&
|
||||
!isViewingVersion &&
|
||||
soulSourceOverride !== 'draft' &&
|
||||
soulSourceOverride !== 'view-version' &&
|
||||
@ -288,6 +298,7 @@ export function useAgentConfigureBuildDraftActions({
|
||||
const queryClient = useQueryClient()
|
||||
const buildDraftRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const buildDraftRefreshGenerationRef = useRef(0)
|
||||
const forceCheckoutBeforeNextBuildRunRef = useRef(false)
|
||||
const buildDraftQueryOptions = consoleQuery.agent.byAgentId.buildDraft.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
@ -313,14 +324,15 @@ export function useAgentConfigureBuildDraftActions({
|
||||
applyBuildDraftMutation
|
||||
const { mutateAsync: discardBuildDraftRequest, isPending: isDiscardingBuildDraft } =
|
||||
discardBuildDraftMutation
|
||||
const { prepareBuildDraftBeforeRun } = usePrepareAgentBuildDraftBeforeRun({
|
||||
agentId,
|
||||
buildDraftAgentSoulConfig,
|
||||
isBuildDraftActive: isActive,
|
||||
rebaseComposerDraft,
|
||||
saveDraft,
|
||||
setSoulSourceOverride,
|
||||
})
|
||||
const { forceCheckoutBuildDraft, prepareBuildDraftBeforeRun } =
|
||||
usePrepareAgentBuildDraftBeforeRun({
|
||||
agentId,
|
||||
buildDraftAgentSoulConfig,
|
||||
isBuildDraftActive: isActive,
|
||||
rebaseComposerDraft,
|
||||
saveDraft,
|
||||
setSoulSourceOverride,
|
||||
})
|
||||
|
||||
const cancelBuildDraftRefresh = useCallback(() => {
|
||||
buildDraftRefreshGenerationRef.current += 1
|
||||
@ -332,8 +344,37 @@ export function useAgentConfigureBuildDraftActions({
|
||||
|
||||
const prepareBuildDraftRun = useCallback(async () => {
|
||||
cancelBuildDraftRefresh()
|
||||
return prepareBuildDraftBeforeRun()
|
||||
}, [cancelBuildDraftRefresh, prepareBuildDraftBeforeRun])
|
||||
if (!forceCheckoutBeforeNextBuildRunRef.current) return prepareBuildDraftBeforeRun()
|
||||
|
||||
await saveDraft()
|
||||
try {
|
||||
const buildDraft = await forceCheckoutBuildDraft()
|
||||
forceCheckoutBeforeNextBuildRunRef.current = false
|
||||
return buildDraft
|
||||
} catch (error) {
|
||||
toast.error(tCommon(($) => $['api.actionFailed']))
|
||||
throw error
|
||||
}
|
||||
}, [
|
||||
cancelBuildDraftRefresh,
|
||||
forceCheckoutBuildDraft,
|
||||
prepareBuildDraftBeforeRun,
|
||||
saveDraft,
|
||||
tCommon,
|
||||
])
|
||||
|
||||
const startFreshBuildSession = useCallback(async () => {
|
||||
cancelBuildDraftRefresh()
|
||||
try {
|
||||
await resetBuildChatSession()
|
||||
forceCheckoutBeforeNextBuildRunRef.current = true
|
||||
setSoulSourceOverride('draft')
|
||||
return true
|
||||
} catch {
|
||||
toast.error(tCommon(($) => $['api.actionFailed']))
|
||||
return false
|
||||
}
|
||||
}, [cancelBuildDraftRefresh, resetBuildChatSession, setSoulSourceOverride, tCommon])
|
||||
|
||||
const refreshBuildDraftAfterBuildChat = useCallback(
|
||||
(onRefreshed?: () => void) => {
|
||||
@ -420,8 +461,10 @@ export function useAgentConfigureBuildDraftActions({
|
||||
})
|
||||
await exitBuildDraftMode(false)
|
||||
toast.success(tCommon(($) => $['api.actionSuccess']))
|
||||
return true
|
||||
} catch {
|
||||
toast.error(tCommon(($) => $['api.actionFailed']))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -439,5 +482,6 @@ export function useAgentConfigureBuildDraftActions({
|
||||
isDiscardingBuildDraft,
|
||||
prepareBuildDraftBeforeRun: prepareBuildDraftRun,
|
||||
refreshBuildDraftAfterBuildChat,
|
||||
startFreshBuildSession,
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,284 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { AgentConfigureRightPanelMode } from './state'
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
type PendingDraftSaveResult = { status: 'saved' } | { status: 'failed'; error: unknown }
|
||||
|
||||
export function useAgentConfigureSessionController({
|
||||
buildDraftAgentSoulConfig,
|
||||
hasActiveBuildDraft,
|
||||
isBuildDraftActive,
|
||||
mode,
|
||||
normalAgentSoulConfig,
|
||||
onModeChange,
|
||||
}: {
|
||||
buildDraftAgentSoulConfig?: AgentSoulConfig
|
||||
hasActiveBuildDraft: boolean
|
||||
isBuildDraftActive: boolean
|
||||
mode: AgentConfigureRightPanelMode
|
||||
normalAgentSoulConfig?: AgentSoulConfig
|
||||
onModeChange: (mode: AgentConfigureRightPanelMode) => void | Promise<unknown>
|
||||
}) {
|
||||
const [buildCallbackRevision, setBuildCallbackRevision] = useState(0)
|
||||
const [buildDraftActionsDisabled, setBuildDraftActionsDisabled] = useState(false)
|
||||
const [hasStartedBuildChat, setHasStartedBuildChat] = useState(false)
|
||||
const [isEnteringBuildMode, setIsEnteringBuildMode] = useState(false)
|
||||
const [showSwitchToPreviewConfirm, setShowSwitchToPreviewConfirm] = useState(false)
|
||||
const buildCallbackGeneration = useMemo(
|
||||
() => Symbol(`agent-build-session:${mode}:${buildCallbackRevision}`),
|
||||
[buildCallbackRevision, mode],
|
||||
)
|
||||
const buildCallbackGenerationRef = useRef(buildCallbackGeneration)
|
||||
const buildCallbacksEnabledRef = useRef(mode === 'build')
|
||||
const modeRef = useRef(mode)
|
||||
const pendingBuildModeTransitionRef = useRef<Promise<void> | null>(null)
|
||||
const pendingBuildDraftPreparationRef = useRef<Promise<unknown> | null>(null)
|
||||
const pendingPreviewDraftSaveRef = useRef<Promise<PendingDraftSaveResult> | null>(null)
|
||||
|
||||
const rotateBuildCallbackGeneration = useCallback((enabled: boolean) => {
|
||||
buildCallbackGenerationRef.current = Symbol('invalid-agent-build-session')
|
||||
buildCallbacksEnabledRef.current = enabled
|
||||
setBuildCallbackRevision((revision) => revision + 1)
|
||||
}, [])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
buildCallbackGenerationRef.current = buildCallbackGeneration
|
||||
}, [buildCallbackGeneration])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (modeRef.current === mode) return
|
||||
|
||||
modeRef.current = mode
|
||||
buildCallbacksEnabledRef.current = mode === 'build'
|
||||
}, [mode])
|
||||
|
||||
const isBuildCallbackCurrent = useCallback(
|
||||
(generation: symbol) =>
|
||||
buildCallbacksEnabledRef.current &&
|
||||
modeRef.current === 'build' &&
|
||||
buildCallbackGenerationRef.current === generation,
|
||||
[],
|
||||
)
|
||||
|
||||
const registerPreviewDraftSave = useCallback((draftSave: Promise<unknown>) => {
|
||||
pendingPreviewDraftSaveRef.current = draftSave.then<
|
||||
PendingDraftSaveResult,
|
||||
PendingDraftSaveResult
|
||||
>(
|
||||
() => ({ status: 'saved' }),
|
||||
(error: unknown) => ({ status: 'failed', error }),
|
||||
)
|
||||
}, [])
|
||||
|
||||
const waitForPendingPreviewDraftSave = useCallback(async () => {
|
||||
const pendingDraftSave = pendingPreviewDraftSaveRef.current
|
||||
if (!pendingDraftSave) return
|
||||
|
||||
const result = await pendingDraftSave
|
||||
if (result.status === 'failed') throw result.error
|
||||
|
||||
if (pendingPreviewDraftSaveRef.current === pendingDraftSave)
|
||||
pendingPreviewDraftSaveRef.current = null
|
||||
}, [])
|
||||
|
||||
const waitForPendingBuildDraftPreparation = useCallback(async () => {
|
||||
const pendingBuildDraftPreparation = pendingBuildDraftPreparationRef.current
|
||||
if (!pendingBuildDraftPreparation) return
|
||||
|
||||
await pendingBuildDraftPreparation.catch(() => undefined)
|
||||
}, [])
|
||||
|
||||
const runBuildPreparation = useCallback(
|
||||
async <T>({
|
||||
generation,
|
||||
markBuildChatStarted = false,
|
||||
prepare,
|
||||
}: {
|
||||
generation: symbol
|
||||
markBuildChatStarted?: boolean
|
||||
prepare: () => Promise<T>
|
||||
}) => {
|
||||
if (!isBuildCallbackCurrent(generation))
|
||||
throw new Error('The Build session is no longer active.')
|
||||
|
||||
if (markBuildChatStarted) {
|
||||
setHasStartedBuildChat(true)
|
||||
setBuildDraftActionsDisabled(true)
|
||||
}
|
||||
|
||||
const preparation = (async () => {
|
||||
const result = await prepare()
|
||||
if (!isBuildCallbackCurrent(generation))
|
||||
throw new Error('The Build session is no longer active.')
|
||||
return result
|
||||
})()
|
||||
pendingBuildDraftPreparationRef.current = preparation
|
||||
|
||||
try {
|
||||
return await preparation
|
||||
} catch (error) {
|
||||
if (markBuildChatStarted && isBuildCallbackCurrent(generation)) {
|
||||
setBuildDraftActionsDisabled(false)
|
||||
setHasStartedBuildChat(false)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
if (pendingBuildDraftPreparationRef.current === preparation)
|
||||
pendingBuildDraftPreparationRef.current = null
|
||||
}
|
||||
},
|
||||
[isBuildCallbackCurrent],
|
||||
)
|
||||
|
||||
const finishBuildAction = useCallback(
|
||||
(generation: symbol) => {
|
||||
if (!isBuildCallbackCurrent(generation)) return
|
||||
setBuildDraftActionsDisabled(false)
|
||||
},
|
||||
[isBuildCallbackCurrent],
|
||||
)
|
||||
|
||||
const resetBuildSessionState = useCallback(() => {
|
||||
const keepBuildCallbacksEnabled =
|
||||
buildCallbacksEnabledRef.current && modeRef.current === 'build'
|
||||
rotateBuildCallbackGeneration(keepBuildCallbacksEnabled)
|
||||
setBuildDraftActionsDisabled(false)
|
||||
setHasStartedBuildChat(false)
|
||||
}, [rotateBuildCallbackGeneration])
|
||||
|
||||
const resetBuildSession = useCallback(
|
||||
async (onResetBuildSession: () => Promise<void>) => {
|
||||
try {
|
||||
await onResetBuildSession()
|
||||
} finally {
|
||||
resetBuildSessionState()
|
||||
}
|
||||
},
|
||||
[resetBuildSessionState],
|
||||
)
|
||||
|
||||
const discardBuildDraftAndSwitchToPreview = useCallback(
|
||||
async (discardBuildDraft: () => Promise<boolean>, stopBuildChat: () => void) => {
|
||||
rotateBuildCallbackGeneration(false)
|
||||
stopBuildChat()
|
||||
await waitForPendingBuildDraftPreparation()
|
||||
const discarded = await discardBuildDraft()
|
||||
if (!discarded) {
|
||||
rotateBuildCallbackGeneration(true)
|
||||
return false
|
||||
}
|
||||
|
||||
modeRef.current = 'preview'
|
||||
onModeChange('preview')
|
||||
return true
|
||||
},
|
||||
[onModeChange, rotateBuildCallbackGeneration, waitForPendingBuildDraftPreparation],
|
||||
)
|
||||
|
||||
const changeMode = useCallback(
|
||||
(
|
||||
nextMode: AgentConfigureRightPanelMode,
|
||||
{
|
||||
discardBuildDraft,
|
||||
rebaseComposerDraft,
|
||||
savePreviewDraft,
|
||||
startFreshBuildSession,
|
||||
stopBuildChat,
|
||||
}: {
|
||||
discardBuildDraft: () => Promise<boolean>
|
||||
rebaseComposerDraft: (agentSoulConfig?: AgentSoulConfig) => void
|
||||
savePreviewDraft: () => Promise<unknown>
|
||||
startFreshBuildSession?: () => Promise<boolean>
|
||||
stopBuildChat: () => void
|
||||
},
|
||||
) => {
|
||||
if (nextMode === modeRef.current) return
|
||||
|
||||
const isLeavingBuildMode = modeRef.current === 'build' && nextMode === 'preview'
|
||||
if (isLeavingBuildMode && (hasActiveBuildDraft || hasStartedBuildChat)) {
|
||||
setShowSwitchToPreviewConfirm(true)
|
||||
return
|
||||
}
|
||||
if (isLeavingBuildMode && pendingBuildDraftPreparationRef.current) {
|
||||
void discardBuildDraftAndSwitchToPreview(discardBuildDraft, stopBuildChat)
|
||||
return
|
||||
}
|
||||
if (isLeavingBuildMode) rotateBuildCallbackGeneration(false)
|
||||
|
||||
const nextUsesBuildDraft = nextMode === 'build' && hasActiveBuildDraft
|
||||
const isEnteringBuildMode = modeRef.current === 'preview' && nextMode === 'build'
|
||||
if (isEnteringBuildMode) {
|
||||
if (startFreshBuildSession) {
|
||||
if (pendingBuildModeTransitionRef.current) return
|
||||
|
||||
setIsEnteringBuildMode(true)
|
||||
const transition = (async () => {
|
||||
try {
|
||||
const started = await startFreshBuildSession()
|
||||
if (!started || modeRef.current !== 'preview') return
|
||||
|
||||
modeRef.current = 'build'
|
||||
rotateBuildCallbackGeneration(true)
|
||||
try {
|
||||
await onModeChange(nextMode)
|
||||
} catch (error) {
|
||||
modeRef.current = 'preview'
|
||||
rotateBuildCallbackGeneration(false)
|
||||
throw error
|
||||
}
|
||||
} catch {}
|
||||
})()
|
||||
pendingBuildModeTransitionRef.current = transition
|
||||
void transition.finally(() => {
|
||||
if (pendingBuildModeTransitionRef.current === transition)
|
||||
pendingBuildModeTransitionRef.current = null
|
||||
setIsEnteringBuildMode(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
modeRef.current = 'build'
|
||||
rotateBuildCallbackGeneration(true)
|
||||
registerPreviewDraftSave(savePreviewDraft())
|
||||
if (nextUsesBuildDraft) rebaseComposerDraft(buildDraftAgentSoulConfig)
|
||||
onModeChange(nextMode)
|
||||
return
|
||||
}
|
||||
|
||||
if (nextUsesBuildDraft !== isBuildDraftActive) {
|
||||
rebaseComposerDraft(nextUsesBuildDraft ? buildDraftAgentSoulConfig : normalAgentSoulConfig)
|
||||
}
|
||||
modeRef.current = nextMode
|
||||
onModeChange(nextMode)
|
||||
},
|
||||
[
|
||||
buildDraftAgentSoulConfig,
|
||||
discardBuildDraftAndSwitchToPreview,
|
||||
hasActiveBuildDraft,
|
||||
hasStartedBuildChat,
|
||||
isBuildDraftActive,
|
||||
normalAgentSoulConfig,
|
||||
onModeChange,
|
||||
registerPreviewDraftSave,
|
||||
rotateBuildCallbackGeneration,
|
||||
],
|
||||
)
|
||||
|
||||
return {
|
||||
buildCallbackGeneration,
|
||||
buildDraftActionsDisabled,
|
||||
changeMode,
|
||||
confirmSwitchToPreview: discardBuildDraftAndSwitchToPreview,
|
||||
finishBuildAction,
|
||||
isEnteringBuildMode,
|
||||
isBuildCallbackCurrent,
|
||||
resetBuildSession,
|
||||
resetBuildSessionState,
|
||||
runBuildPreparation,
|
||||
setShowSwitchToPreviewConfirm,
|
||||
showSwitchToPreviewConfirm,
|
||||
waitForPendingPreviewDraftSave,
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,7 @@ export function useAgentConfigureSync({
|
||||
const enabledRef = useRef(enabled)
|
||||
const lastAutosavedDraftKeyRef = useRef<string | undefined>(undefined)
|
||||
const pageCloseSavingDraftKeyRef = useRef<string | undefined>(undefined)
|
||||
const explicitlySavingDraftKeysRef = useRef(new Set<string>())
|
||||
const publishInFlightRef = useRef(false)
|
||||
|
||||
baseConfigRef.current = baseConfig
|
||||
@ -153,11 +154,17 @@ export function useAgentConfigureSync({
|
||||
debouncedSaveDraft.cancel?.()
|
||||
if (!store.get(isAgentComposerDirtyAtom) && !hasEffectiveModelChange) return
|
||||
|
||||
await saveComposer({
|
||||
configSnapshot,
|
||||
draftBaseline: draft,
|
||||
silent: false,
|
||||
})
|
||||
const draftKey = JSON.stringify(configSnapshot)
|
||||
explicitlySavingDraftKeysRef.current.add(draftKey)
|
||||
try {
|
||||
await saveComposer({
|
||||
configSnapshot,
|
||||
draftBaseline: draft,
|
||||
silent: false,
|
||||
})
|
||||
} finally {
|
||||
explicitlySavingDraftKeysRef.current.delete(draftKey)
|
||||
}
|
||||
}, [debouncedSaveDraft, getAgentSoulDraft, saveComposer, store])
|
||||
|
||||
const saveDirtyDraftOnPageClose = useCallback(() => {
|
||||
@ -174,7 +181,8 @@ export function useAgentConfigureSync({
|
||||
const draftKey = JSON.stringify(configSnapshot)
|
||||
if (
|
||||
lastAutosavedDraftKeyRef.current === draftKey ||
|
||||
pageCloseSavingDraftKeyRef.current === draftKey
|
||||
pageCloseSavingDraftKeyRef.current === draftKey ||
|
||||
explicitlySavingDraftKeysRef.current.has(draftKey)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "تحميل مهارة",
|
||||
"agentDetail.configure.skills.upload.warning.files": "إذا كنت تحتاج فقط إلى استخدام ملفات Markdown، فحمّلها إلى قسم الملفات وأشر إليها في الموجّه الخاص بك.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "يجب أن تتوافق المهارات التي يتم تحميلها مع <specificationLink>مواصفات Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "التبديل إلى وضع المعاينة؟",
|
||||
"agentDetail.configure.title": "تكوين",
|
||||
"agentDetail.configure.tools.add": "إضافة أداة",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "للمطورين",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Skill hochladen",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Wenn du nur Markdown-Dateien verwenden möchtest, lade sie unter „Dateien“ hoch und verweise in deinem Prompt darauf.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Hochgeladene Skills müssen der <specificationLink>Agent-Skills-Spezifikation</specificationLink> entsprechen.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "In den Vorschaumodus wechseln?",
|
||||
"agentDetail.configure.title": "Konfigurieren",
|
||||
"agentDetail.configure.tools.add": "Tool hinzufügen",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Für Entwickler",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Upload skill",
|
||||
"agentDetail.configure.skills.upload.warning.files": "If you only need to use Markdown files, upload them to Files and reference them in your prompt.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Uploaded skills must follow the <specificationLink>Agent Skills specification</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Switch to Preview mode?",
|
||||
"agentDetail.configure.title": "Configure",
|
||||
"agentDetail.configure.tools.add": "Add tool",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "For developers",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Subir habilidad",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Si solo necesitas usar archivos Markdown, súbelos a Archivos y haz referencia a ellos en tu prompt.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Las habilidades subidas deben cumplir la <specificationLink>especificación de Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "¿Cambiar al modo de vista previa?",
|
||||
"agentDetail.configure.title": "Configurar",
|
||||
"agentDetail.configure.tools.add": "Agregar herramienta",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Para desarrolladores",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "بارگذاری مهارت",
|
||||
"agentDetail.configure.skills.upload.warning.files": "اگر فقط میخواهید از فایلهای Markdown استفاده کنید، آنها را در بخش فایلها بارگذاری و در پرامپت خود ارجاع دهید.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "مهارتهای بارگذاریشده باید از <specificationLink>مشخصات Agent Skills</specificationLink> پیروی کنند.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "به حالت پیشنمایش بروید؟",
|
||||
"agentDetail.configure.title": "پیکربندی",
|
||||
"agentDetail.configure.tools.add": "افزودن ابزار",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "برای توسعهدهندگان",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Téléverser une compétence",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Si vous souhaitez uniquement utiliser des fichiers Markdown, téléversez-les dans Fichiers et référencez-les dans votre prompt.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Les compétences téléversées doivent respecter la <specificationLink>spécification Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Passer en mode aperçu ?",
|
||||
"agentDetail.configure.title": "Configurer",
|
||||
"agentDetail.configure.tools.add": "Ajouter un outil",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Pour les développeurs",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "कौशल अपलोड करें",
|
||||
"agentDetail.configure.skills.upload.warning.files": "यदि आपको केवल Markdown फ़ाइलों का उपयोग करना है, तो उन्हें फ़ाइलें में अपलोड करें और अपने प्रॉम्प्ट में उनका संदर्भ दें।",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "अपलोड किए गए कौशल को <specificationLink>Agent Skills विनिर्देश</specificationLink> का पालन करना चाहिए।",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "प्रीव्यू मोड पर स्विच करें?",
|
||||
"agentDetail.configure.title": "कॉन्फ़िगर करें",
|
||||
"agentDetail.configure.tools.add": "उपकरण जोड़ें",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "डेवलपर्स के लिए",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Unggah keterampilan",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Jika Anda hanya perlu menggunakan file Markdown, unggah ke File dan rujuk file tersebut dalam prompt Anda.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Keterampilan yang diunggah harus mengikuti <specificationLink>spesifikasi Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Beralih ke mode Pratinjau?",
|
||||
"agentDetail.configure.title": "Konfigurasi",
|
||||
"agentDetail.configure.tools.add": "Tambahkan alat",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Untuk pengembang",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Carica abilità",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Se devi usare solo file Markdown, caricali in File e richiamali nel tuo prompt.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Le abilità caricate devono rispettare la <specificationLink>specifica Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Passare alla modalità Anteprima?",
|
||||
"agentDetail.configure.title": "Configura",
|
||||
"agentDetail.configure.tools.add": "Aggiungi strumento",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Per sviluppatori",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "スキルをアップロード",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Markdown ファイルのみを使用する場合は、「ファイル」にアップロードし、プロンプト内で参照してください。",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "アップロードするスキルは <specificationLink>Agent Skills 仕様</specificationLink>に準拠する必要があります。",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "プレビューモードに切り替えますか?",
|
||||
"agentDetail.configure.title": "設定",
|
||||
"agentDetail.configure.tools.add": "ツールを追加",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "開発者向け",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "스킬 업로드",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Markdown 파일만 사용하려면 파일에 업로드하고 프롬프트에서 참조하세요.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "업로드한 스킬은 <specificationLink>Agent Skills 사양</specificationLink>을 준수해야 합니다.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "미리보기 모드로 전환할까요?",
|
||||
"agentDetail.configure.title": "구성",
|
||||
"agentDetail.configure.tools.add": "도구 추가",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "개발자용",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Skill uploaden",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Als je alleen Markdown-bestanden wilt gebruiken, upload ze dan naar Bestanden en verwijs ernaar in je prompt.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Geüploade skills moeten voldoen aan de <specificationLink>Agent Skills-specificatie</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Overschakelen naar de voorbeeldmodus?",
|
||||
"agentDetail.configure.title": "Configureren",
|
||||
"agentDetail.configure.tools.add": "Tool toevoegen",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Voor ontwikkelaars",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Prześlij umiejętność",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Jeśli chcesz używać tylko plików Markdown, prześlij je do sekcji Pliki i odwołaj się do nich w swoim prompcie.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Przesyłane umiejętności muszą być zgodne ze <specificationLink>specyfikacją Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Przełączyć się do trybu podglądu?",
|
||||
"agentDetail.configure.title": "Konfiguruj",
|
||||
"agentDetail.configure.tools.add": "Dodaj narzędzie",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Dla deweloperów",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Enviar habilidade",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Se você só precisa usar arquivos Markdown, envie-os para Arquivos e faça referência a eles no seu prompt.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "As habilidades enviadas devem seguir a <specificationLink>especificação Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Mudar para o modo de visualização?",
|
||||
"agentDetail.configure.title": "Configurar",
|
||||
"agentDetail.configure.tools.add": "Adicionar ferramenta",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Para desenvolvedores",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Încarcă abilitate",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Dacă trebuie doar să folosești fișiere Markdown, încarcă-le în Fișiere și menționează-le în promptul tău.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Abilitățile încărcate trebuie să respecte <specificationLink>specificația Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Comuți la modul Previzualizare?",
|
||||
"agentDetail.configure.title": "Configurare",
|
||||
"agentDetail.configure.tools.add": "Adaugă instrument",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Pentru dezvoltatori",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Загрузить навык",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Если вам нужно использовать только файлы Markdown, загрузите их в раздел «Файлы» и укажите ссылки на них в своем промпте.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Загружаемые навыки должны соответствовать <specificationLink>спецификации Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Перейти в режим предпросмотра?",
|
||||
"agentDetail.configure.title": "Настроить",
|
||||
"agentDetail.configure.tools.add": "Добавить инструмент",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Для разработчиков",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Naloži veščino",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Če želite uporabljati samo datoteke Markdown, jih naložite v razdelek Datoteke in se nanje sklicujte v svojem pozivu.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Naložene veščine morajo upoštevati <specificationLink>specifikacijo Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Preklopiti v način predogleda?",
|
||||
"agentDetail.configure.title": "Konfiguriraj",
|
||||
"agentDetail.configure.tools.add": "Dodaj orodje",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Za razvijalce",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "อัปโหลดทักษะ",
|
||||
"agentDetail.configure.skills.upload.warning.files": "หากต้องการใช้เฉพาะไฟล์ Markdown ให้อัปโหลดไปยังส่วนไฟล์และอ้างอิงไฟล์เหล่านั้นในพรอมต์ของคุณ",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "ทักษะที่อัปโหลดต้องเป็นไปตาม<specificationLink>ข้อกำหนด Agent Skills</specificationLink>",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "สลับไปยังโหมดแสดงตัวอย่าง?",
|
||||
"agentDetail.configure.title": "กำหนดค่า",
|
||||
"agentDetail.configure.tools.add": "เพิ่มเครื่องมือ",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "สำหรับนักพัฒนา",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Beceri yükle",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Yalnızca Markdown dosyalarını kullanmanız gerekiyorsa bunları Dosyalar bölümüne yükleyin ve isteminizde referans verin.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Yüklenen beceriler <specificationLink>Agent Skills spesifikasyonuna</specificationLink> uygun olmalıdır.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Önizleme moduna geçilsin mi?",
|
||||
"agentDetail.configure.title": "Yapılandır",
|
||||
"agentDetail.configure.tools.add": "Araç ekle",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Geliştiriciler için",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Завантажити навичку",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Якщо потрібно використовувати лише файли Markdown, завантажте їх у розділ «Файли» та посилайтеся на них у своєму промпті.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Завантажувані навички мають відповідати <specificationLink>специфікації Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Перейти в режим попереднього перегляду?",
|
||||
"agentDetail.configure.title": "Налаштувати",
|
||||
"agentDetail.configure.tools.add": "Додати інструмент",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Для розробників",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "Tải lên kỹ năng",
|
||||
"agentDetail.configure.skills.upload.warning.files": "Nếu bạn chỉ cần sử dụng tệp Markdown, hãy tải chúng lên Tệp và tham chiếu chúng trong prompt của bạn.",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "Kỹ năng được tải lên phải tuân theo <specificationLink>đặc tả Agent Skills</specificationLink>.",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "Chuyển sang chế độ Xem trước?",
|
||||
"agentDetail.configure.title": "Cấu hình",
|
||||
"agentDetail.configure.tools.add": "Thêm công cụ",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "Dành cho nhà phát triển",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "上传 Skill",
|
||||
"agentDetail.configure.skills.upload.warning.files": "如果只需要使用 Markdown 文件,请将它们上传到「文件」,并在你的提示词中引用。",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "上传的 Skill 需遵循 <specificationLink>Agent Skills 规范</specificationLink>。",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "切换到预览模式?",
|
||||
"agentDetail.configure.title": "配置",
|
||||
"agentDetail.configure.tools.add": "添加工具",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "开发者适用",
|
||||
|
||||
@ -226,6 +226,7 @@
|
||||
"agentDetail.configure.skills.upload.title": "上傳 Skill",
|
||||
"agentDetail.configure.skills.upload.warning.files": "如果只需要使用 Markdown 檔案,請將它們上傳到「檔案」,並在你的提示詞中引用。",
|
||||
"agentDetail.configure.skills.upload.warning.specification": "上傳的 Skill 需遵循 <specificationLink>Agent Skills 規範</specificationLink>。",
|
||||
"agentDetail.configure.switchToPreviewConfirm.title": "切換到預覽模式?",
|
||||
"agentDetail.configure.title": "設定",
|
||||
"agentDetail.configure.tools.add": "新增工具",
|
||||
"agentDetail.configure.tools.addMenu.cliTool.badge": "開發者適用",
|
||||
|
||||
@ -458,6 +458,47 @@ describe('ssePost and sseGet', () => {
|
||||
expect(onCompleted).toHaveBeenCalledWith(true, 'Error: stream lost')
|
||||
expect(toast.error).toHaveBeenCalledWith('Error: stream lost')
|
||||
})
|
||||
|
||||
it('should not notify when the stream reader is aborted', async () => {
|
||||
const onError = vi.fn()
|
||||
const onCompleted = vi.fn()
|
||||
const mockReader = {
|
||||
read: vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new DOMException('BodyStreamBuffer was aborted', 'AbortError')),
|
||||
}
|
||||
const response = {
|
||||
status: 200,
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => mockReader,
|
||||
},
|
||||
} as unknown as Response
|
||||
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(response)
|
||||
|
||||
await ssePost(
|
||||
'/chat-messages',
|
||||
{
|
||||
body: {
|
||||
query: 'hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
onError,
|
||||
onCompleted,
|
||||
},
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
'AbortError: BodyStreamBuffer was aborted',
|
||||
'stream_read_error',
|
||||
)
|
||||
})
|
||||
expect(onCompleted).toHaveBeenCalledWith(true, 'AbortError: BodyStreamBuffer was aborted')
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('HTTP methods', () => {
|
||||
|
||||
@ -49,6 +49,17 @@ import { getWebAppPassport } from './webapp-auth'
|
||||
|
||||
const TIME_OUT = 100000
|
||||
|
||||
const isAbortError = (error: unknown) => {
|
||||
if (typeof error === 'string') return error === 'AbortError' || error.startsWith('AbortError:')
|
||||
|
||||
return (
|
||||
typeof error === 'object' && error !== null && 'name' in error && error.name === 'AbortError'
|
||||
)
|
||||
}
|
||||
|
||||
const shouldNotifyStreamError = (error: unknown) =>
|
||||
!isAbortError(error) && !String(error).includes('TypeError: Cannot assign to read only property')
|
||||
|
||||
export type IOnDataMoreInfo = {
|
||||
event?: string
|
||||
conversationId?: string
|
||||
@ -618,12 +629,8 @@ export const ssePost = async (
|
||||
(str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
|
||||
if (moreInfo.errorMessage) {
|
||||
onError?.(moreInfo.errorMessage, moreInfo.errorCode)
|
||||
// TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored.
|
||||
if (
|
||||
moreInfo.errorMessage !== 'AbortError: The user aborted a request.' &&
|
||||
!moreInfo.errorMessage.includes('TypeError: Cannot assign to read only property')
|
||||
)
|
||||
toast.error(moreInfo.errorMessage)
|
||||
// These errors can happen when a stream is intentionally stopped or its page is left.
|
||||
if (shouldNotifyStreamError(moreInfo.errorMessage)) toast.error(moreInfo.errorMessage)
|
||||
return
|
||||
}
|
||||
onData?.(str, isFirstMessage, moreInfo)
|
||||
@ -664,11 +671,7 @@ export const ssePost = async (
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = String(e)
|
||||
if (
|
||||
errorMessage !== 'AbortError: The user aborted a request.' &&
|
||||
!errorMessage.includes('TypeError: Cannot assign to read only property')
|
||||
)
|
||||
toast.error(errorMessage)
|
||||
if (shouldNotifyStreamError(e)) toast.error(errorMessage)
|
||||
onError?.(errorMessage)
|
||||
})
|
||||
}
|
||||
@ -781,12 +784,8 @@ export const sseGet = async (
|
||||
(str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
|
||||
if (moreInfo.errorMessage) {
|
||||
onError?.(moreInfo.errorMessage, moreInfo.errorCode)
|
||||
// TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored.
|
||||
if (
|
||||
moreInfo.errorMessage !== 'AbortError: The user aborted a request.' &&
|
||||
!moreInfo.errorMessage.includes('TypeError: Cannot assign to read only property')
|
||||
)
|
||||
toast.error(moreInfo.errorMessage)
|
||||
// These errors can happen when a stream is intentionally stopped or its page is left.
|
||||
if (shouldNotifyStreamError(moreInfo.errorMessage)) toast.error(moreInfo.errorMessage)
|
||||
return
|
||||
}
|
||||
onData?.(str, isFirstMessage, moreInfo)
|
||||
@ -827,11 +826,7 @@ export const sseGet = async (
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = String(e)
|
||||
if (
|
||||
errorMessage !== 'AbortError: The user aborted a request.' &&
|
||||
!errorMessage.includes('TypeError: Cannot assign to read only property')
|
||||
)
|
||||
toast.error(errorMessage)
|
||||
if (shouldNotifyStreamError(e)) toast.error(errorMessage)
|
||||
onError?.(errorMessage)
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user