mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 14:51:13 +08:00
feat: inline agent show debug and time to show build draft bar
This commit is contained in:
parent
58a7b38a55
commit
6c28ac15fe
@ -182,6 +182,20 @@ vi.mock('../components/agent-orchestrate-drawer-panel', () => ({
|
||||
<div role="region" aria-label={props.isInline ? 'inline-orchestrate-panel' : 'readonly-roster-orchestrate-panel'} />
|
||||
)
|
||||
},
|
||||
WorkflowInlineAgentConfigureWorkspace: (props: {
|
||||
agentId?: string
|
||||
appId?: string
|
||||
inlineComposerState?: unknown
|
||||
isInline: boolean
|
||||
nodeId: string
|
||||
open: boolean
|
||||
}) => {
|
||||
mockOrchestrateDrawerPanelProps.push(props)
|
||||
|
||||
return (
|
||||
<div role="region" aria-label="inline-orchestrate-panel" />
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../components/save-inline-agent-to-roster-dialog', () => ({
|
||||
@ -462,7 +476,7 @@ describe('agent/panel', () => {
|
||||
|
||||
expect(screen.queryByText(/^workflow\.errorMsg\.fieldRequired/)).not.toBeInTheDocument()
|
||||
expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.change' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.change', hidden: true })).toBeDisabled()
|
||||
expect(screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
|
||||
expect(screen.queryByText('workflow.nodes.agent.roster.editInConsole')).not.toBeInTheDocument()
|
||||
@ -478,6 +492,43 @@ describe('agent/panel', () => {
|
||||
expect(container.querySelector('[inert]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens a pending inline agent modal and creates the inline agent before rendering details', () => {
|
||||
const { container, rerender } = render(
|
||||
<AgentV2Panel
|
||||
id="agent-node"
|
||||
data={createData({
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
})}
|
||||
panelProps={panelProps}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/ }))
|
||||
|
||||
expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('agent-node')
|
||||
expect(mockCreateInlineAgentBinding).toHaveBeenCalledWith('agent-node', expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}))
|
||||
mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
|
||||
rerender(
|
||||
<AgentV2Panel
|
||||
id="agent-node"
|
||||
data={createData({
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
})}
|
||||
panelProps={panelProps}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
|
||||
expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders inline agent detail from workflow composer state and opens the inline panel', () => {
|
||||
mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
|
||||
const { container } = render(
|
||||
@ -497,18 +548,15 @@ describe('agent/panel', () => {
|
||||
|
||||
expect(mockUseAgentRosterDetail).toHaveBeenCalledWith(undefined)
|
||||
expect(mockUseWorkflowInlineAgentDetail).toHaveBeenCalledWith('agent-node', 'inline-agent-1')
|
||||
expect(screen.queryByText('Workflow Agent 1')).not.toBeInTheDocument()
|
||||
const trigger = screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/ })
|
||||
expect(screen.getByRole('dialog', { name: 'Workflow Agent 1' })).toBeInTheDocument()
|
||||
const trigger = screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/, hidden: true })
|
||||
expect(within(trigger).getByText('workflow.nodes.agent.roster.inlineSetup.name')).toBeInTheDocument()
|
||||
expect(within(trigger).getByText('workflow.nodes.agent.roster.inlineSetup.type')).toBeInTheDocument()
|
||||
const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
|
||||
const panelConfigureIcon = panel.querySelector('.i-custom-vender-agent-v2-configure')
|
||||
const panel = screen.getByRole('dialog', { name: 'Workflow Agent 1' })
|
||||
expect(container.querySelector('.i-custom-vender-agent-v2-configure')).toHaveClass('h-3.5', 'w-3')
|
||||
expect(container.querySelector('.i-custom-vender-agent-v2-configure')?.parentElement).toHaveClass('size-8', 'rounded-full', 'bg-background-default-burn')
|
||||
expect(panelConfigureIcon).toHaveClass('h-3.5', 'w-3')
|
||||
expect(panelConfigureIcon?.parentElement).toHaveClass('size-9', 'rounded-full', 'bg-background-default-burn')
|
||||
expect(screen.queryByText('workflow.nodes.agent.roster.inlineSetup.title')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.nodes.agent.roster.inlineSetup.description')).toBeInTheDocument()
|
||||
expect(within(panel).getByText('workflow.nodes.agent.roster.inlineSetup.description')).toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
|
||||
expect(mockOrchestrateDrawerPanelProps.at(-1)).toMatchObject({
|
||||
agentId: 'inline-agent-1',
|
||||
@ -554,14 +602,11 @@ describe('agent/panel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
|
||||
const panel = screen.getByRole('dialog', { name: 'Workflow Agent 1' })
|
||||
expect(panel).toBeInTheDocument()
|
||||
expect(panel.querySelector('header')).not.toHaveClass('h-[108px]')
|
||||
expect(panel.querySelector('.i-custom-vender-agent-v2-configure')?.parentElement).toHaveClass('size-9', 'rounded-full', 'bg-background-default-burn')
|
||||
expect(within(panel).queryByText('Workflow Agent 1')).not.toBeInTheDocument()
|
||||
expect(within(panel).getByText('workflow.nodes.agent.roster.inlineSetup.type')).toBeInTheDocument()
|
||||
expect(within(panel).getByText('Workflow Agent 1')).toBeInTheDocument()
|
||||
expect(within(panel).queryByText('workflow.nodes.agent.roster.inlineSetup.title')).not.toBeInTheDocument()
|
||||
expect(within(panel).queryByText('workflow.nodes.agent.roster.inlineSetup.description')).not.toBeInTheDocument()
|
||||
expect(within(panel).getByText('workflow.nodes.agent.roster.inlineSetup.description')).toBeInTheDocument()
|
||||
expect(within(panel).queryByRole('link', { name: 'workflow.nodes.agent.roster.editInConsole' })).not.toBeInTheDocument()
|
||||
expect(within(panel).queryByRole('button', { name: 'workflow.nodes.agent.roster.makeCopy' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
|
||||
@ -583,10 +628,10 @@ describe('agent/panel', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
|
||||
const panel = screen.getByRole('dialog', { name: 'Workflow Agent 1' })
|
||||
fireEvent.click(within(panel).getByRole('button', { name: 'workflow.nodes.agent.roster.more' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'agentV2.roster.saveToRoster' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save inline agent to roster' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save inline agent to roster', hidden: true }))
|
||||
|
||||
expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith(undefined)
|
||||
expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith(
|
||||
|
||||
@ -2,8 +2,11 @@
|
||||
|
||||
import type { AgentConfigSnapshotSummaryResponse, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { WorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
||||
import { useAtom } from 'jotai'
|
||||
import { 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'
|
||||
import { useDefaultModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
@ -11,6 +14,11 @@ import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provid
|
||||
import { useHydrateAgentSoulConfigDraft } from '@/features/agent-v2/agent-composer/store'
|
||||
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
|
||||
import { AgentOrchestratePanel } from '@/features/agent-v2/agent-detail/configure/components/orchestrate'
|
||||
import { AgentBuildPanelBackground } from '@/features/agent-v2/agent-detail/configure/components/preview/build-background'
|
||||
import { AgentBuildChat } from '@/features/agent-v2/agent-detail/configure/components/preview/build-chat'
|
||||
import { AgentPreviewHeader } from '@/features/agent-v2/agent-detail/configure/components/preview/header'
|
||||
import { AgentConfigurePreviewSurface, AgentConfigureWorkspace } from '@/features/agent-v2/agent-detail/configure/components/workspace'
|
||||
import { useAgentPreviewSoulConfig } from '@/features/agent-v2/agent-detail/configure/hooks'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config'
|
||||
|
||||
@ -20,6 +28,7 @@ type AgentOrchestrateDrawerPanelProps = {
|
||||
inlineComposerState?: WorkflowAgentComposerResponse
|
||||
isInline: boolean
|
||||
nodeId: string
|
||||
onClose?: () => void
|
||||
open: boolean
|
||||
}
|
||||
|
||||
@ -31,6 +40,14 @@ export function AgentOrchestrateDrawerPanel(props: AgentOrchestrateDrawerPanelPr
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkflowInlineAgentConfigureWorkspace(props: AgentOrchestrateDrawerPanelProps) {
|
||||
return (
|
||||
<AgentComposerProvider>
|
||||
<WorkflowInlineAgentConfigureWorkspaceContent {...props} />
|
||||
</AgentComposerProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentOrchestrateDrawerPanelContent({
|
||||
agentId,
|
||||
appId,
|
||||
@ -99,6 +116,183 @@ function AgentOrchestrateDrawerPanelContent({
|
||||
)
|
||||
}
|
||||
|
||||
function WorkflowInlineAgentConfigureWorkspaceContent({
|
||||
agentId,
|
||||
appId,
|
||||
inlineComposerState,
|
||||
nodeId,
|
||||
onClose,
|
||||
open,
|
||||
}: AgentOrchestrateDrawerPanelProps) {
|
||||
const [clearChatList, setClearChatList] = useState(false)
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const composerState = inlineComposerState
|
||||
const agentSoulConfig = composerState?.agent_soul
|
||||
const activeConfigSnapshot = ('active_config_snapshot' in (composerState ?? {}))
|
||||
? composerState?.active_config_snapshot as AgentConfigSnapshotSummaryResponse | null | undefined
|
||||
: undefined
|
||||
const { currentModel, setConfigureModel, textGenerationModelList } = useAgentOrchestrateModelOptions()
|
||||
const { draftSavedAt, saveDraft } = useWorkflowInlineAgentConfigureSync({
|
||||
nodeId,
|
||||
baseConfig: agentSoulConfig,
|
||||
currentModel,
|
||||
enabled: open && !!agentSoulConfig,
|
||||
})
|
||||
const previewAgentSoulConfig = useAgentPreviewSoulConfig(agentSoulConfig as AgentSoulConfig | undefined)
|
||||
|
||||
useHydrateAgentSoulConfigDraft({
|
||||
agentId: `${nodeId}:${agentId ?? 'pending'}`,
|
||||
activeVersionId: activeConfigSnapshot?.id,
|
||||
config: agentSoulConfig as AgentSoulConfig | undefined,
|
||||
})
|
||||
|
||||
if (!agentId || !agentSoulConfig) {
|
||||
return (
|
||||
<div className="flex h-full min-h-80 items-center justify-center bg-components-panel-bg">
|
||||
<Loading type="app" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isSaving)
|
||||
return
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await saveDraft()
|
||||
onClose?.()
|
||||
}
|
||||
finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AgentConfigureWorkspace
|
||||
className="rounded-[inherit]"
|
||||
leftPanel={(
|
||||
<AgentOrchestratePanel
|
||||
agentId={agentId}
|
||||
appId={appId}
|
||||
nodeId={nodeId}
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
agentSoulConfig={agentSoulConfig as AgentSoulConfig}
|
||||
agentName={composerState?.agent?.name}
|
||||
currentModel={currentModel}
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
draftSavedAt={draftSavedAt}
|
||||
showPublishBar={false}
|
||||
bottomBar={(
|
||||
<WorkflowInlineAgentConfigureActionBar
|
||||
isSaving={isSaving}
|
||||
onCancel={() => onClose?.()}
|
||||
onSave={() => {
|
||||
void handleSave()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
className="min-w-90"
|
||||
onSelectModel={setConfigureModel}
|
||||
onPublish={() => {
|
||||
void saveDraft()
|
||||
}}
|
||||
onOpenVersions={() => undefined}
|
||||
/>
|
||||
)}
|
||||
rightPanel={(
|
||||
<AgentConfigurePreviewSurface
|
||||
background={<AgentBuildPanelBackground visible />}
|
||||
header={(
|
||||
<AgentPreviewHeader
|
||||
mode="build"
|
||||
previewEnabled={false}
|
||||
isChatFeaturesOpen={false}
|
||||
onModeChange={() => undefined}
|
||||
onToggleChatFeatures={() => undefined}
|
||||
onOpenVersions={() => undefined}
|
||||
onRefresh={() => {
|
||||
setConversationId(null)
|
||||
setClearChatList(true)
|
||||
}}
|
||||
showChatFeaturesAction={false}
|
||||
/>
|
||||
)}
|
||||
chat={(
|
||||
<AgentBuildChat
|
||||
agentId={agentId}
|
||||
agentIcon={composerState?.agent?.icon}
|
||||
agentIconBackground={composerState?.agent?.icon_background}
|
||||
agentIconType={composerState?.agent?.icon_type as Parameters<typeof AgentBuildChat>[0]['agentIconType']}
|
||||
agentName={composerState?.agent?.name}
|
||||
agentSoulConfig={previewAgentSoulConfig}
|
||||
clearChatList={clearChatList}
|
||||
conversationId={conversationId}
|
||||
draftType="debug_build"
|
||||
onClearChatListChange={setClearChatList}
|
||||
onConversationIdChange={setConversationId}
|
||||
onSaveDraftBeforeRun={saveDraft}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkflowInlineAgentConfigureActionBar({
|
||||
isSaving,
|
||||
onCancel,
|
||||
onSave,
|
||||
}: {
|
||||
isSaving: boolean
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 flex justify-center bg-gradient-to-t from-components-panel-bg pt-4 pb-2">
|
||||
<div className="pointer-events-auto flex items-center gap-2 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="min-w-18"
|
||||
disabled={isSaving}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('operation.cancel')}
|
||||
</Button>
|
||||
<div className="flex h-4 items-start px-1">
|
||||
<div className="h-full w-px bg-divider-regular" />
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="px-2"
|
||||
disabled={isSaving}
|
||||
aria-label={t('operation.more')}
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="medium"
|
||||
className="min-w-20"
|
||||
loading={isSaving}
|
||||
onClick={onSave}
|
||||
>
|
||||
{t('operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function useAgentOrchestrateModelOptions() {
|
||||
const [model, setModel] = useAtom(agentComposerModelAtom)
|
||||
const {
|
||||
|
||||
@ -3,6 +3,7 @@ import type { AgentRosterNodeData } from '@/app/components/workflow/block-select
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import {
|
||||
Drawer,
|
||||
DrawerCloseButton,
|
||||
@ -243,6 +244,60 @@ function AgentRosterDrawer({
|
||||
)
|
||||
}
|
||||
|
||||
function AgentRosterInlineConfigureDialog({
|
||||
agent,
|
||||
children,
|
||||
onSaveInlineToRoster,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
agent: AgentRosterDisplayData
|
||||
children?: ReactNode
|
||||
onSaveInlineToRoster?: () => void
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen)
|
||||
onClose()
|
||||
}}
|
||||
disablePointerDismissal
|
||||
>
|
||||
<DialogContent className="h-[min(760px,calc(100dvh-32px))] w-[min(1120px,calc(100vw-32px))] max-w-none overflow-hidden p-0">
|
||||
<DialogCloseButton className="z-10" />
|
||||
{onSaveInlineToRoster && (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t(`${i18nPrefix}.roster.more`, { ns: 'workflow' })}
|
||||
className="absolute top-3 right-12 z-10 flex size-6 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-44 w-max">
|
||||
<DropdownMenuItem className="gap-2 whitespace-nowrap" onClick={onSaveInlineToRoster}>
|
||||
<span aria-hidden className="i-ri-inbox-archive-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span>{t('roster.saveToRoster', { ns: 'agentV2' })}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DialogTitle className="sr-only">
|
||||
{agent.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{t(`${i18nPrefix}.roster.inlineSetup.description`, { ns: 'workflow' })}
|
||||
</DialogDescription>
|
||||
{children ?? <div className="h-full min-h-80 bg-components-panel-bg" />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentRosterField({
|
||||
agent,
|
||||
agentId,
|
||||
@ -382,21 +437,33 @@ export function AgentRosterField({
|
||||
<span aria-hidden className="i-ri-arrow-right-line size-4" />
|
||||
</span>
|
||||
</button>
|
||||
<AgentRosterDrawer
|
||||
agent={agent}
|
||||
isInlineSetup={isInlineSetup}
|
||||
mode={panelMode}
|
||||
open={panelOpen}
|
||||
portalContainerRef={portalContainerRef}
|
||||
showAccessIcon={!isInlineSetup}
|
||||
showDetailActions={showPanelDetailActions}
|
||||
isCopyPending={isPanelCopyPending}
|
||||
onMakeCopy={onMakeCopy}
|
||||
onSaveInlineToRoster={onSaveInlineToRoster}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
>
|
||||
{panelBody}
|
||||
</AgentRosterDrawer>
|
||||
{isInlineSetup
|
||||
? (
|
||||
<AgentRosterInlineConfigureDialog
|
||||
agent={agent}
|
||||
onSaveInlineToRoster={onSaveInlineToRoster}
|
||||
open={panelOpen}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
>
|
||||
{panelBody}
|
||||
</AgentRosterInlineConfigureDialog>
|
||||
)
|
||||
: (
|
||||
<AgentRosterDrawer
|
||||
agent={agent}
|
||||
mode={panelMode}
|
||||
open={panelOpen}
|
||||
portalContainerRef={portalContainerRef}
|
||||
showAccessIcon
|
||||
showDetailActions={showPanelDetailActions}
|
||||
isCopyPending={isPanelCopyPending}
|
||||
onMakeCopy={onMakeCopy}
|
||||
onSaveInlineToRoster={onSaveInlineToRoster}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
>
|
||||
{panelBody}
|
||||
</AgentRosterDrawer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
|
||||
@ -15,7 +15,7 @@ import { useStore } from '@/app/components/workflow/store'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { AgentAdvancedSettings } from './components/agent-advanced-settings'
|
||||
import { AgentOrchestrateDrawerPanel } from './components/agent-orchestrate-drawer-panel'
|
||||
import { AgentOrchestrateDrawerPanel, WorkflowInlineAgentConfigureWorkspace } from './components/agent-orchestrate-drawer-panel'
|
||||
import { AgentOutputVariables } from './components/agent-output-variables'
|
||||
import { AgentRosterField } from './components/agent-roster-field'
|
||||
import { AgentTaskField } from './components/agent-task-field'
|
||||
@ -211,6 +211,29 @@ export function AgentV2Panel({
|
||||
)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, setOpenInlineAgentPanelNodeId])
|
||||
|
||||
const handleInlineAgentBindingCreated = useCallback((binding: {
|
||||
agent_id: string
|
||||
binding_type: 'inline_agent'
|
||||
current_snapshot_id: string
|
||||
}) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster
|
||||
draft.agent_binding = binding
|
||||
draft._openInlineAgentPanel = true
|
||||
})
|
||||
inputsRef.current = newInputs
|
||||
handleNodeDataUpdateWithSyncDraft(
|
||||
{
|
||||
id,
|
||||
data: newInputs,
|
||||
},
|
||||
{
|
||||
sync: true,
|
||||
notRefreshWhenSyncError: true,
|
||||
},
|
||||
)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
const handleStartFromScratch = useCallback(() => {
|
||||
setIsRosterAgentPanelOpen(false)
|
||||
setIsInlineAgentPanelOpenedFromTrigger(false)
|
||||
@ -236,38 +259,26 @@ export function AgentV2Panel({
|
||||
)
|
||||
|
||||
createInlineAgentBinding(id, {
|
||||
onSuccess: (binding) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster
|
||||
draft.agent_binding = binding
|
||||
draft._openInlineAgentPanel = true
|
||||
})
|
||||
inputsRef.current = newInputs
|
||||
handleNodeDataUpdateWithSyncDraft(
|
||||
{
|
||||
id,
|
||||
data: newInputs,
|
||||
},
|
||||
{
|
||||
sync: true,
|
||||
notRefreshWhenSyncError: true,
|
||||
},
|
||||
)
|
||||
},
|
||||
onSuccess: handleInlineAgentBindingCreated,
|
||||
})
|
||||
}, [createInlineAgentBinding, handleNodeDataUpdateWithSyncDraft, id, setOpenInlineAgentPanelNodeId])
|
||||
}, [createInlineAgentBinding, handleInlineAgentBindingCreated, handleNodeDataUpdateWithSyncDraft, id, setOpenInlineAgentPanelNodeId])
|
||||
|
||||
const handleAgentPanelOpenChange = useCallback((open: boolean) => {
|
||||
if (isInlineAgentReady) {
|
||||
if (isInlineAgentReady || isInlineAgentPending) {
|
||||
if (open)
|
||||
setIsInlineAgentPanelOpenedFromTrigger(true)
|
||||
|
||||
setOpenInlineAgentPanelNodeId(open ? id : undefined)
|
||||
if (open && isInlineAgentPending && !isCreatingInlineAgent) {
|
||||
createInlineAgentBinding(id, {
|
||||
onSuccess: handleInlineAgentBindingCreated,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setIsRosterAgentPanelOpen(open)
|
||||
}, [id, isInlineAgentReady, setOpenInlineAgentPanelNodeId])
|
||||
}, [createInlineAgentBinding, handleInlineAgentBindingCreated, id, isCreatingInlineAgent, isInlineAgentPending, isInlineAgentReady, setOpenInlineAgentPanelNodeId])
|
||||
|
||||
const handleDeclaredOutputsChange = useCallback((outputs: ReturnType<typeof getAgentV2DeclaredOutputs>, agentTask?: string) => {
|
||||
const previousOutputs = getAgentV2DeclaredOutputs(inputsRef.current)
|
||||
@ -322,14 +333,28 @@ export function AgentV2Panel({
|
||||
isPending={isAgentBindingPending}
|
||||
panelBody={isAgentPanelOpen && displayedAgent
|
||||
? (
|
||||
<AgentOrchestrateDrawerPanel
|
||||
agentId={inlineAgentId ?? rosterAgentId}
|
||||
appId={appId}
|
||||
inlineComposerState={inlineAgentQuery.data}
|
||||
isInline={isInlineAgentReady || isInlineAgentPending}
|
||||
nodeId={id}
|
||||
open={isAgentPanelOpen}
|
||||
/>
|
||||
isInlineAgentReady || isInlineAgentPending
|
||||
? (
|
||||
<WorkflowInlineAgentConfigureWorkspace
|
||||
agentId={inlineAgentId ?? undefined}
|
||||
appId={appId}
|
||||
inlineComposerState={inlineAgentQuery.data}
|
||||
isInline
|
||||
nodeId={id}
|
||||
onClose={() => handleAgentPanelOpenChange(false)}
|
||||
open={isAgentPanelOpen}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AgentOrchestrateDrawerPanel
|
||||
agentId={rosterAgentId}
|
||||
appId={appId}
|
||||
inlineComposerState={inlineAgentQuery.data}
|
||||
isInline={false}
|
||||
nodeId={id}
|
||||
open={isAgentPanelOpen}
|
||||
/>
|
||||
)
|
||||
)
|
||||
: undefined}
|
||||
panelMode={isInlineAgentPending || (isInlineAgentReady && !isInlineAgentPanelOpenedFromTrigger) ? 'setup' : 'detail'}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { AgentConfigurePage } from '../page'
|
||||
|
||||
@ -196,13 +196,26 @@ vi.mock('../components/orchestrate/build-draft-bar', () => ({
|
||||
vi.mock('../components/preview/build-chat', () => ({
|
||||
AgentBuildChat: (props: {
|
||||
conversationId?: string | null
|
||||
onConversationComplete?: () => void
|
||||
onConversationIdChange?: (conversationId: string) => void
|
||||
onSaveDraftBeforeRun?: () => Promise<void>
|
||||
}) => (
|
||||
<div role="region" aria-label="build-chat">
|
||||
<span>{`build:${props.conversationId ?? 'none'}`}</span>
|
||||
<button type="button" onClick={() => props.onConversationIdChange?.('build-conversation-new')}>
|
||||
save build conversation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void props.onSaveDraftBeforeRun?.()
|
||||
}}
|
||||
>
|
||||
send build message
|
||||
</button>
|
||||
<button type="button" onClick={() => props.onConversationComplete?.()}>
|
||||
complete build conversation
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
@ -310,6 +323,10 @@ describe('AgentConfigurePage', () => {
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('Loading state', () => {
|
||||
it('should show loading instead of the configure panels while composer data is pending', () => {
|
||||
const queryClient = new QueryClient()
|
||||
@ -487,6 +504,120 @@ describe('AgentConfigurePage', () => {
|
||||
expect(screen.getByRole('region', { name: 'build-draft-bar' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show the build draft bar after a new build conversation refresh completes', async () => {
|
||||
vi.useFakeTimers()
|
||||
const queryClient = new QueryClient()
|
||||
const refetchBuildDraft = vi.fn().mockResolvedValue({})
|
||||
mocks.queryState.composer = {
|
||||
data: {
|
||||
agent_soul: {
|
||||
prompt: {
|
||||
system_prompt: 'draft prompt',
|
||||
},
|
||||
},
|
||||
},
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
refetch: vi.fn(),
|
||||
}
|
||||
mocks.queryState.buildDraft = {
|
||||
data: {
|
||||
agent_soul: {
|
||||
prompt: {
|
||||
system_prompt: 'build prompt',
|
||||
},
|
||||
},
|
||||
draft: {},
|
||||
variant: 'agent_app',
|
||||
},
|
||||
dataUpdatedAt: 1,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
refetch: refetchBuildDraft,
|
||||
}
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AgentConfigurePage agentId="agent-1" />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('region', { name: 'build-draft-bar' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'send build message' }))
|
||||
|
||||
expect(screen.queryByRole('region', { name: 'build-draft-bar' })).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'complete build conversation' }))
|
||||
|
||||
expect(refetchBuildDraft).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
})
|
||||
|
||||
expect(refetchBuildDraft).toHaveBeenCalled()
|
||||
expect(screen.getByRole('region', { name: 'build-draft-bar' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should discard the build draft when restarting build mode with a build draft', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = new QueryClient()
|
||||
mocks.queryState.composer = {
|
||||
data: {},
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
refetch: vi.fn(),
|
||||
}
|
||||
mocks.queryState.buildDraft = {
|
||||
data: {
|
||||
agent_soul: {},
|
||||
draft: {},
|
||||
variant: 'agent_app',
|
||||
},
|
||||
dataUpdatedAt: 1,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
refetch: vi.fn(),
|
||||
}
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AgentConfigurePage agentId="agent-1" />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'restart preview' }))
|
||||
|
||||
await waitFor(() => expect(mocks.discardBuildDraft).toHaveBeenCalledWith(
|
||||
{
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
},
|
||||
expect.any(Object),
|
||||
))
|
||||
expect(mocks.refreshDebugConversation).toHaveBeenCalledWith({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
body: {
|
||||
debug_conversation_id: 'debug-conversation-old',
|
||||
},
|
||||
}, expect.any(Object))
|
||||
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:none')
|
||||
})
|
||||
|
||||
it('should switch soul source to view version when selecting a version from build draft mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
@ -35,6 +35,7 @@ export function AgentPreviewHeader({
|
||||
onOpenVersions,
|
||||
onRefresh,
|
||||
refreshDisabled,
|
||||
showChatFeaturesAction = true,
|
||||
}: {
|
||||
mode: AgentConfigureRightPanelMode
|
||||
previewEnabled: boolean
|
||||
@ -44,6 +45,7 @@ export function AgentPreviewHeader({
|
||||
onOpenVersions: () => void
|
||||
onRefresh: () => void
|
||||
refreshDisabled?: boolean
|
||||
showChatFeaturesAction?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const docLink = useDocLink()
|
||||
@ -129,20 +131,24 @@ export function AgentPreviewHeader({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<SegmentedControlDivider className="mx-3" />
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isChatFeaturesOpen}
|
||||
onClick={onToggleChatFeatures}
|
||||
className={cn(
|
||||
'flex h-8 items-center justify-center gap-1 rounded-lg px-2 py-2 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',
|
||||
isChatFeaturesOpen && 'bg-state-base-hover text-text-secondary',
|
||||
)}
|
||||
aria-label={t('agentDetail.configure.preview.chatFeatures')}
|
||||
>
|
||||
<span aria-hidden className="i-ri-chat-settings-line size-4" />
|
||||
<span className="px-0.5 system-sm-medium">{t('agentDetail.configure.preview.chatFeatures')}</span>
|
||||
</button>
|
||||
{showChatFeaturesAction && (
|
||||
<>
|
||||
<SegmentedControlDivider className="mx-3" />
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isChatFeaturesOpen}
|
||||
onClick={onToggleChatFeatures}
|
||||
className={cn(
|
||||
'flex h-8 items-center justify-center gap-1 rounded-lg px-2 py-2 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',
|
||||
isChatFeaturesOpen && 'bg-state-base-hover text-text-secondary',
|
||||
)}
|
||||
aria-label={t('agentDetail.configure.preview.chatFeatures')}
|
||||
>
|
||||
<span aria-hidden className="i-ri-chat-settings-line size-4" />
|
||||
<span className="px-0.5 system-sm-medium">{t('agentDetail.configure.preview.chatFeatures')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type AgentConfigureWorkspaceProps = {
|
||||
'aria-busy'?: boolean
|
||||
'className'?: string
|
||||
'leftPanel': ReactNode
|
||||
'rightPanel': ReactNode
|
||||
'sidePanels'?: ReactNode
|
||||
}
|
||||
|
||||
export function AgentConfigureWorkspace({
|
||||
'aria-busy': ariaBusy,
|
||||
className,
|
||||
leftPanel,
|
||||
rightPanel,
|
||||
sidePanels,
|
||||
}: AgentConfigureWorkspaceProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={t('agentDetail.sections.configure')}
|
||||
aria-busy={ariaBusy}
|
||||
className={cn('flex h-full min-w-0 flex-1 gap-1 overflow-hidden bg-background-body p-1', className)}
|
||||
>
|
||||
{leftPanel}
|
||||
<div className="flex min-w-105 flex-1 gap-1 overflow-hidden">
|
||||
{rightPanel}
|
||||
{sidePanels}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
type AgentConfigurePreviewSurfaceProps = {
|
||||
background?: ReactNode
|
||||
chat: ReactNode
|
||||
header: ReactNode
|
||||
}
|
||||
|
||||
export function AgentConfigurePreviewSurface({
|
||||
background,
|
||||
chat,
|
||||
header,
|
||||
}: AgentConfigurePreviewSurfaceProps) {
|
||||
return (
|
||||
<div className="relative isolate flex min-w-105 flex-1 flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-linear-to-b from-background-gradient-bg-fill-chat-bg-1 to-background-gradient-bg-fill-chat-bg-2 shadow-xl shadow-shadow-shadow-5">
|
||||
{background}
|
||||
{header}
|
||||
<div className="relative z-1 min-h-0 flex-1">
|
||||
{chat}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -16,6 +16,7 @@ import { AgentChatFeaturesPanel } from './components/preview/chat-features-panel
|
||||
import { AgentPreviewHeader } from './components/preview/header'
|
||||
import { AgentPreviewChat } from './components/preview/preview-chat'
|
||||
import { AgentPreviewVersionsPanel } from './components/preview/versions-panel'
|
||||
import { AgentConfigurePreviewSurface, AgentConfigureWorkspace } from './components/workspace'
|
||||
import { useAgentConfigureData, useAgentConfigureModelOptions, useAgentPreviewSoulConfig } from './hooks'
|
||||
import { useAgentConfigureBuildDraftActions, useAgentConfigureBuildDraftData } from './use-agent-configure-build-draft'
|
||||
import { useAgentConfigureSync } from './use-agent-configure-sync'
|
||||
@ -90,6 +91,7 @@ function AgentConfigurePageLoadedContent({
|
||||
const [showPreviewVersions, setShowPreviewVersions] = useState(false)
|
||||
const [clearPreviewChat, setClearPreviewChat] = useState(false)
|
||||
const [rightPanelMode, setRightPanelMode] = useState<AgentConfigureRightPanelMode>('build')
|
||||
const [hideBuildDraftBarUntilRefresh, setHideBuildDraftBarUntilRefresh] = useState(false)
|
||||
const {
|
||||
agentQuery,
|
||||
composerQuery,
|
||||
@ -113,6 +115,7 @@ function AgentConfigurePageLoadedContent({
|
||||
isViewingVersion,
|
||||
normalAgentSoulConfig: agentSoulConfig,
|
||||
})
|
||||
const showBuildDraftBar = buildDraft.isActive && !hideBuildDraftBarUntilRefresh
|
||||
const refreshDebugConversationMutation = useMutation(consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions({
|
||||
onSuccess: ({ debug_conversation_id }) => {
|
||||
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
|
||||
@ -162,16 +165,6 @@ function AgentConfigurePageLoadedContent({
|
||||
[mode]: conversationId,
|
||||
}))
|
||||
}
|
||||
const restartCurrentChat = () => {
|
||||
if (rightPanelChatMode === 'build')
|
||||
refreshDebugConversation(conversationIds.build ?? '')
|
||||
|
||||
setConversationIds(current => ({
|
||||
...current,
|
||||
[rightPanelChatMode]: null,
|
||||
}))
|
||||
setClearPreviewChat(true)
|
||||
}
|
||||
const resetBuildChatSession = useCallback(async () => {
|
||||
await refreshDebugConversationAsync(conversationIds.build ?? '')
|
||||
setConversationIds(current => ({
|
||||
@ -215,6 +208,21 @@ function AgentConfigurePageLoadedContent({
|
||||
buildDraft.setSoulSourceOverride(versionId ? 'view-version' : null)
|
||||
onSelectVersion(versionId)
|
||||
}, [buildDraft, onSelectVersion])
|
||||
const restartCurrentChat = () => {
|
||||
if (rightPanelChatMode === 'build' && buildDraft.isActive) {
|
||||
void buildDraftActions.discardBuildDraft()
|
||||
return
|
||||
}
|
||||
|
||||
if (rightPanelChatMode === 'build')
|
||||
refreshDebugConversation(conversationIds.build ?? '')
|
||||
|
||||
setConversationIds(current => ({
|
||||
...current,
|
||||
[rightPanelChatMode]: null,
|
||||
}))
|
||||
setClearPreviewChat(true)
|
||||
}
|
||||
|
||||
if (buildDraft.isPending) {
|
||||
return (
|
||||
@ -229,62 +237,60 @@ function AgentConfigurePageLoadedContent({
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={t('agentDetail.sections.configure')}
|
||||
<AgentConfigureWorkspace
|
||||
aria-busy={agentQuery.isFetching}
|
||||
className="flex h-full min-w-0 flex-1 gap-1 overflow-hidden bg-background-body p-1"
|
||||
>
|
||||
<AgentOrchestratePanel
|
||||
agentId={agentId}
|
||||
activeConfigIsPublished={agentQuery.data?.active_config_is_published}
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
agentSoulConfig={buildDraft.agentSoulConfig}
|
||||
agentName={agentQuery.data?.name}
|
||||
currentModel={currentModel}
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
draftSavedAt={draftSavedAt}
|
||||
isPublishing={isPublishing}
|
||||
readOnly={isViewingVersion || buildDraft.isActive}
|
||||
selectedVersionSnapshot={isViewingVersion ? activeConfigSnapshot : undefined}
|
||||
isBuildDraftActive={buildDraft.isActive}
|
||||
showPublishBar={!buildDraft.isActive}
|
||||
bottomBar={buildDraft.isActive
|
||||
? (
|
||||
<AgentBuildDraftBar
|
||||
changesCount={buildDraft.changesCount}
|
||||
isApplying={buildDraftActions.isApplyingBuildDraft}
|
||||
isDiscarding={buildDraftActions.isDiscardingBuildDraft}
|
||||
onApply={() => {
|
||||
void buildDraftActions.applyBuildDraft()
|
||||
}}
|
||||
onDiscard={() => {
|
||||
void buildDraftActions.discardBuildDraft()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: undefined}
|
||||
onSelectModel={setConfigureModel}
|
||||
onPublish={publishDraft}
|
||||
onOpenVersions={() => setShowPreviewVersions(true)}
|
||||
onExitVersions={() => selectVersion(null)}
|
||||
/>
|
||||
|
||||
{/* Preview area */}
|
||||
<div className="flex min-w-105 flex-1 gap-1 overflow-hidden">
|
||||
<div className="relative isolate flex min-w-105 flex-1 flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-linear-to-b from-background-gradient-bg-fill-chat-bg-1 to-background-gradient-bg-fill-chat-bg-2 shadow-xl shadow-shadow-shadow-5">
|
||||
<AgentBuildPanelBackground visible={rightPanelChatMode === 'build'} />
|
||||
<AgentPreviewHeader
|
||||
mode={rightPanelChatMode}
|
||||
previewEnabled={false}
|
||||
isChatFeaturesOpen={showChatFeatures}
|
||||
onModeChange={setRightPanelMode}
|
||||
onToggleChatFeatures={() => setShowChatFeatures(open => !open)}
|
||||
onOpenVersions={() => setShowPreviewVersions(true)}
|
||||
onRefresh={restartCurrentChat}
|
||||
refreshDisabled={isRefreshingDebugConversation}
|
||||
/>
|
||||
|
||||
<div className="relative z-1 min-h-0 flex-1">
|
||||
leftPanel={(
|
||||
<AgentOrchestratePanel
|
||||
agentId={agentId}
|
||||
activeConfigIsPublished={agentQuery.data?.active_config_is_published}
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
agentSoulConfig={buildDraft.agentSoulConfig}
|
||||
agentName={agentQuery.data?.name}
|
||||
currentModel={currentModel}
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
draftSavedAt={draftSavedAt}
|
||||
isPublishing={isPublishing}
|
||||
readOnly={isViewingVersion || buildDraft.isActive}
|
||||
selectedVersionSnapshot={isViewingVersion ? activeConfigSnapshot : undefined}
|
||||
isBuildDraftActive={buildDraft.isActive}
|
||||
showPublishBar={!buildDraft.isActive}
|
||||
bottomBar={showBuildDraftBar
|
||||
? (
|
||||
<AgentBuildDraftBar
|
||||
changesCount={buildDraft.changesCount}
|
||||
isApplying={buildDraftActions.isApplyingBuildDraft}
|
||||
isDiscarding={buildDraftActions.isDiscardingBuildDraft}
|
||||
onApply={() => {
|
||||
void buildDraftActions.applyBuildDraft()
|
||||
}}
|
||||
onDiscard={() => {
|
||||
void buildDraftActions.discardBuildDraft()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: undefined}
|
||||
onSelectModel={setConfigureModel}
|
||||
onPublish={publishDraft}
|
||||
onOpenVersions={() => setShowPreviewVersions(true)}
|
||||
onExitVersions={() => selectVersion(null)}
|
||||
/>
|
||||
)}
|
||||
rightPanel={(
|
||||
<AgentConfigurePreviewSurface
|
||||
background={<AgentBuildPanelBackground visible={rightPanelChatMode === 'build'} />}
|
||||
header={(
|
||||
<AgentPreviewHeader
|
||||
mode={rightPanelChatMode}
|
||||
previewEnabled={false}
|
||||
isChatFeaturesOpen={showChatFeatures}
|
||||
onModeChange={setRightPanelMode}
|
||||
onToggleChatFeatures={() => setShowChatFeatures(open => !open)}
|
||||
onOpenVersions={() => setShowPreviewVersions(true)}
|
||||
onRefresh={restartCurrentChat}
|
||||
refreshDisabled={isRefreshingDebugConversation || buildDraftActions.isDiscardingBuildDraft}
|
||||
/>
|
||||
)}
|
||||
chat={(
|
||||
<AgentRightPanelChatWithDraftConfig
|
||||
agentId={agentId}
|
||||
agentIcon={agentQuery.data?.icon}
|
||||
@ -297,29 +303,40 @@ function AgentConfigurePageLoadedContent({
|
||||
draftType={rightPanelChatMode === 'build' ? 'debug_build' : undefined}
|
||||
mode={rightPanelChatMode}
|
||||
onClearChatListChange={setClearPreviewChat}
|
||||
onConversationComplete={buildDraftActions.refreshBuildDraftAfterBuildChat}
|
||||
onConversationComplete={(mode) => {
|
||||
if (mode === 'build')
|
||||
buildDraftActions.refreshBuildDraftAfterBuildChat(() => setHideBuildDraftBarUntilRefresh(false))
|
||||
}}
|
||||
onConversationIdChange={updateConversationId}
|
||||
onSaveDraftBeforeRun={rightPanelChatMode === 'build' ? buildDraftActions.prepareBuildDraftBeforeRun : saveDraft}
|
||||
onSaveDraftBeforeRun={rightPanelChatMode === 'build'
|
||||
? async () => {
|
||||
setHideBuildDraftBarUntilRefresh(true)
|
||||
await buildDraftActions.prepareBuildDraftBeforeRun()
|
||||
}
|
||||
: saveDraft}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showPreviewVersions && (
|
||||
<AgentPreviewVersionsPanel
|
||||
agentId={agentId}
|
||||
activeVersionId={activeVersionId}
|
||||
onSelectVersion={selectVersion}
|
||||
onClose={() => setShowPreviewVersions(false)}
|
||||
/>
|
||||
)}
|
||||
<AgentChatFeaturesPanel
|
||||
show={showChatFeatures}
|
||||
appFeatures={agentSoulConfig?.app_features}
|
||||
disabled={versionQuery.isPending}
|
||||
onClose={() => setShowChatFeatures(false)}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
sidePanels={(
|
||||
<>
|
||||
{showPreviewVersions && (
|
||||
<AgentPreviewVersionsPanel
|
||||
agentId={agentId}
|
||||
activeVersionId={activeVersionId}
|
||||
onSelectVersion={selectVersion}
|
||||
onClose={() => setShowPreviewVersions(false)}
|
||||
/>
|
||||
)}
|
||||
<AgentChatFeaturesPanel
|
||||
show={showChatFeatures}
|
||||
appFeatures={agentSoulConfig?.app_features}
|
||||
disabled={versionQuery.isPending}
|
||||
onClose={() => setShowChatFeatures(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -157,13 +157,14 @@ export function useAgentConfigureBuildDraftActions({
|
||||
setSoulSourceOverride('build-draft')
|
||||
}, [agentId, buildDraftQueryOptions.queryKey, checkoutBuildDraft, isActive, queryClient, saveDraft, setSoulSourceOverride])
|
||||
|
||||
const refreshBuildDraftAfterBuildChat = useCallback(() => {
|
||||
const refreshBuildDraftAfterBuildChat = useCallback((onRefreshed?: () => void) => {
|
||||
if (buildDraftRefreshTimerRef.current)
|
||||
clearTimeout(buildDraftRefreshTimerRef.current)
|
||||
|
||||
buildDraftRefreshTimerRef.current = setTimeout(() => {
|
||||
buildDraftRefreshTimerRef.current = setTimeout(async () => {
|
||||
buildDraftRefreshTimerRef.current = null
|
||||
void refetchBuildDraft()
|
||||
await refetchBuildDraft()
|
||||
onRefreshed?.()
|
||||
}, 1000)
|
||||
}, [refetchBuildDraft])
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user