refactor(web): extract split layout for variable inspect

- add SplitPanel to share left/right shell and narrow menu handling
- reuse InspectHeaderProps for tab header + actions across tabs
- refactor variables/artifacts tabs to plug into shared split layout
- align right-side header/close behavior and consolidate empty/loading flows
This commit is contained in:
yyh
2026-01-28 14:03:58 +08:00
parent 40a8e8febc
commit 0b6522df42
6 changed files with 365 additions and 344 deletions

View File

@ -1,10 +1,12 @@
import type { FC } from 'react'
import type { InspectHeaderProps } from './inspect-layout'
import type { SandboxFileTreeNode } from '@/types/sandbox-file'
import {
RiCloseLine,
RiDownloadLine,
RiMenuLine,
} from '@remixicon/react'
import { memo, useCallback, useState } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import CopyFeedback from '@/app/components/base/copy-feedback'
@ -13,7 +15,8 @@ import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts-t
import { useAppContext } from '@/context/app-context'
import { useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file'
import { cn } from '@/utils/classnames'
import { useStore } from '../store'
import InspectLayout from './inspect-layout'
import SplitPanel from './split-panel'
const formatFileSize = (bytes: number | null): string => {
if (bytes === null || bytes === 0)
@ -23,88 +26,10 @@ const formatFileSize = (bytes: number | null): string => {
return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`
}
type ArtifactsPreviewPaneProps = {
file: SandboxFileTreeNode | null
onDownload: (node: SandboxFileTreeNode) => void
isDownloading: boolean
onOpenMenu: () => void
}
const ArtifactsPreviewPane = memo<ArtifactsPreviewPaneProps>(({
file,
onDownload,
isDownloading,
onOpenMenu,
}) => {
const ArtifactsTab: FC<InspectHeaderProps> = (headerProps) => {
const { t } = useTranslation('workflow')
const bottomPanelWidth = useStore(s => s.bottomPanelWidth)
if (!file) {
return (
<div className="flex h-full items-center justify-center p-2">
<p className="system-xs-regular text-text-tertiary">
{t('debug.variableInspect.tabArtifacts.selectFile')}
</p>
</div>
)
}
const pathParts = file.path.split('/')
return (
<div className="flex h-full flex-col">
<div className="flex shrink-0 items-center justify-between gap-1 px-2 pt-2">
{bottomPanelWidth < 488 && (
<ActionButton className="shrink-0" onClick={onOpenMenu} aria-label="Open menu">
<RiMenuLine className="h-4 w-4" />
</ActionButton>
)}
<div className="flex w-0 grow items-center gap-1">
<div className="flex items-center gap-1 truncate">
{pathParts.map((part, i) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span className="system-sm-regular text-text-quaternary">/</span>}
<span className={cn(
'system-sm-semibold truncate',
i === pathParts.length - 1 ? 'text-text-secondary' : 'text-text-tertiary',
)}
>
{part}
</span>
</span>
))}
</div>
<span className="system-xs-medium shrink-0 text-text-tertiary">
{formatFileSize(file.size)}
</span>
</div>
<div className="flex shrink-0 items-center gap-1">
<CopyFeedback content={file.path} />
<ActionButton
onClick={() => onDownload(file)}
disabled={isDownloading}
aria-label={`Download ${file.name}`}
>
<RiDownloadLine className="h-4 w-4" />
</ActionButton>
</div>
</div>
<div className="grow overflow-auto p-2">
<div className="flex h-full items-center justify-center rounded-xl bg-background-section">
<p className="system-xs-regular text-text-tertiary">
{t('debug.variableInspect.tabArtifacts.previewNotAvailable')}
</p>
</div>
</div>
</div>
)
})
const ArtifactsTab: FC = () => {
const { t } = useTranslation()
const { userProfile } = useAppContext()
const sandboxId = userProfile?.id
const bottomPanelWidth = useStore(s => s.bottomPanelWidth)
const { data: treeData, hasFiles, isLoading } = useSandboxFilesTree(sandboxId, {
enabled: !!sandboxId,
@ -112,7 +37,6 @@ const ArtifactsTab: FC = () => {
const downloadMutation = useDownloadSandboxFile(sandboxId)
const [selectedFile, setSelectedFile] = useState<SandboxFileTreeNode | null>(null)
const [showLeftPanel, setShowLeftPanel] = useState(true)
const handleFileSelect = useCallback((node: SandboxFileTreeNode) => {
if (node.node_type === 'file')
@ -132,40 +56,35 @@ const ArtifactsTab: FC = () => {
if (isLoading) {
return (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
<InspectLayout {...headerProps}>
<div className="flex h-full items-center justify-center">
<Loading />
</div>
</InspectLayout>
)
}
if (!hasFiles) {
return (
<div className="flex h-full items-center justify-center p-2">
<div className="rounded-lg bg-background-section p-3">
<p className="system-xs-regular text-text-tertiary">
{t('skillSidebar.artifacts.emptyState', { ns: 'workflow' })}
</p>
<InspectLayout {...headerProps}>
<div className="flex h-full items-center justify-center p-2">
<div className="rounded-lg bg-background-section p-3">
<p className="system-xs-regular text-text-tertiary">
{t('skillSidebar.artifacts.emptyState')}
</p>
</div>
</div>
</div>
</InspectLayout>
)
}
const file = selectedFile
return (
<div className={cn('relative flex h-full')}>
{bottomPanelWidth < 488 && showLeftPanel && (
<div role="presentation" className="absolute left-0 top-0 h-full w-full" onClick={() => setShowLeftPanel(false)} />
)}
<div
className={cn(
'flex w-60 shrink-0 flex-col border-r border-divider-burn',
bottomPanelWidth < 488
? showLeftPanel
? 'absolute left-0 top-0 z-10 h-full w-[217px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'
: 'hidden'
: '',
)}
>
<div className="min-h-0 flex-1 overflow-y-auto py-1">
<SplitPanel
{...headerProps}
left={(
<div className="h-full overflow-y-auto py-1">
<ArtifactsTree
data={treeData}
onDownload={handleDownload}
@ -174,16 +93,78 @@ const ArtifactsTab: FC = () => {
isDownloading={downloadMutation.isPending}
/>
</div>
</div>
<div className="w-0 grow">
<ArtifactsPreviewPane
file={selectedFile}
onDownload={handleDownload}
isDownloading={downloadMutation.isPending}
onOpenMenu={() => setShowLeftPanel(true)}
/>
</div>
</div>
)}
>
{({ isNarrow, onOpenMenu, onClose: handleClose }) => (
<>
<div className="flex shrink-0 items-center justify-between gap-1 px-2 pt-2">
<div className="flex min-w-0 flex-1 items-center gap-1">
{isNarrow && (
<ActionButton className="shrink-0" onClick={onOpenMenu} aria-label="Open menu">
<RiMenuLine className="h-4 w-4" />
</ActionButton>
)}
{file && (
<>
<div className="flex w-0 grow items-center gap-1">
<div className="flex items-center gap-1 truncate">
{file.path.split('/').map((part, i, arr) => (
<span key={i} className="flex items-center gap-1">
{i > 0 && <span className="system-sm-regular text-text-quaternary">/</span>}
<span
className={cn(
'system-sm-semibold truncate',
i === arr.length - 1 ? 'text-text-secondary' : 'text-text-tertiary',
)}
>
{part}
</span>
</span>
))}
</div>
<span className="system-xs-medium shrink-0 text-text-tertiary">
{formatFileSize(file.size)}
</span>
</div>
<div className="flex shrink-0 items-center gap-1">
<CopyFeedback content={file.path} />
<ActionButton
onClick={() => handleDownload(file)}
disabled={downloadMutation.isPending}
aria-label={`Download ${file.name}`}
>
<RiDownloadLine className="h-4 w-4" />
</ActionButton>
</div>
</>
)}
</div>
<ActionButton className="shrink-0" onClick={handleClose} aria-label="Close">
<RiCloseLine className="h-4 w-4" />
</ActionButton>
</div>
<div className="flex min-h-0 flex-1 flex-col">
{file
? (
<div className="grow overflow-auto p-2">
<div className="flex h-full items-center justify-center rounded-xl bg-background-section">
<p className="system-xs-regular text-text-tertiary">
{t('debug.variableInspect.tabArtifacts.previewNotAvailable')}
</p>
</div>
</div>
)
: (
<div className="flex h-full items-center justify-center p-2">
<p className="system-xs-regular text-text-tertiary">
{t('debug.variableInspect.tabArtifacts.selectFile')}
</p>
</div>
)}
</div>
</>
)}
</SplitPanel>
)
}

View File

@ -4,11 +4,14 @@ import { RiCloseLine } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import TabHeader from './tab-header'
type InspectLayoutProps = {
export type InspectHeaderProps = {
activeTab: InspectTab
onTabChange: (tab: InspectTab) => void
onClose: () => void
headerActions?: ReactNode
}
type InspectLayoutProps = InspectHeaderProps & {
children: ReactNode
}
@ -31,7 +34,7 @@ const InspectLayout: FC<InspectLayoutProps> = ({
</ActionButton>
</div>
</div>
<div className="min-h-0 flex-1">
<div className="flex min-h-0 flex-1 flex-col">
{children}
</div>
</div>

View File

@ -1,19 +1,15 @@
import type { FC } from 'react'
import { lazy, Suspense, useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import useCurrentVars from '../hooks/use-inspect-vars-crud'
import { useStore } from '../store'
import InspectLayout from './inspect-layout'
import ArtifactsTab from './artifacts-tab'
import { InspectTab } from './types'
import VariablesTab from './variables-tab'
const ArtifactsTab = lazy(() => import('./artifacts-tab'))
const Panel: FC = () => {
const VariablesPanel: FC<{ onClose: () => void }> = ({ onClose }) => {
const { t } = useTranslation('workflow')
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
const [activeTab, setActiveTab] = useState<InspectTab>(InspectTab.Variables)
@ -29,10 +25,6 @@ const Panel: FC = () => {
setCurrentFocusNodeId('')
}, [deleteAllInspectorVars, setCurrentFocusNodeId])
const handleClose = useCallback(() => {
setShowVariableInspectPanel(false)
}, [setShowVariableInspectPanel])
const headerActions = activeTab === InspectTab.Variables && !isVariablesEmpty
? (
<Button variant="ghost" size="small" onClick={handleClear}>
@ -41,21 +33,26 @@ const Panel: FC = () => {
)
: undefined
return (
<InspectLayout
activeTab={activeTab}
onTabChange={setActiveTab}
onClose={handleClose}
headerActions={headerActions}
>
{activeTab === InspectTab.Variables && <VariablesTab />}
{activeTab === InspectTab.Artifacts && (
<Suspense fallback={<div className="flex h-full items-center justify-center"><Loading /></div>}>
<ArtifactsTab />
</Suspense>
)}
</InspectLayout>
)
const headerProps = {
activeTab,
onTabChange: setActiveTab,
onClose,
headerActions,
}
return activeTab === InspectTab.Variables
? <VariablesTab {...headerProps} />
: <ArtifactsTab {...headerProps} />
}
const Panel: FC = () => {
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
const handleClose = useCallback(() => {
setShowVariableInspectPanel(false)
}, [setShowVariableInspectPanel])
return <VariablesPanel onClose={handleClose} />
}
export default Panel

View File

@ -1,7 +1,10 @@
import type { FC } from 'react'
import type { SplitRightProps } from './split-panel'
import type { currentVarType } from './variables-tab'
import type { GenRes } from '@/service/debug'
import {
RiArrowGoBackLine,
RiCloseLine,
RiFileDownloadFill,
RiMenuLine,
RiSparklingFill,
@ -21,7 +24,6 @@ import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/com
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { AppModeEnum } from '@/types/app'
import { VarInInspectType } from '@/types/workflow'
import { cn } from '@/utils/classnames'
import GetCodeGeneratorResModal from '../../app/configuration/config/code-generator/get-code-generator-res'
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '../../base/prompt-editor/plugins/update-block'
import { useNodesInteractions, useToolIcon } from '../hooks'
@ -30,27 +32,26 @@ import useCurrentVars from '../hooks/use-inspect-vars-crud'
import useNodeCrud from '../nodes/_base/hooks/use-node-crud'
import useNodeInfo from '../nodes/_base/hooks/use-node-info'
import { CodeLanguage } from '../nodes/code/types'
import { useStore } from '../store'
import { BlockEnum } from '../types'
import Empty from './empty'
import { formatVarTypeLabel } from './utils'
import ValueContent from './value-content'
type Props = {
type Props = SplitRightProps & {
nodeId: string
currentNodeVar?: currentVarType
handleOpenMenu: () => void
isValueFetching?: boolean
}
const Right = ({
const Right: FC<Props> = ({
nodeId,
currentNodeVar,
handleOpenMenu,
isValueFetching,
}: Props) => {
isNarrow,
onOpenMenu,
onClose,
}) => {
const { t } = useTranslation()
const bottomPanelWidth = useStore(s => s.bottomPanelWidth)
const toolIcon = useToolIcon(currentNodeVar?.nodeData)
const currentVar = currentNodeVar?.var
const currentNodeType = currentNodeVar?.nodeType
@ -89,10 +90,8 @@ const Right = ({
const value = currentVar?.value
if (value === null || value === undefined)
return ''
if (typeof value === 'object')
return JSON.stringify(value)
return String(value)
}
@ -109,10 +108,6 @@ const Right = ({
return ''
if (blockType === BlockEnum.LLM)
return node?.data?.prompt_template?.text || node?.data?.prompt_template?.[0].text
// if (blockType === BlockEnum.Agent) {
// return node?.data?.agent_parameters?.instruction?.value
// }
if (blockType === BlockEnum.Code)
return node?.data?.code
}, [canShowPromptGenerator])
@ -137,13 +132,6 @@ const Right = ({
draft.prompt_template.text = res.modified
}
break
// Agent is a plugin, may has many instructions, can not locate which one to update
// case BlockEnum.Agent:
// if (draft?.agent_parameters?.instruction) {
// draft.agent_parameters.instruction.value = res.modified
// }
// break
case BlockEnum.Code:
draft.code = res.modified
break
@ -167,154 +155,156 @@ const Right = ({
const displaySchemaType = shouldShowSchemaType ? (`(${schemaType})`) : ''
return (
<div className={cn('flex h-full flex-col')}>
{/* header */}
<>
<div className="flex shrink-0 items-center justify-between gap-1 px-2 pt-2">
{bottomPanelWidth < 488 && (
<ActionButton className="shrink-0" onClick={handleOpenMenu} aria-label="Open menu">
<RiMenuLine className="h-4 w-4" />
</ActionButton>
)}
<div className="flex w-0 grow items-center gap-1">
{currentVar && (
<>
{
currentNodeType
&& [VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeType as VarInInspectType) && (
<div className="flex min-w-0 flex-1 items-center gap-1">
{isNarrow && (
<ActionButton className="shrink-0" onClick={onOpenMenu} aria-label="Open menu">
<RiMenuLine className="h-4 w-4" />
</ActionButton>
)}
<div className="flex w-0 grow items-center gap-1">
{currentVar && (
<>
{currentNodeType
&& [VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeType as VarInInspectType) && (
<VariableIconWithColor
variableCategory={currentNodeType as VarInInspectType}
className="size-4"
/>
)
}
{currentNodeType
&& currentNodeType !== VarInInspectType.environment
&& currentNodeType !== VarInInspectType.conversation
&& currentNodeType !== VarInInspectType.system
&& (
<>
<BlockIcon
className="shrink-0"
type={currentNodeType as BlockEnum}
size="xs"
toolIcon={toolIcon}
/>
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeTitle}</div>
<div className="system-sm-regular shrink-0 text-text-quaternary">/</div>
</>
)}
<div title={displayVarName} className="system-sm-semibold truncate text-text-secondary">{displayVarName}</div>
<div className="system-xs-medium ml-1 shrink-0 space-x-2 text-text-tertiary">
<span>{`${valueTypeLabel}${displaySchemaType}`}</span>
{isTruncated && (
<>
<span>·</span>
<span>
{((fullContent?.size_bytes || 0) / 1024 / 1024).toFixed(1)}
MB
</span>
</>
)}
</div>
</>
)}
</div>
<div className="flex shrink-0 items-center gap-1">
{currentVar && (
<>
{canShowPromptGenerator && (
<Tooltip popupContent={t('generate.optimizePromptTooltip', { ns: 'appDebug' })}>
<button
type="button"
className="cursor-pointer rounded-md p-1 hover:bg-state-accent-active"
onClick={handleShowPromptGenerator}
>
<RiSparklingFill className="size-4 text-components-input-border-active-prompt-1" />
</button>
</Tooltip>
)}
{isTruncated && (
<Tooltip popupContent={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })}>
<ActionButton
onClick={() => window.open(fullContent?.download_url, '_blank')}
aria-label={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })}
>
<RiFileDownloadFill className="size-4" />
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentVar.edited && (
<Badge>
<span className="ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary"></span>
<span className="system-2xs-semibold-uupercase">{t('debug.variableInspect.edited', { ns: 'workflow' })}</span>
</Badge>
)}
{!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.reset', { ns: 'workflow' })}>
<ActionButton onClick={resetValue}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.resetConversationVar', { ns: 'workflow' })}>
<ActionButton onClick={handleClear}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{currentVar.value_type !== 'secret' && (
<CopyFeedback content={getCopyContent()} />
)}
</>
)}
</div>
</div>
{/* content */}
<div className="grow p-2">
{!currentVar && <Empty />}
{isValueFetching && (
<div className="flex h-full items-center justify-center">
<Loading />
{currentNodeType
&& currentNodeType !== VarInInspectType.environment
&& currentNodeType !== VarInInspectType.conversation
&& currentNodeType !== VarInInspectType.system
&& (
<>
<BlockIcon
className="shrink-0"
type={currentNodeType as BlockEnum}
size="xs"
toolIcon={toolIcon}
/>
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeTitle}</div>
<div className="system-sm-regular shrink-0 text-text-quaternary">/</div>
</>
)}
<div title={displayVarName} className="system-sm-semibold truncate text-text-secondary">{displayVarName}</div>
<div className="system-xs-medium ml-1 shrink-0 space-x-2 text-text-tertiary">
<span>{`${valueTypeLabel}${displaySchemaType}`}</span>
{isTruncated && (
<>
<span>·</span>
<span>
{((fullContent?.size_bytes || 0) / 1024 / 1024).toFixed(1)}
MB
</span>
</>
)}
</div>
</>
)}
</div>
)}
{currentVar && currentNodeId && !isValueFetching && (
<ValueContent
key={`${currentNodeId}-${currentVar.id}`}
currentVar={currentVar}
handleValueChange={handleValueChange}
isTruncated={!!isTruncated}
/>
<div className="flex shrink-0 items-center gap-1">
{currentVar && (
<>
{canShowPromptGenerator && (
<Tooltip popupContent={t('generate.optimizePromptTooltip', { ns: 'appDebug' })}>
<button
type="button"
className="cursor-pointer rounded-md p-1 hover:bg-state-accent-active"
onClick={handleShowPromptGenerator}
>
<RiSparklingFill className="size-4 text-components-input-border-active-prompt-1" />
</button>
</Tooltip>
)}
{isTruncated && (
<Tooltip popupContent={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })}>
<ActionButton
onClick={() => window.open(fullContent?.download_url, '_blank')}
aria-label={t('debug.variableInspect.exportToolTip', { ns: 'workflow' })}
>
<RiFileDownloadFill className="size-4" />
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentVar.edited && (
<Badge>
<span className="ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary"></span>
<span className="system-2xs-semibold-uupercase">{t('debug.variableInspect.edited', { ns: 'workflow' })}</span>
</Badge>
)}
{!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.reset', { ns: 'workflow' })}>
<ActionButton onClick={resetValue}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && (
<Tooltip popupContent={t('debug.variableInspect.resetConversationVar', { ns: 'workflow' })}>
<ActionButton onClick={handleClear}>
<RiArrowGoBackLine className="h-4 w-4" />
</ActionButton>
</Tooltip>
)}
{currentVar.value_type !== 'secret' && (
<CopyFeedback content={getCopyContent()} />
)}
</>
)}
</div>
</div>
<ActionButton className="shrink-0" onClick={onClose} aria-label="Close">
<RiCloseLine className="h-4 w-4" />
</ActionButton>
</div>
<div className="flex min-h-0 flex-1 flex-col">
<div className="grow p-2">
{!currentVar && <Empty />}
{isValueFetching && (
<div className="flex h-full items-center justify-center">
<Loading />
</div>
)}
{currentVar && currentNodeId && !isValueFetching && (
<ValueContent
key={`${currentNodeId}-${currentVar.id}`}
currentVar={currentVar}
handleValueChange={handleValueChange}
isTruncated={!!isTruncated}
/>
)}
</div>
{isShowPromptGenerator && (
isCodeBlock
? (
<GetCodeGeneratorResModal
isShow
mode={AppModeEnum.CHAT}
onClose={handleHidePromptGenerator}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentCode={currentPrompt}
codeLanguages={node?.data?.code_languages || CodeLanguage.python3}
onFinished={handleUpdatePrompt}
/>
)
: (
<GetAutomaticResModal
mode={AppModeEnum.CHAT}
isShow
onClose={handleHidePromptGenerator}
onFinished={handleUpdatePrompt}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentPrompt={currentPrompt}
/>
)
)}
</div>
{isShowPromptGenerator && (
isCodeBlock
? (
<GetCodeGeneratorResModal
isShow
mode={AppModeEnum.CHAT}
onClose={handleHidePromptGenerator}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentCode={currentPrompt}
codeLanguages={node?.data?.code_languages || CodeLanguage.python3}
onFinished={handleUpdatePrompt}
/>
)
: (
<GetAutomaticResModal
mode={AppModeEnum.CHAT}
isShow
onClose={handleHidePromptGenerator}
onFinished={handleUpdatePrompt}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentPrompt={currentPrompt}
/>
)
)}
</div>
</>
)
}

View File

@ -0,0 +1,62 @@
import type { FC, ReactNode } from 'react'
import type { InspectHeaderProps } from './inspect-layout'
import { useState } from 'react'
import { cn } from '@/utils/classnames'
import { useStore } from '../store'
import TabHeader from './tab-header'
export type SplitRightProps = {
isNarrow: boolean
onOpenMenu: () => void
onClose: () => void
}
type SplitPanelProps = InspectHeaderProps & {
left: ReactNode
children: (rightProps: SplitRightProps) => ReactNode
}
const SplitPanel: FC<SplitPanelProps> = ({
activeTab,
onTabChange,
onClose,
headerActions,
left,
children,
}) => {
const bottomPanelWidth = useStore(s => s.bottomPanelWidth)
const isNarrow = bottomPanelWidth < 488
const [showLeftPanel, setShowLeftPanel] = useState(true)
return (
<div className="flex h-full">
<div className="relative flex w-60 shrink-0 flex-col border-r border-divider-burn">
<div className="flex shrink-0 items-center">
<TabHeader activeTab={activeTab} onTabChange={onTabChange}>
{headerActions}
</TabHeader>
</div>
{isNarrow && showLeftPanel && (
<div role="presentation" className="absolute left-0 top-0 h-full w-full" onClick={() => setShowLeftPanel(false)} />
)}
<div
className={cn(
'min-h-0 flex-1',
isNarrow
? showLeftPanel
? 'absolute left-0 top-0 z-10 h-full w-[217px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'
: 'hidden'
: '',
)}
>
{left}
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col">
{children({ isNarrow, onOpenMenu: () => setShowLeftPanel(true), onClose })}
</div>
</div>
)
}
export default SplitPanel

View File

@ -1,17 +1,19 @@
import type { FC } from 'react'
import type { NodeProps } from '../types'
import type { InspectHeaderProps } from './inspect-layout'
import type { VarInInspect } from '@/types/workflow'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { VarInInspectType } from '@/types/workflow'
import { cn } from '@/utils/classnames'
import useCurrentVars from '../hooks/use-inspect-vars-crud'
import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type'
import { useStore } from '../store'
import Empty from './empty'
import InspectLayout from './inspect-layout'
import Left from './left'
import Listening from './listening'
import Right from './right'
import SplitPanel from './split-panel'
import { EVENT_WORKFLOW_STOP } from './types'
import { toEnvVarInInspect } from './utils'
@ -24,11 +26,8 @@ export type currentVarType = {
nodeData?: NodeProps['data']
}
const VariablesTab: FC = () => {
const bottomPanelWidth = useStore(s => s.bottomPanelWidth)
const [showLeftPanel, setShowLeftPanel] = useState(true)
const VariablesTab: FC<InspectHeaderProps> = (headerProps) => {
const isListening = useStore(s => s.isListening)
const environmentVariables = useStore(s => s.environmentVariables)
const currentFocusNodeId = useStore(s => s.currentFocusNodeId)
const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId)
@ -42,8 +41,7 @@ const VariablesTab: FC = () => {
} = useCurrentVars()
const isEmpty = useMemo(() => {
const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars]
return allVars.length === 0
return [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars].length === 0
}, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
const currentNodeInfo = useMemo(() => {
@ -128,7 +126,7 @@ const VariablesTab: FC = () => {
const { isLoading, schemaTypeDefinitions } = useMatchSchemaType()
const { eventEmitter } = useEventEmitterContextContext()
const handleStopListening = useCallback(() => {
const onStopListening = useCallback(() => {
// eslint-disable-next-line ts/no-explicit-any -- EventEmitter is typed as string but project-wide convention passes { type } objects
eventEmitter?.emit({ type: EVENT_WORKFLOW_STOP } as any)
}, [eventEmitter])
@ -143,49 +141,39 @@ const VariablesTab: FC = () => {
if (isListening) {
return (
<div className="h-full p-2">
<Listening onStop={handleStopListening} />
</div>
<InspectLayout {...headerProps}>
<div className="h-full p-2"><Listening onStop={onStopListening} /></div>
</InspectLayout>
)
}
if (isEmpty) {
return (
<div className="h-full p-2">
<Empty />
</div>
<InspectLayout {...headerProps}>
<div className="h-full p-2"><Empty /></div>
</InspectLayout>
)
}
return (
<div className={cn('relative flex h-full')}>
{bottomPanelWidth < 488 && showLeftPanel && <div role="presentation" className="absolute left-0 top-0 h-full w-full" onClick={() => setShowLeftPanel(false)}></div>}
<div
className={cn(
'flex w-60 shrink-0 flex-col border-r border-divider-burn',
bottomPanelWidth < 488
? showLeftPanel
? 'absolute left-0 top-0 z-10 h-full w-[217px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'
: 'hidden'
: '',
)}
>
<div className="min-h-0 flex-1">
<Left
currentNodeVar={currentNodeInfo as currentVarType}
handleVarSelect={handleNodeVarSelect}
/>
</div>
</div>
<div className="w-0 grow">
<Right
nodeId={currentFocusNodeId!}
isValueFetching={isCurrentNodeVarValueFetching}
<SplitPanel
{...headerProps}
left={(
<Left
currentNodeVar={currentNodeInfo as currentVarType}
handleOpenMenu={() => setShowLeftPanel(true)}
handleVarSelect={handleNodeVarSelect}
/>
</div>
</div>
)}
>
{rightProps => (
<Right
{...rightProps}
nodeId={currentFocusNodeId!}
currentNodeVar={currentNodeInfo as currentVarType}
isValueFetching={isCurrentNodeVarValueFetching}
/>
)}
</SplitPanel>
)
}