Merge branch 'main' into feat/plugin-auto-upgrade-fe

This commit is contained in:
Joel
2025-06-30 14:01:29 +08:00
56 changed files with 3371 additions and 2577 deletions

View File

@ -191,6 +191,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
const { notify } = useContext(ToastContext)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem,
@ -312,18 +313,34 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
return item
}))
}, [allChatItems])
const handleAnnotationRemoved = useCallback((index: number) => {
setAllChatItems(allChatItems.map((item, i) => {
if (i === index) {
return {
...item,
content: item.content,
annotation: undefined,
}
const handleAnnotationRemoved = useCallback(async (index: number): Promise<boolean> => {
const annotation = allChatItems[index]?.annotation
try {
if (annotation?.id) {
const { delAnnotation } = await import('@/service/annotation')
await delAnnotation(appDetail?.id || '', annotation.id)
}
return item
}))
}, [allChatItems])
setAllChatItems(allChatItems.map((item, i) => {
if (i === index) {
return {
...item,
content: item.content,
annotation: undefined,
}
}
return item
}))
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
return true
}
catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
return false
}
}, [allChatItems, appDetail?.id, t])
const fetchInitiated = useRef(false)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -18,7 +18,7 @@ describe('InputNumber Component', () => {
it('renders input with default values', () => {
render(<InputNumber {...defaultProps} />)
const input = screen.getByRole('textbox')
const input = screen.getByRole('spinbutton')
expect(input).toBeInTheDocument()
})
@ -56,7 +56,7 @@ describe('InputNumber Component', () => {
it('handles direct input changes', () => {
render(<InputNumber {...defaultProps} />)
const input = screen.getByRole('textbox')
const input = screen.getByRole('spinbutton')
fireEvent.change(input, { target: { value: '42' } })
expect(defaultProps.onChange).toHaveBeenCalledWith(42)
@ -64,7 +64,7 @@ describe('InputNumber Component', () => {
it('handles empty input', () => {
render(<InputNumber {...defaultProps} value={0} />)
const input = screen.getByRole('textbox')
const input = screen.getByRole('spinbutton')
fireEvent.change(input, { target: { value: '' } })
expect(defaultProps.onChange).toHaveBeenCalledWith(undefined)
@ -72,7 +72,7 @@ describe('InputNumber Component', () => {
it('handles invalid input', () => {
render(<InputNumber {...defaultProps} />)
const input = screen.getByRole('textbox')
const input = screen.getByRole('spinbutton')
fireEvent.change(input, { target: { value: 'abc' } })
expect(defaultProps.onChange).not.toHaveBeenCalled()
@ -86,7 +86,7 @@ describe('InputNumber Component', () => {
it('disables controls when disabled prop is true', () => {
render(<InputNumber {...defaultProps} disabled />)
const input = screen.getByRole('textbox')
const input = screen.getByRole('spinbutton')
const incrementBtn = screen.getByRole('button', { name: /increment/i })
const decrementBtn = screen.getByRole('button', { name: /decrement/i })

View File

@ -55,8 +55,8 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
return <div className={classNames('flex', wrapClassName)}>
<Input {...rest}
// disable default controller
type='text'
className={classNames('rounded-r-none', className)}
type='number'
className={classNames('no-spinner rounded-r-none', className)}
value={value}
max={max}
min={min}
@ -77,8 +77,8 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
size={size}
/>
<div className={classNames(
'flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs',
disabled && 'opacity-50 cursor-not-allowed',
'flex flex-col rounded-r-md border-l border-divider-subtle bg-components-input-bg-normal text-text-tertiary focus:shadow-xs',
disabled && 'cursor-not-allowed opacity-50',
controlWrapClassName)}
>
<button

View File

@ -1,10 +1,10 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid'
import Badge from '../badge/index'
import { RiCheckLine } from '@remixicon/react'
import { RiCheckLine, RiLoader4Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import classNames from '@/utils/classnames'
import {
@ -51,6 +51,8 @@ export type ISelectProps = {
item: Item
selected: boolean
}) => React.ReactNode
isLoading?: boolean
onOpenChange?: (open: boolean) => void
}
const Select: FC<ISelectProps> = ({
className,
@ -178,17 +180,20 @@ const SimpleSelect: FC<ISelectProps> = ({
defaultValue = 1,
disabled = false,
onSelect,
onOpenChange,
placeholder,
optionWrapClassName,
optionClassName,
hideChecked,
notClearable,
renderOption,
isLoading = false,
}) => {
const { t } = useTranslation()
const localPlaceholder = placeholder || t('common.placeholder.select')
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
useEffect(() => {
let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue)
@ -199,8 +204,10 @@ const SimpleSelect: FC<ISelectProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValue])
const listboxRef = useRef<HTMLDivElement>(null)
return (
<Listbox
<Listbox ref={listboxRef}
value={selectedItem}
onChange={(value: Item) => {
if (!disabled) {
@ -212,10 +219,17 @@ const SimpleSelect: FC<ISelectProps> = ({
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
{!renderTrigger && (
<ListboxButton className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
<ListboxButton onClick={() => {
// get data-open, use setTimeout to ensure the attribute is set
setTimeout(() => {
if (listboxRef.current)
onOpenChange?.(listboxRef.current.getAttribute('data-open') !== null)
})
}} className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
<span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
{(selectedItem && !notClearable)
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
: (selectedItem && !notClearable)
? (
<XMarkIcon
onClick={(e) => {
@ -237,7 +251,7 @@ const SimpleSelect: FC<ISelectProps> = ({
</ListboxButton>
)}
{!disabled && (
{(!disabled) && (
<ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => (
<ListboxOption

View File

@ -1,6 +1,5 @@
import { useState } from 'react'
import { useCallback, useState } from 'react'
import type { ChangeEvent, FC, KeyboardEvent } from 'react'
import { } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import AutosizeInput from 'react-18-input-autosize'
import { RiAddLine, RiCloseLine } from '@remixicon/react'
@ -40,6 +39,29 @@ const TagInput: FC<TagInputProps> = ({
onChange(copyItems)
}
const handleNewTag = useCallback((value: string) => {
const valueTrimmed = value.trim()
if (!valueTrimmed) {
notify({ type: 'error', message: t('datasetDocuments.segment.keywordEmpty') })
return
}
if ((items.find(item => item === valueTrimmed))) {
notify({ type: 'error', message: t('datasetDocuments.segment.keywordDuplicate') })
return
}
if (valueTrimmed.length > 20) {
notify({ type: 'error', message: t('datasetDocuments.segment.keywordError') })
return
}
onChange([...items, valueTrimmed])
setTimeout(() => {
setValue('')
})
}, [items, onChange, notify, t])
const handleKeyDown = (e: KeyboardEvent) => {
if (isSpecialMode && e.key === 'Enter')
setValue(`${value}`)
@ -48,24 +70,12 @@ const TagInput: FC<TagInputProps> = ({
if (isSpecialMode)
e.preventDefault()
const valueTrimmed = value.trim()
if (!valueTrimmed || (items.find(item => item === valueTrimmed)))
return
if (valueTrimmed.length > 20) {
notify({ type: 'error', message: t('datasetDocuments.segment.keywordError') })
return
}
onChange([...items, valueTrimmed])
setTimeout(() => {
setValue('')
})
handleNewTag(value)
}
}
const handleBlur = () => {
setValue('')
handleNewTag(value)
setFocused(false)
}

View File

@ -52,8 +52,8 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
datasetId={datasetId || creationCache?.dataset?.id || ''}
batchId={creationCache?.batch || ''}
documents={creationCache?.documents as FullDocumentDetail[]}
indexingType={indexingType || creationCache?.dataset?.indexing_technique}
retrievalMethod={retrievalMethod || creationCache?.dataset?.retrieval_model?.search_method}
indexingType={creationCache?.dataset?.indexing_technique || indexingType}
retrievalMethod={creationCache?.dataset?.retrieval_model_dict?.search_method || retrievalMethod}
/>
</div>
</div>

View File

@ -507,13 +507,15 @@ const StepTwo = ({
const separator = rules.segmentation.separator
const max = rules.segmentation.max_tokens
const overlap = rules.segmentation.chunk_overlap
const isHierarchicalDocument = documentDetail.doc_form === ChunkingMode.parentChild
|| (rules.parent_mode && rules.subchunk_segmentation)
setSegmentIdentifier(separator)
setMaxChunkLength(max)
setOverlap(overlap!)
setRules(rules.pre_processing_rules)
setDefaultConfig(rules)
if (documentDetail.dataset_process_rule.mode === 'hierarchical') {
if (isHierarchicalDocument) {
setParentChildConfig({
chunkForContext: rules.parent_mode || 'paragraph',
parent: {
@ -575,6 +577,7 @@ const StepTwo = ({
onSuccess(data) {
updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
updateResultCache && updateResultCache(data)
updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
},
})
}

View File

@ -19,12 +19,14 @@ export enum FormTypeEnum {
toolSelector = 'tool-selector',
multiToolSelector = 'array[tools]',
appSelector = 'app-selector',
dynamicSelect = 'dynamic-select',
}
export type FormOption = {
label: TypeWithI18N
value: string
show_on: FormShowOnObject[]
icon?: string
}
export enum ModelTypeEnum {

View File

@ -30,7 +30,7 @@ const HeaderWrapper = ({
return (
<div className={classNames(
'sticky top-0 left-0 right-0 z-30 flex flex-col grow-0 shrink-0 basis-auto min-h-[56px]',
'sticky left-0 right-0 top-0 z-[15] flex min-h-[56px] shrink-0 grow-0 basis-auto flex-col',
s.header,
isBordered ? 'border-b border-divider-regular' : '',
)}

View File

@ -18,6 +18,15 @@ type Props = {
onSaved: (value: Record<string, any>) => void
}
const extractDefaultValues = (schemas: any[]) => {
const result: Record<string, any> = {}
for (const field of schemas) {
if (field.default !== undefined)
result[field.name] = field.default
}
return result
}
const EndpointModal: FC<Props> = ({
formSchemas,
defaultValues = {},
@ -26,7 +35,10 @@ const EndpointModal: FC<Props> = ({
}) => {
const getValueFromI18nObject = useRenderI18nObject()
const { t } = useTranslation()
const [tempCredential, setTempCredential] = React.useState<any>(defaultValues)
const initialValues = Object.keys(defaultValues).length > 0
? defaultValues
: extractDefaultValues(formSchemas)
const [tempCredential, setTempCredential] = React.useState<any>(initialValues)
const handleSave = () => {
for (const field of formSchemas) {

View File

@ -117,6 +117,7 @@ const MultipleToolSelector = ({
)}
{!disabled && (
<ActionButton className='mx-1' onClick={() => {
setCollapse(false)
setOpen(!open)
setPanelShowState(true)
}}>
@ -126,23 +127,6 @@ const MultipleToolSelector = ({
</div>
{!collapse && (
<>
<ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope}
value={undefined}
selectedTools={value}
onSelect={handleAdd}
controlledState={open}
onControlledStateChange={setOpen}
trigger={
<div className=''></div>
}
panelShowState={panelShowState}
onPanelShowStateChange={setPanelShowState}
isEdit={false}
/>
{value.length === 0 && (
<div className='system-xs-regular flex justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>{t('plugin.detailPanel.toolSelector.empty')}</div>
)}
@ -164,6 +148,23 @@ const MultipleToolSelector = ({
))}
</>
)}
<ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope}
value={undefined}
selectedTools={value}
onSelect={handleAdd}
controlledState={open}
onControlledStateChange={setOpen}
trigger={
<div className=''></div>
}
panelShowState={panelShowState}
onPanelShowStateChange={setPanelShowState}
isEdit={false}
/>
</>
)
}

View File

@ -275,7 +275,7 @@ const ToolSelector: FC<Props> = ({
/>
)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<PortalToFollowElemContent>
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', !isShowSettingAuth && 'overflow-y-auto pb-2')}>
{!isShowSettingAuth && (
<>

View File

@ -36,7 +36,7 @@ export type ToolValue = {
provider_name: string
tool_name: string
tool_label: string
tool_description: string
tool_description?: string
settings?: Record<string, any>
parameters?: Record<string, any>
enabled?: boolean

View File

@ -33,6 +33,8 @@ const HeaderInNormal = ({
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
const nodes = useNodes<StartNodeType>()
const selectedNode = nodes.find(node => node.data.selected)
const { handleBackupDraft } = useWorkflowRun()
@ -46,8 +48,10 @@ const HeaderInNormal = ({
setShowWorkflowVersionHistoryPanel(true)
setShowEnvPanel(false)
setShowDebugAndPreviewPanel(false)
setShowVariableInspectPanel(false)
setShowChatVariablePanel(false)
}, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode,
setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel])
setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel])
return (
<>

View File

@ -15,6 +15,7 @@ import {
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
import ActionButton from '@/app/components/base/action-button'
type Props = {
className?: string
@ -88,15 +89,16 @@ const Base: FC<Props> = ({
<CodeGeneratorButton onGenerated={onGenerated} codeLanguages={codeLanguages} />
</div>
)}
{!isCopied
? (
<Clipboard className='mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary' onClick={handleCopy} />
)
: (
<ClipboardCheck className='mx-1 h-3.5 w-3.5 text-text-tertiary' />
)
}
<ActionButton className='ml-1' onClick={handleCopy}>
{!isCopied
? (
<Clipboard className='h-4 w-4 cursor-pointer' />
)
: (
<ClipboardCheck className='h-4 w-4' />
)
}
</ActionButton>
<div className='ml-1'>
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
</div>

View File

@ -13,6 +13,8 @@ type Props = {
readonly: boolean
value: string
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
onOpenChange?: (open: boolean) => void
isLoading?: boolean
}
const DEFAULT_SCHEMA = {} as CredentialFormSchema
@ -22,6 +24,8 @@ const ConstantField: FC<Props> = ({
readonly,
value,
onChange,
onOpenChange,
isLoading,
}) => {
const language = useLanguage()
const placeholder = (schema as CredentialFormSchemaSelect).placeholder
@ -36,7 +40,7 @@ const ConstantField: FC<Props> = ({
return (
<>
{schema.type === FormTypeEnum.select && (
{(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && (
<SimpleSelect
wrapperClassName='w-full !h-8'
className='flex items-center'
@ -45,6 +49,8 @@ const ConstantField: FC<Props> = ({
items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleSelectChange(item.value)}
placeholder={placeholder?.[language] || placeholder?.en_US}
onOpenChange={onOpenChange}
isLoading={isLoading}
/>
)}
{schema.type === FormTypeEnum.textNumber && (

View File

@ -1090,13 +1090,13 @@ export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSe
break
}
case BlockEnum.Code: {
const targetVar = (data as CodeNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.'))
const targetVar = (data as CodeNodeType).variables?.find(v => Array.isArray(v.value_selector) && v.value_selector && v.value_selector.join('.') === valueSelector.join('.'))
if (targetVar)
res = targetVar.variable
break
}
case BlockEnum.TemplateTransform: {
const targetVar = (data as TemplateTransformNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.'))
const targetVar = (data as TemplateTransformNodeType).variables?.find(v => Array.isArray(v.value_selector) && v.value_selector && v.value_selector.join('.') === valueSelector.join('.'))
if (targetVar)
res = targetVar.variable
break

View File

@ -6,6 +6,7 @@ import {
RiArrowDownSLine,
RiCloseLine,
RiErrorWarningFill,
RiLoader4Line,
RiMoreLine,
} from '@remixicon/react'
import produce from 'immer'
@ -16,8 +17,9 @@ import VarReferencePopup from './var-reference-popup'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
import ConstantField from './constant-field'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { BlockEnum } from '@/app/components/workflow/types'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
@ -40,6 +42,8 @@ import Tooltip from '@/app/components/base/tooltip'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from './var-full-path-panel'
import { noop } from 'lodash-es'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import type { Tool } from '@/app/components/tools/types'
const TRIGGER_DEFAULT_WIDTH = 227
@ -68,6 +72,8 @@ type Props = {
minWidth?: number
popupFor?: 'assigned' | 'toAssigned'
zIndex?: number
currentTool?: Tool
currentProvider?: ToolWithProvider
}
const DEFAULT_VALUE_SELECTOR: Props['value'] = []
@ -97,6 +103,8 @@ const VarReferencePicker: FC<Props> = ({
minWidth,
popupFor,
zIndex,
currentTool,
currentProvider,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
@ -316,6 +324,42 @@ const VarReferencePicker: FC<Props> = ({
return null
}, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type])
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
const [isLoading, setIsLoading] = useState(false)
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
'tool',
)
const handleFetchDynamicOptions = async () => {
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider)
return
setIsLoading(true)
try {
const data = await fetchDynamicOptions()
setDynamicOptions(data?.options || [])
}
finally {
setIsLoading(false)
}
}
useEffect(() => {
handleFetchDynamicOptions()
}, [currentTool, currentProvider, schema])
const schemaWithDynamicSelect = useMemo(() => {
if (schema?.type !== FormTypeEnum.dynamicSelect)
return schema
// rewrite schema.options with dynamicOptions
if (dynamicOptions) {
return {
...schema,
options: dynamicOptions,
}
}
return schema
}, [dynamicOptions])
return (
<div className={cn(className, !readonly && 'cursor-pointer')}>
<PortalToFollowElem
@ -366,8 +410,9 @@ const VarReferencePicker: FC<Props> = ({
<ConstantField
value={value as string}
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
schema={schema as CredentialFormSchema}
schema={schemaWithDynamicSelect as CredentialFormSchema}
readonly={readonly}
isLoading={isLoading}
/>
)
: (
@ -412,6 +457,7 @@ const VarReferencePicker: FC<Props> = ({
)}
<div className='flex items-center text-text-accent'>
{!hasValue && <Variable02 className='h-3.5 w-3.5' />}
{isLoading && <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />}
{isEnv && <Env className='h-3.5 w-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
@ -424,7 +470,16 @@ const VarReferencePicker: FC<Props> = ({
{!isValidVar && <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />}
</>
)
: <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</div>}
: <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>
{isLoading ? (
<div className='flex items-center'>
<RiLoader4Line className='mr-1 h-3.5 w-3.5 animate-spin text-text-secondary' />
<span>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</span>
</div>
) : (
placeholder ?? t('workflow.common.setVarValuePlaceholder')
)}
</div>}
</div>
</Tooltip>
</div>

View File

@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
import type { ToolVarInputs } from '../types'
import { VarType as VarKindType } from '../types'
import cn from '@/utils/classnames'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
@ -17,6 +17,7 @@ import { VarType } from '@/app/components/workflow/types'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { noop } from 'lodash-es'
import type { Tool } from '@/app/components/tools/types'
type Props = {
readOnly: boolean
@ -27,6 +28,8 @@ type Props = {
onOpen?: (index: number) => void
isSupportConstantValue?: boolean
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
}
const InputVarList: FC<Props> = ({
@ -38,6 +41,8 @@ const InputVarList: FC<Props> = ({
onOpen = noop,
isSupportConstantValue,
filterVar,
currentTool,
currentProvider,
}) => {
const language = useLanguage()
const { t } = useTranslation()
@ -58,6 +63,8 @@ const InputVarList: FC<Props> = ({
return 'ModelSelector'
else if (type === FormTypeEnum.toolSelector)
return 'ToolSelector'
else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select)
return 'Select'
else
return 'String'
}
@ -149,6 +156,7 @@ const InputVarList: FC<Props> = ({
const handleOpen = useCallback((index: number) => {
return () => onOpen(index)
}, [onOpen])
return (
<div className='space-y-3'>
{
@ -163,7 +171,8 @@ const InputVarList: FC<Props> = ({
} = schema
const varInput = value[variable]
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
@ -198,11 +207,13 @@ const InputVarList: FC<Props> = ({
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue={isSupportConstantValue}
filterVar={isNumber ? filterVar : undefined}
availableVars={isSelect ? availableVars : undefined}
schema={schema}
currentTool={currentTool}
currentProvider={currentProvider}
/>
)}
{isFile && (

View File

@ -42,6 +42,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
isLoading,
outputSchema,
hasObjectOutput,
currTool,
} = useConfig(id, data)
if (isLoading) {
@ -80,6 +81,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
filterVar={filterVar}
isSupportConstantValue
onOpen={handleOnVarOpen}
currentProvider={currCollection}
currentTool={currTool}
/>
</Field>
)}

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import { memo, useEffect, useRef } from 'react'
import { memo, useCallback, useEffect, useRef } from 'react'
import { useNodes } from 'reactflow'
import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes'
@ -13,6 +13,51 @@ export type PanelProps = {
right?: React.ReactNode
}
}
/**
* Reference MDN standard implementationhttps://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserverEntry/borderBoxSize
*/
const getEntryWidth = (entry: ResizeObserverEntry, element: HTMLElement): number => {
if (entry.borderBoxSize?.length > 0)
return entry.borderBoxSize[0].inlineSize
if (entry.contentRect.width > 0)
return entry.contentRect.width
return element.getBoundingClientRect().width
}
const useResizeObserver = (
callback: (width: number) => void,
dependencies: React.DependencyList = [],
) => {
const elementRef = useRef<HTMLDivElement>(null)
const stableCallback = useCallback(callback, [callback])
useEffect(() => {
const element = elementRef.current
if (!element) return
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = getEntryWidth(entry, element)
stableCallback(width)
}
})
resizeObserver.observe(element)
const initialWidth = element.getBoundingClientRect().width
stableCallback(initialWidth)
return () => {
resizeObserver.disconnect()
}
}, [stableCallback, ...dependencies])
return elementRef
}
const Panel: FC<PanelProps> = ({
components,
}) => {
@ -20,44 +65,21 @@ const Panel: FC<PanelProps> = ({
const selectedNode = nodes.find(node => node.data.selected)
const showEnvPanel = useStore(s => s.showEnvPanel)
const isRestoring = useStore(s => s.isRestoring)
const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel)
const rightPanelRef = useRef<HTMLDivElement>(null)
const setRightPanelWidth = useStore(s => s.setRightPanelWidth)
// get right panel width
useEffect(() => {
if (rightPanelRef.current) {
const resizeRightPanelObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize } = entry.borderBoxSize[0]
setRightPanelWidth(inlineSize)
}
})
resizeRightPanelObserver.observe(rightPanelRef.current)
return () => {
resizeRightPanelObserver.disconnect()
}
}
}, [setRightPanelWidth])
const otherPanelRef = useRef<HTMLDivElement>(null)
const setOtherPanelWidth = useStore(s => s.setOtherPanelWidth)
// get other panel width
useEffect(() => {
if (otherPanelRef.current) {
const resizeOtherPanelObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize } = entry.borderBoxSize[0]
setOtherPanelWidth(inlineSize)
}
})
resizeOtherPanelObserver.observe(otherPanelRef.current)
return () => {
resizeOtherPanelObserver.disconnect()
}
}
}, [setOtherPanelWidth])
const rightPanelRef = useResizeObserver(
setRightPanelWidth,
[setRightPanelWidth, selectedNode, showEnvPanel, showWorkflowVersionHistoryPanel],
)
const otherPanelRef = useResizeObserver(
setOtherPanelWidth,
[setOtherPanelWidth, showEnvPanel, showWorkflowVersionHistoryPanel],
)
return (
<div
ref={rightPanelRef}
@ -65,26 +87,14 @@ const Panel: FC<PanelProps> = ({
className={cn('absolute bottom-1 right-0 top-14 z-10 flex outline-none')}
key={`${isRestoring}`}
>
{
components?.left
}
{
!!selectedNode && (
<NodePanel {...selectedNode!} />
)
}
{components?.left}
{!!selectedNode && <NodePanel {...selectedNode} />}
<div
className='relative'
className="relative"
ref={otherPanelRef}
>
{
components?.right
}
{
showEnvPanel && (
<EnvPanel />
)
}
{components?.right}
{showEnvPanel && <EnvPanel />}
</div>
</div>
)

View File

@ -29,6 +29,7 @@ import type {
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import { hasRetryNode } from '@/app/components/workflow/utils'
import { useDocLink } from '@/context/i18n'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
className?: string
@ -129,10 +130,16 @@ const NodePanel: FC<Props> = ({
/>
)}
<BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('mr-2 shrink-0', inMessage && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
<div className={cn(
'system-xs-semibold-uppercase grow truncate text-text-secondary',
hideInfo && '!text-xs',
)} title={nodeInfo.title}>{nodeInfo.title}</div>
<Tooltip
popupContent={
<div className='max-w-xs'>{nodeInfo.title}</div>
}
>
<div className={cn(
'system-xs-semibold-uppercase grow truncate text-text-secondary',
hideInfo && '!text-xs',
)}>{nodeInfo.title}</div>
</Tooltip>
{nodeInfo.status !== 'running' && !hideInfo && (
<div className='system-xs-regular shrink-0 text-text-tertiary'>{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}</div>
)}

View File

@ -12,6 +12,7 @@ import type { CommonNodeType } from '@/app/components/workflow/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
import cn from '@/utils/classnames'
import { useNodesReadOnly } from '../hooks/use-workflow'
const VariableInspectTrigger: FC = () => {
const { t } = useTranslation()
@ -32,7 +33,10 @@ const VariableInspectTrigger: FC = () => {
const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars]
return allVars
}, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
const {
nodesReadOnly,
getNodesReadOnly,
} = useNodesReadOnly()
const workflowRunningData = useStore(s => s.workflowRunningData)
const nodes = useNodes<CommonNodeType>()
const isStepRunning = useMemo(() => nodes.some(node => node.data._singleRunningStatus === NodeRunningStatus.Running), [nodes])
@ -61,8 +65,14 @@ const VariableInspectTrigger: FC = () => {
<div className={cn('flex items-center gap-1')}>
{!isRunning && !currentVars.length && (
<div
className='system-2xs-semibold-uppercase flex h-5 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-background-default-hover'
onClick={() => setShowVariableInspectPanel(true)}
className={cn('system-2xs-semibold-uppercase flex h-5 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-background-default-hover',
nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
)}
onClick={() => {
if (getNodesReadOnly())
return
setShowVariableInspectPanel(true)
}}
>
{t('workflow.debug.variableInspect.trigger.normal')}
</div>
@ -70,13 +80,21 @@ const VariableInspectTrigger: FC = () => {
{!isRunning && currentVars.length > 0 && (
<>
<div
className='system-xs-medium flex h-6 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-accent shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent'
onClick={() => setShowVariableInspectPanel(true)}
className={cn('system-xs-medium flex h-6 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-accent shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent',
nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
)}
onClick={() => {
if (getNodesReadOnly())
return
setShowVariableInspectPanel(true)
}}
>
{t('workflow.debug.variableInspect.trigger.cached')}
</div>
<div
className='system-xs-medium flex h-6 cursor-pointer items-center rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-1 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent hover:text-text-accent'
className={cn('system-xs-medium flex h-6 cursor-pointer items-center rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-1 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent hover:text-text-accent',
nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
)}
onClick={handleClearAll}
>
{t('workflow.debug.variableInspect.trigger.clear')}