Compare commits

...

31 Commits

Author SHA1 Message Date
Joel
3107ec878a chore: edit metadata batch i18n 2025-02-20 16:06:10 +08:00
Joel
7d5fcfef4c feat: document dataset i18n 2025-02-20 15:22:13 +08:00
Joel
e862ab0def feat: document and tech params 2025-02-20 14:54:02 +08:00
Joel
15f80a72b8 feat: add popup 2025-02-20 11:45:36 +08:00
Joel
1a7de23864 feat: doc metadata can show edit and cancel 2025-02-20 11:00:38 +08:00
Joel
10fccd2b3f feat: metadata panel 2025-02-19 18:38:29 +08:00
Joel
b568947e00 feat: no data 2025-02-17 17:33:44 +08:00
Joel
7dcbb75839 chore: rename 2025-02-17 17:00:05 +08:00
Joel
ffdfcdd4a4 feat: apply to all selected 2025-02-17 16:57:59 +08:00
Joel
f4604bf6d0 feat: new meta data 2025-02-17 15:12:38 +08:00
Joel
3a72b76c32 file 2025-02-17 14:30:36 +08:00
Joel
49dd77e219 feat: edit beacon 2025-02-17 14:30:23 +08:00
Joel
b9f223d9d4 feat: modal row 2025-02-17 12:53:34 +08:00
Joel
3c4da03575 feat: eidt row item 2025-02-17 11:38:12 +08:00
Joel
7692476097 feat: edit modal struct 2025-02-14 17:58:19 +08:00
Joel
428438eeca feat: i18n 2025-02-14 16:39:31 +08:00
Joel
b7c546f2ad feat: dataset metadata collection 2025-02-14 16:24:49 +08:00
Joel
0ed892a747 feat: dataset metadata 2025-02-14 15:28:18 +08:00
Joel
5e2bd407a8 chore: select i18n and other 2025-02-14 14:01:03 +08:00
Joel
a4668e0ffc fix: manage btn hover 2025-02-14 11:21:48 +08:00
Joel
1ca79ea729 feat: select metadata 2025-02-14 11:18:40 +08:00
Joel
ebb6de5f52 feat: split main conten 2025-02-13 16:48:02 +08:00
Joel
2adc704463 feat: add back button 2025-02-13 16:19:30 +08:00
Joel
b74f1b3c07 chore: i18n 2025-02-13 15:50:18 +08:00
Joel
f60e650400 feat: create metadata modal 2025-02-13 15:34:09 +08:00
zxhlyh
83d0142641 fix: refresh after install plugin (#13593) 2025-02-12 15:51:55 +08:00
Yeuoly
56c7f49625 fix: add langgenius to list tool api (#13578) 2025-02-12 15:37:10 +08:00
Yeuoly
7c1d842cfe (1.0) fix: invalid default model provider (#13572) 2025-02-12 14:21:58 +08:00
KVOJJJin
2ea3b64a45 Feat: tool setting support variable (#13465)
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
2025-02-12 12:54:10 +08:00
Joel
824f8d8994 chore: add debug doc link (#13537) 2025-02-11 18:32:01 +08:00
Joel
31c17e6378 fix: installed plugin not show upgrade (#13523) 2025-02-11 14:08:43 +08:00
73 changed files with 2159 additions and 145 deletions

View File

@@ -20,6 +20,7 @@ from core.model_runtime.model_providers.__base.text_embedding_model import TextE
from core.model_runtime.model_providers.__base.tts_model import TTSModel from core.model_runtime.model_providers.__base.tts_model import TTSModel
from core.model_runtime.schema_validators.model_credential_schema_validator import ModelCredentialSchemaValidator from core.model_runtime.schema_validators.model_credential_schema_validator import ModelCredentialSchemaValidator
from core.model_runtime.schema_validators.provider_credential_schema_validator import ProviderCredentialSchemaValidator from core.model_runtime.schema_validators.provider_credential_schema_validator import ProviderCredentialSchemaValidator
from core.plugin.entities.plugin import ModelProviderID
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
from core.plugin.manager.asset import PluginAssetManager from core.plugin.manager.asset import PluginAssetManager
from core.plugin.manager.model import PluginModelManager from core.plugin.manager.model import PluginModelManager
@@ -112,6 +113,9 @@ class ModelProviderFactory:
:param provider: provider name :param provider: provider name
:return: provider schema :return: provider schema
""" """
if "/" not in provider:
provider = str(ModelProviderID(provider))
# fetch plugin model providers # fetch plugin model providers
plugin_model_provider_entities = self.get_plugin_model_providers() plugin_model_provider_entities = self.get_plugin_model_providers()
@@ -363,4 +367,4 @@ class ModelProviderFactory:
plugin_id = "/".join(provider.split("/")[:-1]) plugin_id = "/".join(provider.split("/")[:-1])
provider_name = provider.split("/")[-1] provider_name = provider.split("/")[-1]
return plugin_id, provider_name return str(plugin_id), provider_name

View File

@@ -169,6 +169,21 @@ class GenericProviderID:
return f"{self.organization}/{self.plugin_name}" return f"{self.organization}/{self.plugin_name}"
class ModelProviderID(GenericProviderID):
def __init__(self, value: str, is_hardcoded: bool = False) -> None:
super().__init__(value, is_hardcoded)
if self.organization == "langgenius" and self.provider_name == "google":
self.provider_name = "gemini"
class ToolProviderID(GenericProviderID):
def __init__(self, value: str, is_hardcoded: bool = False) -> None:
super().__init__(value, is_hardcoded)
if self.organization == "langgenius":
if self.provider_name in ["jina", "siliconflow"]:
self.provider_name = f"{self.provider_name}_tool"
class PluginDependency(BaseModel): class PluginDependency(BaseModel):
class Type(enum.StrEnum): class Type(enum.StrEnum):
Github = PluginInstallationSource.Github.value Github = PluginInstallationSource.Github.value

View File

@@ -1,7 +1,6 @@
import datetime import datetime
import json import json
import logging import logging
import sys
import time import time
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@@ -418,8 +417,6 @@ class PluginMigration:
logger.info("Uninstall plugins") logger.info("Uninstall plugins")
sys.exit(-1)
# get installation # get installation
try: try:
installation = manager.list_plugins(fake_tenant_id) installation = manager.list_plugins(fake_tenant_id)

View File

@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from configs import dify_config from configs import dify_config
from core.helper.position_helper import is_filtered from core.helper.position_helper import is_filtered
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.plugin.entities.plugin import GenericProviderID from core.plugin.entities.plugin import GenericProviderID, ToolProviderID
from core.plugin.manager.exc import PluginDaemonClientSideError from core.plugin.manager.exc import PluginDaemonClientSideError
from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort
from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity
@@ -240,10 +240,7 @@ class BuiltinToolManageService:
# rewrite db_providers # rewrite db_providers
for db_provider in db_providers: for db_provider in db_providers:
try: db_provider.provider = str(ToolProviderID(db_provider.provider))
GenericProviderID(db_provider.provider)
except Exception:
db_provider.provider = f"langgenius/{db_provider.provider}/{db_provider.provider}"
# find provider # find provider
def find_provider(provider): def find_provider(provider):

View File

@@ -20,7 +20,9 @@ import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development' import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
// import DatasetMetadataDrawer from '@/app/components/datasets/metadata/dataset-metadata-drawer'
// import MetaDataDocument from '@/app/components/datasets/metadata/metadata-document'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
// Services // Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets' import { fetchDatasetApiBaseUrl } from '@/service/datasets'
@@ -29,6 +31,7 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store' import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context' import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { DataType } from '@/app/components/datasets/metadata/types'
const Container = () => { const Container = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -81,8 +84,54 @@ const Container = () => {
return router.replace('/apps') return router.replace('/apps')
}, [currentWorkspace, router]) }, [currentWorkspace, router])
const [isBuiltInEnabled, setIsBuiltInEnabled] = useState(false)
const [userMetadata, setUserMetadata] = useState([
{ id: '1', name: 'name1', type: DataType.string, valueLength: 1 },
{ id: '2', name: 'name2', type: DataType.number, valueLength: 2 },
{ id: '3', name: 'name3', type: DataType.time, valueLength: 3 },
])
return ( return (
<div ref={containerRef} className='grow relative flex flex-col bg-background-body overflow-y-auto scroll-container'> <div ref={containerRef} className='grow relative flex flex-col bg-background-body overflow-y-auto scroll-container'>
<div className='flex justify-end mt-[300px] mr-[100px]'>
{/* <MetaDataDocument /> */}
{/* <SelectMetadataModal trigger={<Button className='w-[200px]'>select</Button>} onSave={(data) => { console.log(data) }} />
<CreateModal trigger={<Button className='w-[200px]'>add</Button>} hasBack onSave={(data) => { console.log(data) }} />
<Button className='flex w-[200px]' size="medium" onClick={() => setShowExternalApiPanel(true)}>
Metadata
</Button> */}
{/* <DatasetMetadataDrawer
userMetadata={userMetadata}
onChange={setUserMetadata}
builtInMetadata={[
{ id: '1', name: 'name1', type: DataType.string, valueLength: 1 },
{ id: '2', name: 'name2', type: DataType.number, valueLength: 2 },
{ id: '3', name: 'name3', type: DataType.time, valueLength: 3 },
]}
isBuiltInEnabled={isBuiltInEnabled}
onIsBuiltInEnabledChange={setIsBuiltInEnabled}
onClose={() => { }}
/> */}
<EditMetadataBatchModal
documentNum={20}
list={[
{
id: '1', name: 'name1', type: DataType.string, value: 'aaa',
},
{
id: '2', name: 'name2', type: DataType.number, value: 'ccc', isMultipleValue: true, isUpdated: true,
},
{
id: '2.1', name: 'num v', type: DataType.number, value: 10,
},
{
id: '3', name: 'name3', type: DataType.time, value: '', isUpdated: true, // updateType: UpdateType.delete,
},
]}
onHide={() => { }}
onChange={(list, newList, isApplyToAllSelectDocument) => { console.log(list, newList, isApplyToAllSelectDocument) }}
/>
</div>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'> <div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
<TabSliderNew <TabSliderNew
value={activeTab} value={activeTab}

View File

@@ -53,15 +53,18 @@ export default function Drawer({
/> />
<div className={cn('relative z-50 flex flex-col justify-between bg-components-panel-bg w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}> <div className={cn('relative z-50 flex flex-col justify-between bg-components-panel-bg w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
<> <>
{title && <Dialog.Title <div className='flex justify-between'>
as="h3" {title && <Dialog.Title
className="text-lg font-medium leading-6 text-text-primary" as="h3"
> className="text-lg font-medium leading-6 text-text-primary"
{title} >
</Dialog.Title>} {title}
{showClose && <Dialog.Title className="flex items-center mb-4" as="div"> </Dialog.Title>}
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={onClose} /> {showClose && <Dialog.Title className="flex items-center mb-4" as="div">
</Dialog.Title>} <XMarkIcon className='w-4 h-4 text-text-tertiary cursor-pointer' onClick={onClose} />
</Dialog.Title>}
</div>
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>{description}</Dialog.Description>} {description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>{description}</Dialog.Description>}
{children} {children}
</> </>

View File

@@ -12,10 +12,13 @@ export type InputNumberProps = {
max?: number max?: number
min?: number min?: number
defaultValue?: number defaultValue?: number
wrapClassName?: string
controlWrapClassName?: string
controlClassName?: string
} & Omit<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'> } & Omit<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'>
export const InputNumber: FC<InputNumberProps> = (props) => { export const InputNumber: FC<InputNumberProps> = (props) => {
const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, ...rest } = props const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, wrapClassName, controlWrapClassName, controlClassName, ...rest } = props
const isValidValue = (v: number) => { const isValidValue = (v: number) => {
if (max && v > max) if (max && v > max)
@@ -46,7 +49,7 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
onChange(newValue) onChange(newValue)
} }
return <div className='flex'> return <div className={classNames('flex', wrapClassName)}>
<Input {...rest} <Input {...rest}
// disable default controller // disable default controller
type='text' type='text'
@@ -68,16 +71,18 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
}} }}
unit={unit} unit={unit}
/> />
<div className='flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs'> <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', controlWrapClassName)}>
<button onClick={inc} className={classNames( <button onClick={inc} className={classNames(
size === 'sm' ? 'pt-1' : 'pt-1.5', size === 'sm' ? 'pt-1' : 'pt-1.5',
'px-1.5 hover:bg-components-input-bg-hover', 'px-1.5 hover:bg-components-input-bg-hover',
controlClassName,
)}> )}>
<RiArrowUpSLine className='size-3' /> <RiArrowUpSLine className='size-3' />
</button> </button>
<button onClick={dec} className={classNames( <button onClick={dec} className={classNames(
size === 'sm' ? 'pb-1' : 'pb-1.5', size === 'sm' ? 'pb-1' : 'pb-1.5',
'px-1.5 hover:bg-components-input-bg-hover', 'px-1.5 hover:bg-components-input-bg-hover',
controlClassName,
)}> )}>
<RiArrowDownSLine className='size-3' /> <RiArrowDownSLine className='size-3' />
</button> </button>

View File

@@ -0,0 +1,58 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import Button from '../button'
import { RiCloseLine } from '@remixicon/react'
type Props = {
title: string
className?: string
beforeHeader?: React.ReactNode
onClose: () => void
hideCloseBtn?: boolean
onConfirm: () => void
children: React.ReactNode
}
const ModalLikeWrap: FC<Props> = ({
title,
className,
beforeHeader,
children,
onClose,
hideCloseBtn,
onConfirm,
}) => {
const { t } = useTranslation()
return (
<div className={cn('w-[320px] px-3 pt-3.5 pb-4 bg-components-panel-bg shadow-xl rounded-2xl border-[0.5px] border-components-panel-border', className)}>
{beforeHeader || null}
<div className='mb-1 flex h-6 items-center justify-between'>
<div className='system-xl-semibold text-text-primary'>{title}</div>
{!hideCloseBtn && (
<div
className='p-1.5 text-text-tertiary cursor-pointer'
onClick={onClose}
>
<RiCloseLine className='size-4' />
</div>
)}
</div>
<div className='mt-2'>{children}</div>
<div className='mt-4 flex justify-end'>
<Button
className='mr-2'
onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button
onClick={onConfirm}
variant='primary'
>{t('common.operation.save')}</Button>
</div>
</div>
)
}
export default React.memo(ModalLikeWrap)

View File

@@ -5,7 +5,7 @@ import classNames from '@/utils/classnames'
type SwitchProps = { type SwitchProps = {
onChange?: (value: boolean) => void onChange?: (value: boolean) => void
size?: 'sm' | 'md' | 'lg' | 'l' size?: 'xs' | 'sm' | 'md' | 'lg' | 'l'
defaultValue?: boolean defaultValue?: boolean
disabled?: boolean disabled?: boolean
className?: string className?: string
@@ -23,6 +23,7 @@ const Switch = React.forwardRef(
l: 'h-5 w-9', l: 'h-5 w-9',
md: 'h-4 w-7', md: 'h-4 w-7',
sm: 'h-3 w-5', sm: 'h-3 w-5',
xs: 'h-2.5 w-3.5',
} }
const circleStyle = { const circleStyle = {
@@ -30,6 +31,7 @@ const Switch = React.forwardRef(
l: 'h-4 w-4', l: 'h-4 w-4',
md: 'h-3 w-3', md: 'h-3 w-3',
sm: 'h-2 w-2', sm: 'h-2 w-2',
xs: 'h-1.5 w-1',
} }
const translateLeft = { const translateLeft = {
@@ -37,6 +39,7 @@ const Switch = React.forwardRef(
l: 'translate-x-4', l: 'translate-x-4',
md: 'translate-x-3', md: 'translate-x-3',
sm: 'translate-x-2', sm: 'translate-x-2',
xs: 'translate-x-1.5',
} }
return ( return (
<OriginalSwitch <OriginalSwitch
@@ -53,6 +56,7 @@ const Switch = React.forwardRef(
enabled ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked', enabled ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked',
'relative inline-flex flex-shrink-0 cursor-pointer rounded-[5px] border-2 border-transparent transition-colors duration-200 ease-in-out', 'relative inline-flex flex-shrink-0 cursor-pointer rounded-[5px] border-2 border-transparent transition-colors duration-200 ease-in-out',
disabled ? '!opacity-50 !cursor-not-allowed' : '', disabled ? '!opacity-50 !cursor-not-allowed' : '',
size === 'xs' && 'rounded-sm',
className, className,
)} )}
> >
@@ -61,6 +65,7 @@ const Switch = React.forwardRef(
className={classNames( className={classNames(
circleStyle[size], circleStyle[size],
enabled ? translateLeft[size] : 'translate-x-0', enabled ? translateLeft[size] : 'translate-x-0',
size === 'xs' && 'rounded-[1px]',
'pointer-events-none inline-block transform rounded-[3px] bg-components-toggle-knob shadow ring-0 transition duration-200 ease-in-out', 'pointer-events-none inline-block transform rounded-[3px] bg-components-toggle-knob shadow ring-0 transition duration-200 ease-in-out',
)} )}
/> />

View File

@@ -3,47 +3,51 @@ import type { FC } from 'react'
import React from 'react' import React from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
interface Option { type Option = {
value: string value: string
text: string | JSX.Element text: string | JSX.Element
} }
interface ItemProps { type ItemProps = {
className?: string className?: string
isActive: boolean isActive: boolean
onClick: (v: string) => void onClick: (v: string) => void
option: Option option: Option
smallItem?: boolean
} }
const Item: FC<ItemProps> = ({ const Item: FC<ItemProps> = ({
className, className,
isActive, isActive,
onClick, onClick,
option, option,
smallItem,
}) => { }) => {
return ( return (
<div <div
key={option.value} key={option.value}
className={cn( className={cn(
'relative pb-2.5 system-xl-semibold', 'relative pb-2.5 ',
!isActive && 'cursor-pointer', !isActive && 'cursor-pointer',
smallItem ? 'system-sm-semibold-uppercase' : 'system-xl-semibold',
className, className,
)} )}
onClick={() => !isActive && onClick(option.value)} onClick={() => !isActive && onClick(option.value)}
> >
<div className={cn(isActive ? 'text-text-primary' : 'text-text-tertiary')}>{option.text}</div> <div className={cn(isActive ? 'text-text-primary' : 'text-text-tertiary')}>{option.text}</div>
{isActive && ( {isActive && (
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-util-colors-blue-blue-500'></div> <div className='absolute bottom-0 left-0 right-0 h-0.5 bg-util-colors-blue-brand-blue-brand-600'></div>
)} )}
</div> </div>
) )
} }
interface Props { type Props = {
className?: string className?: string
value: string value: string
onChange: (v: string) => void onChange: (v: string) => void
options: Option[] options: Option[]
noBorderBottom?: boolean noBorderBottom?: boolean
smallItem?: boolean
itemClassName?: string itemClassName?: string
} }
@@ -54,6 +58,7 @@ const TabSlider: FC<Props> = ({
options, options,
noBorderBottom, noBorderBottom,
itemClassName, itemClassName,
smallItem,
}) => { }) => {
return ( return (
<div className={cn(className, !noBorderBottom && 'border-b border-divider-subtle', 'flex space-x-6')}> <div className={cn(className, !noBorderBottom && 'border-b border-divider-subtle', 'flex space-x-6')}>
@@ -64,6 +69,7 @@ const TabSlider: FC<Props> = ({
onClick={onChange} onClick={onChange}
key={option.value} key={option.value}
className={itemClassName} className={itemClassName}
smallItem={smallItem}
/> />
))} ))}
</div> </div>

View File

@@ -0,0 +1,31 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Button from '../../base/button'
import { RiAddLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
onClick?: () => void
}
const AddedMetadataButton: FC<Props> = ({
className,
onClick,
}) => {
const { t } = useTranslation()
return (
<Button
className={cn('w-full flex items-center', className)}
size='small'
variant='tertiary'
onClick={onClick}
>
<RiAddLine className='mr-1 size-3.5' />
<div>{t('dataset.metadata.addMetadata')}</div>
</Button>
)
}
export default React.memo(AddedMetadataButton)

View File

@@ -0,0 +1,87 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { DataType } from './types'
import ModalLikeWrap from '../../base/modal-like-wrap'
import Field from './field'
import OptionCard from '../../workflow/nodes/_base/components/option-card'
import Input from '@/app/components/base/input'
import { RiArrowLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'dataset.metadata.createMetadata'
export type Props = {
onSave: (data: any) => void
hasBack?: boolean
onBack?: () => void
}
const CreateContent: FC<Props> = ({
hasBack,
onBack,
onSave,
}) => {
const { t } = useTranslation()
const [type, setType] = useState(DataType.string)
const handleTypeChange = useCallback((newType: DataType) => {
return () => setType(newType)
}, [setType])
const [name, setName] = useState('')
const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}, [setName])
const handleSave = useCallback(() => {
onSave({
type,
name,
})
}, [onSave, type, name])
return (
<ModalLikeWrap
title={t(`${i18nPrefix}.title`)}
onClose={() => { }}
onConfirm={handleSave}
hideCloseBtn={hasBack}
beforeHeader={hasBack && (
<div className='relative left-[-4px] mb-1 flex items-center py-1 space-x-1 text-text-accent cursor-pointer' onClick={onBack}>
<RiArrowLeftLine className='size-4' />
<div className='system-xs-semibold-uppercase'>{t(`${i18nPrefix}.back`)}</div>
</div>
)}
>
<div className='space-y-3'>
<Field label={t(`${i18nPrefix}.type`)}>
<div className='grid grid-cols-3 gap-2'>
<OptionCard
title='String'
selected={type === DataType.string}
onSelect={handleTypeChange(DataType.string)}
/>
<OptionCard
title='Number'
selected={type === DataType.number}
onSelect={handleTypeChange(DataType.number)}
/>
<OptionCard
title='Time'
selected={type === DataType.time}
onSelect={handleTypeChange(DataType.time)}
/>
</div>
</Field>
<Field label={t(`${i18nPrefix}.name`)}>
<Input
value={name}
onChange={handleNameChange}
placeholder={t(`${i18nPrefix}.namePlaceholder`)}
/>
</Field>
</div>
</ModalLikeWrap>
)
}
export default React.memo(CreateContent)

View File

@@ -0,0 +1,43 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import type { Props as CreateContentProps } from './create-content'
import CreateContent from './create-content'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
type Props = {
onSave: (data: any) => void
trigger: React.ReactNode
popupLeft?: number
} & CreateContentProps
const CreateMetadataModal: FC<Props> = ({
trigger,
popupLeft = 20,
...createContentProps
}) => {
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='left-start'
offset={{
mainAxis: popupLeft,
crossAxis: -38,
}}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<CreateContent {...createContentProps} />
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(CreateMetadataModal)

View File

@@ -0,0 +1,228 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import type { MetadataItemWithValueLength } from './types'
import Drawer from '../../base/drawer'
import Button from '@/app/components/base/button'
import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { getIcon } from './utils/get-icon'
import cn from '@/utils/classnames'
import Modal from '../../base/modal'
import Field from './field'
import Input from '@/app/components/base/input'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import Switch from '../../base/switch'
import Tooltip from '../../base/tooltip'
import CreateModal from '@/app/components/datasets/metadata/create-metadata-modal'
import { useBoolean, useHover } from 'ahooks'
import Confirm from '@/app/components/base/confirm'
const i18nPrefix = 'dataset.metadata.datasetMetadata'
type Props = {
userMetadata: MetadataItemWithValueLength[]
builtInMetadata: MetadataItemWithValueLength[]
isBuiltInEnabled: boolean
onIsBuiltInEnabledChange: (value: boolean) => void
onClose: () => void
onChange: (data: MetadataItemWithValueLength[]) => void
}
type ItemProps = {
readonly?: boolean
disabled?: boolean
payload: MetadataItemWithValueLength
onRename?: () => void
onDelete?: () => void
}
const Item: FC<ItemProps> = ({
readonly,
disabled,
payload,
onRename,
onDelete,
}) => {
const { t } = useTranslation()
const Icon = getIcon(payload.type)
const handleRename = useCallback(() => {
onRename?.()
}, [onRename])
const deleteBtnRef = useRef<HTMLDivElement>(null)
const isDeleteHovering = useHover(deleteBtnRef)
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const handleDelete = useCallback(() => {
hideDeleteConfirm()
onDelete?.()
}, [hideDeleteConfirm, onDelete])
return (
<div
key={payload.id}
className={cn(
!readonly && !disabled && 'group/item hover:shadow-xs cursor-pointer',
'border border-components-panel-border-subtle rounded-md bg-components-panel-on-panel-item-bg',
isDeleteHovering && 'border border-state-destructive-border bg-state-destructive-hover',
)}
>
<div
className={cn(
'flex items-center h-8 px-2 justify-between',
disabled && 'opacity-30', // not include border and bg
)}
>
<div className='flex items-center h-full text-text-tertiary space-x-1'>
<Icon className='shrink-0 size-4' />
<div className='max-w-[250px] truncate system-sm-medium text-text-primary'>{payload.name}</div>
<div className='shrink-0 system-xs-regular'>{payload.type}</div>
</div>
<div className='group-hover/item:hidden ml-2 shrink-0 system-xs-regular text-text-tertiary'>
{disabled ? t(`${i18nPrefix}.disabled`) : t(`${i18nPrefix}.values`, { num: payload.valueLength || 0 })}
</div>
<div className='group-hover/item:flex hidden ml-2 items-center text-text-tertiary space-x-1'>
<RiEditLine className='size-4 cursor-pointer' onClick={handleRename} />
<div ref={deleteBtnRef} className='hover:text-text-destructive'>
<RiDeleteBinLine className='size-4 cursor-pointer' onClick={showDeleteConfirm} />
</div>
</div>
{isShowDeleteConfirm && (
<Confirm
isShow
type='warning'
title={'Confirm to delete'}
content={`Are you sure you want to delete the metadata "${payload.name}"?`}
onConfirm={handleDelete}
onCancel={hideDeleteConfirm}
/>
)}
</div>
</div>
)
}
const DatasetMetadataDrawer: FC<Props> = ({
userMetadata,
builtInMetadata,
isBuiltInEnabled,
onIsBuiltInEnabledChange,
onClose,
onChange,
}) => {
const { t } = useTranslation()
const [isShowRenameModal, setIsShowRenameModal] = useState(false)
const [currPayload, setCurrPayload] = useState<MetadataItemWithValueLength | null>(null)
const [templeName, setTempleName] = useState('')
const handleRename = useCallback((payload: MetadataItemWithValueLength) => {
return () => {
setCurrPayload(payload)
setTempleName(payload.name)
setIsShowRenameModal(true)
}
}, [setCurrPayload, setIsShowRenameModal])
const handleAdd = useCallback((data: MetadataItemWithValueLength) => {
const nextUserMetadata = produce(userMetadata, (draft) => {
draft.push(data)
})
onChange(nextUserMetadata)
}, [userMetadata, onChange])
const handleRenamed = useCallback(() => {
const nextUserMetadata = produce(userMetadata, (draft) => {
const index = draft.findIndex(p => p.id === currPayload?.id)
if (index !== -1)
draft[index].name = templeName!
})
onChange(nextUserMetadata)
setIsShowRenameModal(false)
}, [currPayload, templeName, userMetadata, onChange])
const handleDelete = useCallback((payload: MetadataItemWithValueLength) => {
return () => {
const nextUserMetadata = userMetadata.filter(p => p.id !== payload.id)
onChange(nextUserMetadata)
}
}, [userMetadata, onChange])
return (
<Drawer
isOpen={true}
onClose={onClose}
showClose
title={t('dataset.metadata.metadata')}
footer={null}
panelClassname='px-4 block !max-w-[420px] my-2 rounded-l-2xl'
>
<div className='system-sm-regular text-text-tertiary'>{t(`${i18nPrefix}.description`)}</div>
<CreateModal trigger={<Button variant='primary' className='mt-3'>
<RiAddLine className='mr-1' />
{t(`${i18nPrefix}.addMetaData`)}
</Button>} hasBack onSave={handleAdd} />
<div className='mt-3 space-y-1'>
{userMetadata.map(payload => (
<Item
key={payload.id}
payload={payload}
onRename={handleRename(payload)}
onDelete={handleDelete(payload)}
/>
))}
</div>
<div className='mt-3 flex h-6 items-center'>
<Switch
defaultValue={isBuiltInEnabled}
onChange={onIsBuiltInEnabledChange}
/>
<div className='ml-2 mr-0.5 system-sm-semibold text-text-secondary'>{t(`${i18nPrefix}.builtIn`)}</div>
<Tooltip popupContent={<div className='max-w-[100px]'>{t(`${i18nPrefix}.builtInDescription`)}</div>} />
</div>
<div className='mt-1 space-y-1'>
{builtInMetadata.map(payload => (
<Item
key={payload.id}
readonly
disabled={!isBuiltInEnabled}
payload={payload}
/>
))}
</div>
{isShowRenameModal && (
<Modal isShow title={t(`${i18nPrefix}.rename`)} onClose={() => setIsShowRenameModal(false)}>
<Field label={t(`${i18nPrefix}.name`)}>
<Input
value={templeName}
onChange={e => setTempleName(e.target.value)}
placeholder={t(`${i18nPrefix}.namePlaceholder`)}
/>
</Field>
<div className='mt-4 flex justify-end'>
<Button
className='mr-2'
onClick={() => {
setIsShowRenameModal(false)
setTempleName(currPayload!.name)
}}>{t('common.operation.cancel')}</Button>
<Button
onClick={handleRenamed}
variant='primary'
disabled={!templeName}
>{t('common.operation.save')}</Button>
</div>
</Modal>
)}
</Drawer>
)
}
export default React.memo(DatasetMetadataDrawer)

View File

@@ -0,0 +1,45 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { MetadataItemWithEdit } from '../types'
import cn from '@/utils/classnames'
import Label from './label'
import InputCombined from './input-combined'
import { RiIndeterminateCircleLine } from '@remixicon/react'
type Props = {
className?: string
payload: MetadataItemWithEdit
onChange: (value: any) => void
onRemove: () => void
}
const AddRow: FC<Props> = ({
className,
payload,
onChange,
onRemove,
}) => {
return (
<div className={cn('flex h-6 items-center space-x-0.5', className)}>
<Label text={payload.name} />
<InputCombined
type={payload.type}
value={payload.value}
onChange={onChange}
/>
<div
className={
cn(
'p-1 rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive cursor-pointer',
)
}
onClick={onRemove}
>
<RiIndeterminateCircleLine className='size-4' />
</div>
</div>
)
}
export default React.memo(AddRow)

View File

@@ -0,0 +1,50 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { type MetadataItemWithEdit, UpdateType } from '../types'
import Label from './label'
import { RiDeleteBinLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import InputHasSetMultipleValue from './input-has-set-multiple-value'
import InputCombined from './input-combined'
import EditedBeacon from './edited-beacon'
type Props = {
payload: MetadataItemWithEdit
onChange: (payload: MetadataItemWithEdit) => void
onRemove: (id: string) => void
}
const EditMetadatabatchItem: FC<Props> = ({
payload,
onChange,
onRemove,
}) => {
const isUpdated = payload.isUpdated
const isDeleted = payload.updateType === UpdateType.delete
return (
<div className='flex h-6 items-center space-x-0.5'>
{isUpdated ? <EditedBeacon onReset={() => { }} /> : <div className='shrink-0 size-4' />}
<Label text={payload.name} isDeleted={isDeleted} />
{payload.isMultipleValue
? <InputHasSetMultipleValue onClear={() => onChange({ ...payload, isMultipleValue: false })} />
: <InputCombined
type={payload.type}
value={payload.value}
onChange={v => onChange({ ...payload, value: v as string })
} />}
<div
className={
cn(
'p-1 rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive cursor-pointer',
isDeleted && 'cursor-default bg-state-destructive-hover text-text-destructive')
}
onClick={() => onRemove(payload.id)}
>
<RiDeleteBinLine className='size-4' />
</div>
</div>
)
}
export default React.memo(EditMetadatabatchItem)

View File

@@ -0,0 +1,36 @@
'use client'
import type { FC } from 'react'
import React, { useRef } from 'react'
import { useHover } from 'ahooks'
import { RiResetLeftLine } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import { useTranslation } from 'react-i18next'
type Props = {
onReset: () => void
}
const EditedBeacon: FC<Props> = ({
onReset,
}) => {
const { t } = useTranslation()
const ref = useRef(null)
const isHovering = useHover(ref)
return (
<div ref={ref} className='size-4 cursor-pointer'>
{isHovering ? (
<Tooltip popupContent={t('common.operation.reset')}>
<div className='flex justify-center items-center size-4 bg-text-accent-secondary rounded-full' onClick={onReset}>
<RiResetLeftLine className='size-[10px] text-text-primary-on-surface' />
</div>
</Tooltip>
) : (
<div className='flex items-center justify-center size-4'>
<div className='size-1 rounded-full bg-text-accent-secondary'></div>
</div>
)}
</div>
)
}
export default React.memo(EditedBeacon)

View File

@@ -0,0 +1,51 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { DataType } from '../types'
import Input from '@/app/components/base/input'
import { InputNumber } from '@/app/components/base/input-number'
import cn from '@/utils/classnames'
type Props = {
className?: string
type: DataType
value: any
onChange: (value: any) => void
}
const InputCombined: FC<Props> = ({
className: configClassName,
type,
value,
onChange,
}) => {
// TODO: configClassName...
const className = cn('grow p-0.5 h-6 text-xs')
if (type === DataType.time)
return <div className='grow text-xs'>Datepicker placeholder</div>
if (type === DataType.number) {
return (
<div className='grow text-[0]'>
<InputNumber
className={cn(className, 'rounded-l-md')}
value={value}
onChange={onChange}
size='sm'
controlWrapClassName='overflow-hidden'
controlClassName='pt-0 pb-0'
/>
</div>
)
}
return (
<Input
wrapperClassName={configClassName}
className={cn(className, 'rounded-md')}
value={value}
onChange={e => onChange(e.target.value)}
>
</Input>
)
}
export default React.memo(InputCombined)

View File

@@ -0,0 +1,29 @@
'use client'
import { RiCloseLine } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
onClear: () => void
}
const InputHasSetMultipleValue: FC<Props> = ({
onClear,
}) => {
const { t } = useTranslation()
return (
<div className='grow h-6 p-0.5 rounded-md bg-components-input-bg-normal text-[0]'>
<div className='inline-flex rounded-[5px] items-center h-5 pl-1.5 pr-0.5 bg-components-badge-white-to-dark border-[0.5px] border-components-panel-border shadow-xs space-x-0.5'>
<div className='system-xs-regular text-text-secondary'>{t('dataset.metadata.batchEditMetadata.multipleValue')}</div>
<div className='p-0.5rounded-sm text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer'>
<RiCloseLine
className='size-3.5 '
onClick={onClear}
/>
</div>
</div>
</div>
)
}
export default React.memo(InputHasSetMultipleValue)

View File

@@ -0,0 +1,27 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
isDeleted?: boolean,
className?: string,
text: string
}
const Label: FC<Props> = ({
isDeleted,
className,
text,
}) => {
return (
<div className={cn(
'shrink-0 w-[136px] system-xs-medium text-text-tertiary truncate',
isDeleted && 'line-through text-text-quaternary',
className,
)}>
{text}
</div>
)
}
export default React.memo(Label)

View File

@@ -0,0 +1,139 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import Modal from '../../../base/modal'
import { DataType, type MetadataItemWithEdit } from '../types'
import EditMetadataBatchItem from './edit-row'
import AddedMetadataItem from './add-row'
import Button from '../../../base/button'
import { useTranslation } from 'react-i18next'
import Checkbox from '../../../base/checkbox'
import Tooltip from '../../../base/tooltip'
import SelectMetadataModal from '../select-metadata-modal'
import { RiQuestionLine } from '@remixicon/react'
import Divider from '@/app/components/base/divider'
import AddMetadataButton from '../add-metadata-button'
const i18nPrefix = 'dataset.metadata.batchEditMetadata'
type Props = {
documentNum: number
list: MetadataItemWithEdit[]
onChange: (list: MetadataItemWithEdit[], addedList: MetadataItemWithEdit[], isApplyToAllSelectDocument: boolean) => void
onHide: () => void
}
const EditMetadataBatchModal: FC<Props> = ({
documentNum,
list,
onChange,
onHide,
}) => {
const { t } = useTranslation()
const [templeList, setTempleList] = useState<MetadataItemWithEdit[]>(list)
const handleTemplesChange = useCallback((payload: MetadataItemWithEdit) => {
const newTempleList = templeList.map(i => i.id === payload.id ? payload : i)
setTempleList(newTempleList)
}, [templeList])
const handleTempleItemRemove = useCallback((id: string) => {
const newTempleList = templeList.filter(i => i.id !== id)
setTempleList(newTempleList)
}, [templeList])
const testAddedList: MetadataItemWithEdit[] = [
{
id: '1', name: 'name1', type: DataType.string, value: 'aaa',
},
{
id: '2.1', name: 'num v', type: DataType.number, value: 10,
},
]
const [addedList, setAddedList] = useState<MetadataItemWithEdit[]>(testAddedList)
const handleAddedListChange = useCallback((payload: MetadataItemWithEdit) => {
const newAddedList = addedList.map(i => i.id === payload.id ? payload : i)
setAddedList(newAddedList)
}, [addedList])
const handleAddedItemRemove = useCallback((removeIndex: number) => {
return () => {
const newAddedList = addedList.filter((i, index) => index !== removeIndex)
setAddedList(newAddedList)
}
}, [addedList])
const [isApplyToAllSelectDocument, setIsApplyToAllSelectDocument] = useState(false)
const handleSave = useCallback(() => {
onChange(templeList, addedList, isApplyToAllSelectDocument)
}, [templeList, addedList, isApplyToAllSelectDocument, onChange])
return (
<Modal
title={t(`${i18nPrefix}.editMetadata`)}
isShow
closable
onClose={onHide}
className='!max-w-[640px]'
>
<div className='system-xs-medium text-text-accent'>{t(`${i18nPrefix}.editDocumentsNum`, { num: documentNum })}</div>
{/* TODO handle list scroll. There is two list. */}
<div className='mt-4 space-y-2'>
{templeList.map(item => (
<EditMetadataBatchItem
key={item.id}
payload={item}
onChange={handleTemplesChange}
onRemove={handleTempleItemRemove}
/>
))}
</div>
<div className='mt-4 pl-[18px]'>
<div className='flex items-center'>
<div className='mr-2 shrink-0 system-xs-medium-uppercase text-text-tertiary'>{t('dataset.metadata.createMetadata.title')}</div>
<Divider bgStyle='gradient' />
</div>
<div className='mt-2 space-y-2'>
{addedList.map((item, i) => (
<AddedMetadataItem
key={i}
payload={item}
onChange={handleAddedListChange}
onRemove={handleAddedItemRemove(i)}
/>
))}
</div>
<div className='mt-3'>
<SelectMetadataModal
popupPlacement='top-start'
popupOffset={{ mainAxis: 4, crossAxis: 0 }}
trigger={
<AddMetadataButton />
}
onSave={data => setAddedList([...addedList, data])}
/>
</div>
</div>
<div className='mt-4 flex items-center justify-between'>
<div className='flex items-center select-none'>
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} />
<div className='ml-2 mr-1 system-xs-medium text-text-secondary'>{t(`${i18nPrefix}.applyToAllSelectDocument`)}</div>
<Tooltip popupContent={
<div className='max-w-[240px]'>{t(`${i18nPrefix}.applyToAllSelectDocumentTip`)}</div>
} >
<div className='p-px cursor-pointer'>
<RiQuestionLine className='size-3.5 text-text-tertiary' />
</div>
</Tooltip>
</div>
<div className='flex items-center space-x-2'>
<Button
onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button
onClick={handleSave}
variant='primary'
>{t('common.operation.save')}</Button>
</div>
</div>
</Modal>
)
}
export default React.memo(EditMetadataBatchModal)

View File

@@ -0,0 +1,23 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
className?: string
label: string
children: React.ReactNode
}
const Field: FC<Props> = ({
className,
label,
children,
}) => {
return (
<div className={className}>
<div className='py-1 system-sm-semibold text-text-secondary'>{label}</div>
<div className='mt-1'>{children}</div>
</div>
)
}
export default React.memo(Field)

View File

@@ -0,0 +1,26 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
label: string
children: React.ReactNode
}
const Field: FC<Props> = ({
label,
children,
}) => {
return (
<div className='flex items-start space-x-2'>
<div className='shrink-0 w-[128px] truncate py-1 items-center text-text-tertiary system-xs-medium'>
{label}
</div>
<div className='shrink-0 w-[244px]'>
{children}
</div>
</div>
)
}
export default React.memo(Field)

View File

@@ -0,0 +1,127 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { DataType, type MetadataItemWithValue } from '../types'
import InfoGroup from './info-group'
import NoData from './no-data'
import Button from '@/app/components/base/button'
import { RiEditLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
const i18nPrefix = 'dataset.metadata.documentMetadata'
const MetadataDocument: FC = () => {
const { t } = useTranslation()
const [isEdit, setIsEdit] = useState(true)
const [list, setList] = useState<MetadataItemWithValue[]>([
{
id: '1',
name: 'Doc type',
value: 'PDF',
type: DataType.string,
},
{
id: '2',
name: 'Title',
value: 'PDF',
type: DataType.string,
},
])
const [tempList, setTempList] = useState<MetadataItemWithValue[]>(list)
const builtInEnabled = true
const builtList = [
{
id: '1',
name: 'OriginalfileNmae',
value: 'Steve Jobs The Man Who Thought Different.pdf',
type: DataType.string,
},
{
id: '2',
name: 'Title',
value: 'PDF',
type: DataType.string,
},
]
const hasData = list.length > 0
const documentInfoList = builtList
const technicalParams = builtList
return (
<div className='w-[388px] space-y-4'>
{!hasData ? (
<div className='pl-2'>
<InfoGroup
title={t('dataset.metadata.metadata')}
uppercaseTitle={false}
titleTooltip={t(`${i18nPrefix}.metadataToolTip`)}
list={isEdit ? tempList : list}
headerRight={isEdit ? (
<div className='flex space-x-1'>
<Button variant='ghost' size='small' onClick={() => {
setTempList(list)
setIsEdit(false)
}}>
<div>{t('common.operation.cancel')}</div>
</Button>
<Button variant='primary' size='small' onClick={() => {
setIsEdit(false)
setList(tempList)
}}>
<div>{t('common.operation.save')}</div>
</Button>
</div>
) : (
<Button variant='ghost' size='small' onClick={() => {
setTempList(list)
setIsEdit(true)
}}>
<RiEditLine className='mr-1 size-3.5 text-text-tertiary cursor-pointer' />
<div>{t('common.operation.edit')}</div>
</Button>
)}
isEdit={isEdit}
contentClassName='mt-5'
onChange={(item) => {
const newList = tempList.map(i => (i.name === item.name ? item : i))
setList(newList)
}}
onDelete={(item) => {
const newList = tempList.filter(i => i.name !== item.name)
setList(newList)
}}
onAdd={() => {
}}
/>
{builtInEnabled && (
<>
<Divider className='my-3' bgStyle='gradient' />
<InfoGroup
noHeader
titleTooltip='Built-in metadata is system-generated metadata that is automatically added to the document. You can enable or disable built-in metadata here.'
list={builtList}
/>
</>
)}
</div>
) : (
<NoData onStart={() => { }} />
)}
<InfoGroup
className='pl-2'
title={t(`${i18nPrefix}.documentInformation`)}
list={documentInfoList}
/>
<InfoGroup
className='pl-2'
title={t(`${i18nPrefix}.technicalParameters`)}
list={technicalParams}
/>
</div>
)
}
export default React.memo(MetadataDocument)

View File

@@ -0,0 +1,94 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { MetadataItemWithValue } from '../types'
import Field from './field'
import InputCombined from '../edit-metadata-batch/input-combined'
import { RiDeleteBinLine, RiQuestionLine } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import Divider from '@/app/components/base/divider'
import SelectMetadataModal from '../select-metadata-modal'
import AddMetadataButton from '../add-metadata-button'
type Props = {
className?: string
noHeader?: boolean
title?: string
uppercaseTitle?: boolean
titleTooltip?: string
headerRight?: React.ReactNode
contentClassName?: string
list: MetadataItemWithValue[]
isEdit?: boolean
onChange?: (item: MetadataItemWithValue) => void
onDelete?: (item: MetadataItemWithValue) => void
onAdd?: (item: MetadataItemWithValue) => void
}
const InfoGroup: FC<Props> = ({
className,
noHeader,
title,
uppercaseTitle = true,
titleTooltip,
headerRight,
contentClassName,
list,
isEdit,
onChange,
onDelete,
onAdd,
}) => {
return (
<div className={cn('bg-white', className)}>
{!noHeader && (
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-1'>
<div className={cn('text-text-secondary', uppercaseTitle ? 'system-xs-semibold-uppercase' : 'system-md-semibold')}>{title}</div>
{titleTooltip && (
<Tooltip popupContent={<div className='max-w-[240px]'>{titleTooltip}</div>}>
<RiQuestionLine className='size-3.5 text-text-tertiary' />
</Tooltip>
)}
</div>
{headerRight}
{/* <div className='flex px-1.5 rounded-md hover:bg-components-button-tertiary-bg-hover items-center h-6 space-x-1 cursor-pointer' onClick={() => setIsEdit(true)}>
</div> */}
</div>
)}
<div className={cn('mt-3 space-y-1', !noHeader && 'mt-0', contentClassName)}>
{isEdit && (
<div>
<SelectMetadataModal
trigger={
<AddMetadataButton />
}
onSave={() => { }}
/>
<Divider className='my-3 ' bgStyle='gradient' />
</div>
)}
{list.map((item, i) => (
<Field key={item.id || `${i}`} label={item.name}>
{isEdit ? (
<div className='flex items-center space-x-0.5'>
<InputCombined
className='h-6'
type={item.type}
value={item.value}
onChange={value => onChange?.({ ...item, value })}
/>
<div className='shrink-0 p-1 rounded-md text-text-tertiary hover:text-text-destructive hover:bg-state-destructive-hover cursor-pointer'>
<RiDeleteBinLine className='size-4' />
</div>
</div>
) : (<div className='py-1 system-xs-regular text-text-secondary'>{item.value}</div>)}
</Field>
))}
</div>
</div>
)
}
export default React.memo(InfoGroup)

View File

@@ -0,0 +1,27 @@
'use client'
import Button from '@/app/components/base/button'
import { RiArrowRightLine } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
onStart: () => void
}
const NoData: FC<Props> = ({
onStart,
}) => {
const { t } = useTranslation()
return (
<div className='p-4 pt-3 rounded-xl bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2'>
<div className='text-text-secondary text-xs font-semibold leading-5'>{t('dataset.metadata.metadata')}</div>
<div className='mt-1 system-xs-regular text-text-tertiary'>{t('dataset.metadata.documentMetadata.metadataToolTip')}</div>
<Button variant='primary' className='mt-2' onClick={onStart}>
<div>{t('dataset.metadata.documentMetadata.startLabeling')}</div>
<RiArrowRightLine className='ml-1 size-4' />
</Button>
</div>
)
}
export default React.memo(NoData)

View File

@@ -0,0 +1,76 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import type { Props as CreateContentProps } from './create-content'
import CreateContent from './create-content'
import SelectMetadata from './select-metadata'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
import type { MetadataItem } from './types'
import type { Placement } from '@floating-ui/react'
import { DataType } from './types'
type Props = {
popupPlacement?: Placement
popupOffset?: { mainAxis: number, crossAxis: number }
onSave: (data: any) => void
trigger: React.ReactNode
} & CreateContentProps
enum Step {
select = 'select',
create = 'create',
}
const testMetadataList: MetadataItem[] = [
{ id: '1', name: 'name1', type: DataType.string },
{ id: '2', name: 'name2', type: DataType.number },
{ id: '3', name: 'name3', type: DataType.time },
]
const SelectMetadataModal: FC<Props> = ({
popupPlacement = 'left-start',
popupOffset = { mainAxis: -38, crossAxis: 4 },
trigger,
onSave,
}) => {
const [open, setOpen] = useState(false)
const [step, setStep] = useState(Step.select)
const handleSave = useCallback((data: MetadataItem) => {
onSave(data)
setOpen(false)
}, [onSave])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={popupPlacement}
offset={popupOffset}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
className='block'
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
{step === Step.select ? (
<SelectMetadata
onSelect={handleSave}
list={testMetadataList}
onNew={() => setStep(Step.create)}
onManage={() => { }}
/>
) : (
<CreateContent
onSave={handleSave}
hasBack
onBack={() => setStep(Step.select)}
/>
)}
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(SelectMetadataModal)

View File

@@ -0,0 +1,72 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import type { MetadataItem } from './types'
import SearchInput from '../../base/search-input'
import { RiAddLine, RiArrowRightUpLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { getIcon } from './utils/get-icon'
const i18nPrefix = 'dataset.metadata.selectMetadata'
type Props = {
list: MetadataItem[]
onSelect: (data: any) => void
onNew: () => void
onManage: () => void
}
const SelectMetadata: FC<Props> = ({
list,
onSelect,
onNew,
onManage,
}) => {
const { t } = useTranslation()
const [query, setQuery] = useState('')
return (
<div className='w-[320px] pt-2 pb-0 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg backdrop-blur-[5px]'>
<SearchInput
className='mx-2'
value={query}
onChange={setQuery}
placeholder={t(`${i18nPrefix}.search`)}
/>
<div className='mt-2'>
{list.map((item) => {
const Icon = getIcon(item.type)
return (
<div
key={item.id}
className='mx-1 flex items-center h-6 px-3 justify-between rounded-md hover:bg-state-base-hover cursor-pointer'
onClick={() => onSelect(item)}
>
<div className='w-0 grow flex items-center h-full text-text-secondary'>
<Icon className='shrink-0 mr-[5px] size-3.5' />
<div className='w-0 grow truncate system-sm-medium'>{item.name}</div>
</div>
<div className='ml-1 shrink-0 system-xs-regular text-text-tertiary'>
{item.type}
</div>
</div>
)
})}
</div>
<div className='mt-1 flex justify-between p-1 border-t border-divider-subtle'>
<div className='flex items-center h-6 px-3 text-text-secondary rounded-md hover:bg-state-base-hover cursor-pointer space-x-1' onClick={onNew}>
<RiAddLine className='size-3.5' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.newAction`)}</div>
</div>
<div className='flex items-center h-6 text-text-secondary '>
<div className='mr-[3px] w-px h-3 bg-divider-regular'></div>
<div className='flex h-full items-center px-1.5 hover:bg-state-base-hover rounded-md cursor-pointer' onClick={onManage}>
<div className='mr-1 system-sm-medium'>{t(`${i18nPrefix}.manageAction`)}</div>
<RiArrowRightUpLine className='size-3.5' />
</div>
</div>
</div>
</div>
)
}
export default React.memo(SelectMetadata)

View File

@@ -0,0 +1,28 @@
export enum DataType {
string = 'string',
number = 'number',
time = 'time',
}
export type MetadataItem = {
id: string
type: DataType
name: string
}
export type MetadataItemWithValue = MetadataItem & {
value: string | number
}
export type MetadataItemWithValueLength = MetadataItem & {
valueLength: number
}
export enum UpdateType {
changeValue = 'changeValue',
delete = 'delete',
}
export type MetadataItemWithEdit = MetadataItemWithValue & {
isMultipleValue?: boolean
isUpdated?: boolean
updateType?: UpdateType
}

View File

@@ -0,0 +1,10 @@
import { DataType } from '../types'
import { RiHashtag, RiTextSnippet, RiTimeLine } from '@remixicon/react'
export const getIcon = (type: DataType) => {
return ({
[DataType.string]: RiTextSnippet,
[DataType.number]: RiHashtag,
[DataType.time]: RiTimeLine,
}[type] || RiTextSnippet)
}

View File

@@ -22,6 +22,10 @@ import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-sele
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector' import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import RadioE from '@/app/components/base/radio/ui' import RadioE from '@/app/components/base/radio/ui'
import type {
NodeOutPutVar,
} from '@/app/components/workflow/types'
import type { Node } from 'reactflow'
type FormProps< type FormProps<
CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never, CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
@@ -47,6 +51,9 @@ type FormProps<
) => ReactNode ) => ReactNode
// If return falsy value, this field will fallback to default render // If return falsy value, this field will fallback to default render
override?: [Array<FormTypeEnum>, (formSchema: CredentialFormSchema, props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>) => ReactNode] override?: [Array<FormTypeEnum>, (formSchema: CredentialFormSchema, props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>) => ReactNode]
nodeId?: string
nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[],
} }
function Form< function Form<
@@ -69,6 +76,9 @@ function Form<
fieldMoreInfo, fieldMoreInfo,
customRenderField, customRenderField,
override, override,
nodeId,
nodeOutputVars,
availableNodes,
}: FormProps<CustomFormSchema>) { }: FormProps<CustomFormSchema>) {
const language = useLanguage() const language = useLanguage()
const [changeKey, setChangeKey] = useState('') const [changeKey, setChangeKey] = useState('')
@@ -326,6 +336,9 @@ function Form<
</div> </div>
<ToolSelector <ToolSelector
scope={scope} scope={scope}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
disabled={readonly} disabled={readonly}
value={value[variable]} value={value[variable]}
// selectedTools={value[variable] ? [value[variable]] : []} // selectedTools={value[variable] ? [value[variable]] : []}
@@ -351,6 +364,9 @@ function Form<
<div key={variable} className={cn(itemClassName, 'py-3')}> <div key={variable} className={cn(itemClassName, 'py-3')}>
<MultipleToolSelector <MultipleToolSelector
disabled={readonly} disabled={readonly}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
scope={scope} scope={scope}
label={label[language] || label.en_US} label={label[language] || label.en_US}
required={required} required={required}

View File

@@ -1,4 +1,5 @@
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools' import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
@@ -8,7 +9,9 @@ import { PluginType } from '../../types'
const useRefreshPluginList = () => { const useRefreshPluginList = () => {
const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const updateModelProviders = useUpdateModelProviders() const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank)
const { refreshModelProviders } = useProviderContext() const { refreshModelProviders } = useProviderContext()
const invalidateAllToolProviders = useInvalidateAllToolProviders() const invalidateAllToolProviders = useInvalidateAllToolProviders()
@@ -31,8 +34,10 @@ const useRefreshPluginList = () => {
// model select // model select
if (PluginType.model.includes(manifest.category) || refreshAllType) { if (PluginType.model.includes(manifest.category) || refreshAllType) {
updateModelProviders()
refreshModelProviders() refreshModelProviders()
refetchLLMModelList()
refetchEmbeddingModelList()
refetchRerankModelList()
} }
// agent select // agent select

View File

@@ -87,10 +87,13 @@ const InstallByDSLList: FC<Props> = ({
const failedIndex: number[] = [] const failedIndex: number[] = []
const nextPlugins = produce(pluginsRef.current, (draft) => { const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => { marketPlaceInDSLIndex.forEach((index, i) => {
if (payloads[i]) if (payloads[i]) {
draft[index] = payloads[i] draft[index] = {
else ...payloads[i],
failedIndex.push(index) version: payloads[i].version || payloads[i].latest_version,
}
}
else { failedIndex.push(index) }
}) })
}) })
setPlugins(nextPlugins) setPlugins(nextPlugins)
@@ -192,8 +195,8 @@ const InstallByDSLList: FC<Props> = ({
key={index} key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)} onCheckedChange={handleSelect(index)}
payload={plugins[index] as Plugin} payload={plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version!} version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)} versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/> />
) )

View File

@@ -136,9 +136,10 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.uploadFailed })) setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.uploadFailed }))
}, []) }, [])
const handleInstalled = useCallback(() => { const handleInstalled = useCallback((notRefresh?: boolean) => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed })) setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
refreshPluginList(manifest) if (!notRefresh)
refreshPluginList(manifest)
setIsInstalling(false) setIsInstalling(false)
onSuccess() onSuccess()
}, [manifest, onSuccess, refreshPluginList, setIsInstalling]) }, [manifest, onSuccess, refreshPluginList, setIsInstalling])

View File

@@ -24,7 +24,7 @@ type LoadedProps = {
selectedPackage: string selectedPackage: string
onBack: () => void onBack: () => void
onStartToInstall?: () => void onStartToInstall?: () => void
onInstalled: () => void onInstalled: (notRefresh?: boolean) => void
onFailed: (message?: string) => void onFailed: (message?: string) => void
} }
@@ -55,7 +55,7 @@ const Loaded: React.FC<LoadedProps> = ({
const [isInstalling, setIsInstalling] = React.useState(false) const [isInstalling, setIsInstalling] = React.useState(false)
const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub() const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub()
const { handleRefetch } = usePluginTaskList() const { handleRefetch } = usePluginTaskList(payload.category)
const { check } = checkTaskStatus() const { check } = checkTaskStatus()
useEffect(() => { useEffect(() => {
@@ -127,7 +127,7 @@ const Loaded: React.FC<LoadedProps> = ({
onFailed(error) onFailed(error)
return return
} }
onInstalled() onInstalled(true)
} }
catch (e) { catch (e) {
if (typeof e === 'string') { if (typeof e === 'string') {

View File

@@ -32,9 +32,10 @@ const ReadyToInstall: FC<Props> = ({
}) => { }) => {
const { refreshPluginList } = useRefreshPluginList() const { refreshPluginList } = useRefreshPluginList()
const handleInstalled = useCallback(() => { const handleInstalled = useCallback((notRefresh?: boolean) => {
onStepChange(InstallStep.installed) onStepChange(InstallStep.installed)
refreshPluginList(manifest) if (!notRefresh)
refreshPluginList(manifest)
setIsInstalling(false) setIsInstalling(false)
}, [manifest, onStepChange, refreshPluginList, setIsInstalling]) }, [manifest, onStepChange, refreshPluginList, setIsInstalling])

View File

@@ -20,7 +20,7 @@ type Props = {
payload: PluginDeclaration payload: PluginDeclaration
onCancel: () => void onCancel: () => void
onStartToInstall?: () => void onStartToInstall?: () => void
onInstalled: () => void onInstalled: (notRefresh?: boolean) => void
onFailed: (message?: string) => void onFailed: (message?: string) => void
} }
@@ -62,7 +62,7 @@ const Installed: FC<Props> = ({
onCancel() onCancel()
} }
const { handleRefetch } = usePluginTaskList() const { handleRefetch } = usePluginTaskList(payload.category)
const handleInstall = async () => { const handleInstall = async () => {
if (isInstalling) return if (isInstalling) return
setIsInstalling(true) setIsInstalling(true)
@@ -92,7 +92,7 @@ const Installed: FC<Props> = ({
onFailed(error) onFailed(error)
return return
} }
onInstalled() onInstalled(true)
} }
catch (e) { catch (e) {
if (typeof e === 'string') { if (typeof e === 'string') {

View File

@@ -54,9 +54,10 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
return t(`${i18nPrefix}.installPlugin`) return t(`${i18nPrefix}.installPlugin`)
}, [isBundle, step, t]) }, [isBundle, step, t])
const handleInstalled = useCallback(() => { const handleInstalled = useCallback((notRefresh?: boolean) => {
setStep(InstallStep.installed) setStep(InstallStep.installed)
refreshPluginList(manifest) if (!notRefresh)
refreshPluginList(manifest)
setIsInstalling(false) setIsInstalling(false)
}, [manifest, refreshPluginList, setIsInstalling]) }, [manifest, refreshPluginList, setIsInstalling])

View File

@@ -21,7 +21,7 @@ type Props = {
payload: PluginManifestInMarket | Plugin payload: PluginManifestInMarket | Plugin
onCancel: () => void onCancel: () => void
onStartToInstall?: () => void onStartToInstall?: () => void
onInstalled: () => void onInstalled: (notRefresh?: boolean) => void
onFailed: (message?: string) => void onFailed: (message?: string) => void
} }
@@ -51,7 +51,7 @@ const Installed: FC<Props> = ({
check, check,
stop, stop,
} = checkTaskStatus() } = checkTaskStatus()
const { handleRefetch } = usePluginTaskList() const { handleRefetch } = usePluginTaskList(payload.category)
useEffect(() => { useEffect(() => {
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier) if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
@@ -106,7 +106,7 @@ const Installed: FC<Props> = ({
onFailed(error) onFailed(error)
return return
} }
onInstalled() onInstalled(true)
} }
catch (e) { catch (e) {
if (typeof e === 'string') { if (typeof e === 'string') {

View File

@@ -113,6 +113,7 @@ const DetailHeader = ({
}, },
payload: { payload: {
type: PluginSource.github, type: PluginSource.github,
category: detail.declaration.category,
github: { github: {
originalPackageInfo: { originalPackageInfo: {
id: detail.plugin_unique_identifier, id: detail.plugin_unique_identifier,
@@ -287,6 +288,7 @@ const DetailHeader = ({
isShowUpdateModal && ( isShowUpdateModal && (
<UpdateFromMarketplace <UpdateFromMarketplace
payload={{ payload={{
category: detail.declaration.category,
originalPackageInfo: { originalPackageInfo: {
id: detail.plugin_unique_identifier, id: detail.plugin_unique_identifier,
payload: detail.declaration, payload: detail.declaration,

View File

@@ -10,6 +10,8 @@ import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import type { ToolValue } from '@/app/components/workflow/block-selector/types' import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
@@ -21,6 +23,9 @@ type Props = {
supportCollapse?: boolean supportCollapse?: boolean
scope?: string scope?: string
onChange: (value: ToolValue[]) => void onChange: (value: ToolValue[]) => void
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string
} }
const MultipleToolSelector = ({ const MultipleToolSelector = ({
@@ -32,6 +37,9 @@ const MultipleToolSelector = ({
supportCollapse, supportCollapse,
scope, scope,
onChange, onChange,
nodeOutputVars,
availableNodes,
nodeId,
}: Props) => { }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const enabledCount = value.filter(item => item.enabled).length const enabledCount = value.filter(item => item.enabled).length
@@ -121,6 +129,9 @@ const MultipleToolSelector = ({
{!collapse && ( {!collapse && (
<> <>
<ToolSelector <ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope} scope={scope}
value={undefined} value={undefined}
selectedTools={value} selectedTools={value}
@@ -140,6 +151,9 @@ const MultipleToolSelector = ({
{value.length > 0 && value.map((item, index) => ( {value.length > 0 && value.map((item, index) => (
<div className='mb-1' key={index}> <div className='mb-1' key={index}>
<ToolSelector <ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope} scope={scope}
value={item} value={item}
selectedTools={value} selectedTools={value}

View File

@@ -21,8 +21,10 @@ import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/too
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { import {
@@ -41,6 +43,8 @@ import type {
Placement, Placement,
} from '@floating-ui/react' } from '@floating-ui/react'
import { MARKETPLACE_API_PREFIX } from '@/config' import { MARKETPLACE_API_PREFIX } from '@/config'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
@@ -54,6 +58,7 @@ type Props = {
provider_name: string provider_name: string
tool_name: string tool_name: string
tool_label: string tool_label: string
settings?: Record<string, any>
parameters?: Record<string, any> parameters?: Record<string, any>
extra?: Record<string, any> extra?: Record<string, any>
}) => void }) => void
@@ -65,6 +70,9 @@ type Props = {
onControlledStateChange?: (state: boolean) => void onControlledStateChange?: (state: boolean) => void
panelShowState?: boolean panelShowState?: boolean
onPanelShowStateChange?: (state: boolean) => void onPanelShowStateChange?: (state: boolean) => void
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string,
} }
const ToolSelector: FC<Props> = ({ const ToolSelector: FC<Props> = ({
value, value,
@@ -81,6 +89,9 @@ const ToolSelector: FC<Props> = ({
onControlledStateChange, onControlledStateChange,
panelShowState, panelShowState,
onPanelShowStateChange, onPanelShowStateChange,
nodeOutputVars,
availableNodes,
nodeId = '',
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isShow, onShowChange] = useState(false) const [isShow, onShowChange] = useState(false)
@@ -107,17 +118,20 @@ const ToolSelector: FC<Props> = ({
const [isShowChooseTool, setIsShowChooseTool] = useState(false) const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const handleSelectTool = (tool: ToolDefaultValue) => { const handleSelectTool = (tool: ToolDefaultValue) => {
const paramValues = addDefaultValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any)) const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
const toolValue = { const toolValue = {
provider_name: tool.provider_id, provider_name: tool.provider_id,
type: tool.provider_type, type: tool.provider_type,
tool_name: tool.tool_name, tool_name: tool.tool_name,
tool_label: tool.tool_label, tool_label: tool.tool_label,
settings: settingValues,
parameters: paramValues, parameters: paramValues,
enabled: tool.is_team_authorization, enabled: tool.is_team_authorization,
extra: { extra: {
description: '', description: '',
}, },
schemas: tool.paramSchemas,
} }
onSelect(toolValue) onSelect(toolValue)
// setIsShowChooseTool(false) // setIsShowChooseTool(false)
@@ -133,14 +147,33 @@ const ToolSelector: FC<Props> = ({
} as any) } as any)
} }
const currentToolParams = useMemo(() => { // tool settings & params
const currentToolSettings = useMemo(() => {
if (!currentProvider) return [] if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || [] return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || []
}, [currentProvider, value]) }, [currentProvider, value])
const currentToolParams = useMemo(() => {
if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form === 'llm') || []
}, [currentProvider, value])
const [currType, setCurrType] = useState('settings')
const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0
const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length
const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length
const formSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams]) const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings])
const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const handleFormChange = (v: Record<string, any>) => { const handleSettingsFormChange = (v: Record<string, any>) => {
const newValue = getStructureValue(v)
const toolValue = {
...value,
settings: newValue,
}
onSelect(toolValue as any)
}
const handleParamsFormChange = (v: Record<string, any>) => {
const toolValue = { const toolValue = {
...value, ...value,
parameters: v, parameters: v,
@@ -281,12 +314,9 @@ const ToolSelector: FC<Props> = ({
</div> </div>
{/* authorization */} {/* authorization */}
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && ( {currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<div className='px-4 pt-3 flex flex-col'> <>
<div className='flex items-center gap-2'> <Divider className='my-1 w-full' />
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.auth')}</div> <div className='px-4 py-2'>
<Divider bgStyle='gradient' className='grow' />
</div>
<div className='py-2'>
{!currentProvider.is_team_authorization && ( {!currentProvider.is_team_authorization && (
<Button <Button
variant='primary' variant='primary'
@@ -309,37 +339,87 @@ const ToolSelector: FC<Props> = ({
</Button> </Button>
)} )}
</div> </div>
</div> </>
)} )}
{/* tool settings */} {/* tool settings */}
{currentToolParams.length > 0 && currentProvider?.is_team_authorization && ( {(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
<div className='px-4 pt-3'> <>
<div className='flex items-center gap-2'> <Divider className='my-1 w-full' />
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div> {/* tabs */}
<Divider bgStyle='gradient' className='grow' /> {nodeId && showTabSlider && (
</div> <TabSlider
<div className='py-2'> className='shrink-0 mt-1 px-4'
<Form itemClassName='py-3'
value={value?.parameters || {}} noBorderBottom
onChange={handleFormChange} smallItem
formSchemas={formSchemas as any} value={currType}
isEditMode={true} onChange={(value) => {
showOnVariableMap={{}} setCurrType(value)
validating={false} }}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover' options={[
fieldMoreInfo={item => item.url { value: 'settings', text: t('plugin.detailPanel.toolSelector.settings')! },
? (<a { value: 'params', text: t('plugin.detailPanel.toolSelector.params')! },
href={item.url} ]}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
/> />
</div> )}
</div> {nodeId && showTabSlider && currType === 'params' && (
<div className='px-4 py-2'>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className='p-4 pb-1'>
<div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
</div>
)}
{/* reasoning config only */}
{nodeId && reasoningConfigOnly && (
<div className='mb-1 p-4 pb-1'>
<div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.params')}</div>
<div className='pb-1'>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<Form
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
formSchemas={settingsFormSchemas as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
/>
</div>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/>
)}
</>
)} )}
</> </>
)} )}

View File

@@ -0,0 +1,275 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import {
RiArrowRightUpLine,
} from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Node } from 'reactflow'
import type {
NodeOutPutVar,
ValueSelector,
Var,
} from '@/app/components/workflow/types'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
value: Record<string, any>
onChange: (val: Record<string, any>) => void
schemas: any[]
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId: string
}
const ReasoningConfigForm: React.FC<Props> = ({
value,
onChange,
schemas,
nodeOutputVars,
availableNodes,
nodeId,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const handleAutomatic = (key: string, val: any) => {
onChange({
...value,
[key]: {
value: val ? null : value[key]?.value,
auto: val ? 1 : 0,
},
})
}
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
const handleInputFocus = useCallback((variable: string) => {
return (value: boolean) => {
setInputsIsFocus((prev) => {
return {
...prev,
[variable]: value,
}
})
}
}, [])
const handleNotMixedTypeChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.type = varKindType
target.value = varValue
}
else {
draft[variable].value = {
type: varKindType,
value: varValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleMixedTypeChange = useCallback((variable: string) => {
return (itemValue: string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.value = itemValue
}
else {
draft[variable].value = {
type: VarKindType.mixed,
value: itemValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleFileChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: VarKindType.variable,
value: varValue,
}
})
onChange(newValue)
}
}, [value, onChange])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
inputs: Record<string, any>
files?: any[]
}) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = app as any
})
onChange(newValue)
}
}, [onChange, value])
const handleModelChange = useCallback((variable: string) => {
return (model: any) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
...draft[variable].value,
...model,
} as any
})
onChange(newValue)
}
}, [onChange, value])
const renderField = (schema: any) => {
const {
variable,
label,
required,
tooltip,
type,
scope,
url,
} = schema
const auto = value[variable]?.auto
const tooltipContent = (tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
asChild={false} />
))
const varInput = value[variable].value
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
return (
<div key={variable} className='space-y-1'>
<div className='flex items-center justify-between py-2 system-sm-semibold text-text-secondary'>
<div className='flex items-center space-x-2'>
<span className={cn('text-text-secondary code-sm-semibold')}>{label[language] || label.en_US}</span>
{required && (
<span className='ml-1 text-red-500'>*</span>
)}
{tooltipContent}
</div>
<div className='flex items-center gap-1 px-2 py-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter cursor-pointer hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}>
<span className='text-text-secondary system-xs-medium'>{t('plugin.detailPanel.toolSelector.auto')}</span>
<Switch
size='xs'
defaultValue={!!auto}
onChange={val => handleAutomatic(variable, val)}
/>
</div>
</div>
{auto === 0 && (
<>
{isString && (
<Input
className={cn(inputsIsFocus[variable] ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'rounded-lg px-3 py-[6px] border')}
value={varInput?.value as string || ''}
onChange={handleMixedTypeChange(variable)}
nodesOutputVars={nodeOutputVars}
availableNodes={availableNodes}
onFocusChange={handleInputFocus(variable)}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)}
{/* {isString && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || ''}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.number || varPayload.type === VarType.secret || varPayload.type === VarType.string}
/>
)} */}
{(isNumber || isSelect) && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue
filterVar={isNumber ? (varPayload: Var) => varPayload.type === schema._type : undefined}
availableVars={isSelect ? nodeOutputVars : undefined}
schema={schema}
/>
)}
{isFile && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleFileChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
/>
)}
{isAppSelector && (
<AppSelector
disabled={false}
scope={scope || 'all'}
value={varInput as any}
onSelect={handleAppChange(variable)}
/>
)}
{isModelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
value={varInput as any}
setModel={handleModelChange(variable)}
scope={scope}
/>
)}
</>
)}
{url && (
<a
href={url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>
)}
</div>
)
}
return (
<div className='px-4 py-2 space-y-3'>
{schemas.map(schema => renderField(schema))}
</div>
)
}
export default ReasoningConfigForm

View File

@@ -14,6 +14,7 @@ import { useGitHubReleases } from '../install-plugin/hooks'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import type { PluginType } from '@/app/components/plugins/types'
const i18nPrefix = 'plugin.action' const i18nPrefix = 'plugin.action'
@@ -22,6 +23,7 @@ type Props = {
installationId: string installationId: string
pluginUniqueIdentifier: string pluginUniqueIdentifier: string
pluginName: string pluginName: string
category: PluginType
usedInApps: number usedInApps: number
isShowFetchNewVersion: boolean isShowFetchNewVersion: boolean
isShowInfo: boolean isShowInfo: boolean
@@ -34,6 +36,7 @@ const Action: FC<Props> = ({
installationId, installationId,
pluginUniqueIdentifier, pluginUniqueIdentifier,
pluginName, pluginName,
category,
isShowFetchNewVersion, isShowFetchNewVersion,
isShowInfo, isShowInfo,
isShowDelete, isShowDelete,
@@ -67,6 +70,7 @@ const Action: FC<Props> = ({
}, },
payload: { payload: {
type: PluginSource.github, type: PluginSource.github,
category,
github: { github: {
originalPackageInfo: { originalPackageInfo: {
id: pluginUniqueIdentifier, id: pluginUniqueIdentifier,

View File

@@ -20,11 +20,9 @@ import Title from '../card/base/title'
import Action from './action' import Action from './action'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
import { useSingleCategories } from '../hooks' import { useSingleCategories } from '../hooks'
import { useProviderContext } from '@/context/provider-context'
import { useRenderI18nObject } from '@/hooks/use-i18n' import { useRenderI18nObject } from '@/hooks/use-i18n'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
type Props = { type Props = {
className?: string className?: string
@@ -39,10 +37,7 @@ const PluginItem: FC<Props> = ({
const { categoriesMap } = useSingleCategories() const { categoriesMap } = useSingleCategories()
const currentPluginID = usePluginPageContext(v => v.currentPluginID) const currentPluginID = usePluginPageContext(v => v.currentPluginID)
const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID) const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID)
const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const { refreshPluginList } = useRefreshPluginList()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const { refreshModelProviders } = useProviderContext()
const { const {
source, source,
@@ -60,13 +55,7 @@ const PluginItem: FC<Props> = ({
}, [source, author]) }, [source, author])
const handleDelete = () => { const handleDelete = () => {
invalidateInstalledPluginList() refreshPluginList({ category } as any)
if (PluginType.model.includes(category))
refreshModelProviders()
if (PluginType.tool.includes(category)) {
invalidateAllToolProviders()
invalidateAllBuiltinTools()
}
} }
const getValueFromI18nObject = useRenderI18nObject() const getValueFromI18nObject = useRenderI18nObject()
const title = getValueFromI18nObject(label) const title = getValueFromI18nObject(label)
@@ -116,6 +105,7 @@ const PluginItem: FC<Props> = ({
isShowDelete isShowDelete
meta={meta} meta={meta}
onDelete={handleDelete} onDelete={handleDelete}
category={category}
/> />
</div> </div>
</div> </div>

View File

@@ -29,8 +29,8 @@ const DebugInfo: FC = () => {
popupContent={ popupContent={
<> <>
<div className='flex items-center gap-1 self-stretch'> <div className='flex items-center gap-1 self-stretch'>
<span className='flex flex-col justify-center items-start flex-grow flex-shrink-0 basis-0 text-text-secondary system-sm-semibold'>{t(`${i18nPrefix}.title`)}</span> <span className='flex flex-col justify-center items-start grow shrink-0 basis-0 text-text-secondary system-sm-semibold'>{t(`${i18nPrefix}.title`)}</span>
<a href='' target='_blank' className='flex items-center gap-0.5 text-text-accent-light-mode-only cursor-pointer'> <a href='https://docs.dify.ai/plugins/quick-start/develop-plugins/debug-plugin' target='_blank' className='flex items-center gap-0.5 text-text-accent-light-mode-only cursor-pointer'>
<span className='system-xs-medium'>{t(`${i18nPrefix}.viewDocs`)}</span> <span className='system-xs-medium'>{t(`${i18nPrefix}.viewDocs`)}</span>
<RiArrowRightUpLine className='w-3 h-3' /> <RiArrowRightUpLine className='w-3 h-3' />
</a> </a>

View File

@@ -151,6 +151,7 @@ export type Permissions = {
} }
export type UpdateFromMarketPlacePayload = { export type UpdateFromMarketPlacePayload = {
category: PluginType
originalPackageInfo: { originalPackageInfo: {
id: string id: string
payload: PluginDeclaration payload: PluginDeclaration
@@ -173,6 +174,7 @@ export type UpdateFromGitHubPayload = {
export type UpdatePluginPayload = { export type UpdatePluginPayload = {
type: PluginSource type: PluginSource
category: PluginType
marketPlace?: UpdateFromMarketPlacePayload marketPlace?: UpdateFromMarketPlacePayload
github?: UpdateFromGitHubPayload github?: UpdateFromGitHubPayload
} }

View File

@@ -57,7 +57,7 @@ const UpdatePluginModal: FC<Props> = ({
} }
const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted) const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted)
const { handleRefetch } = usePluginTaskList() const { handleRefetch } = usePluginTaskList(payload.category)
const configBtnText = useMemo(() => { const configBtnText = useMemo(() => {
return ({ return ({

View File

@@ -69,7 +69,7 @@ const ProviderList = () => {
className='relative flex flex-col overflow-y-auto bg-background-body grow' className='relative flex flex-col overflow-y-auto bg-background-body grow'
> >
<div className={cn( <div className={cn(
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] z-20 flex-wrap gap-y-2', 'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-20 flex-wrap gap-y-2',
currentProvider && 'pr-6', currentProvider && 'pr-6',
)}> )}>
<TabSliderNew <TabSliderNew

View File

@@ -63,3 +63,34 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
}) })
return newValues return newValues
} }
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => {
const newValues = {} as any
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
newValues[formSchema.variable] = {
...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }),
}
}
})
return newValues
}
export const getPlainValue = (value: Record<string, any>) => {
const plainValue = { ...value } as any
Object.keys(plainValue).forEach((key) => {
plainValue[key] = value[key].value
})
return plainValue
}
export const getStructureValue = (value: Record<string, any>) => {
const newValue = { ...value } as any
Object.keys(newValue).forEach((key) => {
newValue[key] = {
value: value[key],
}
})
return newValue
}

View File

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

View File

@@ -184,7 +184,7 @@ export const useChecklistBeforePublish = () => {
} }
return true return true
}, [nodesExtraData, notify, t, store, isChatMode, buildInTools, customTools, workflowTools, language]) }, [store, isChatMode, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders])
return { return {
handleCheckBeforePublish, handleCheckBeforePublish,

View File

@@ -36,6 +36,7 @@ export type AgentStrategyProps = {
onFormValueChange: (value: ToolVarInputs) => void onFormValueChange: (value: ToolVarInputs) => void
nodeOutputVars?: NodeOutPutVar[], nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[], availableNodes?: Node[],
nodeId?: string
} }
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
@@ -46,7 +47,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
export const AgentStrategy = memo((props: AgentStrategyProps) => { export const AgentStrategy = memo((props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes } = props const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
const { t } = useTranslation() const { t } = useTranslation()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration) const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
const renderI18nObject = useRenderI18nObject() const renderI18nObject = useRenderI18nObject()
@@ -141,7 +142,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
] ]
const renderField: ComponentProps<typeof Form<CustomField>>['customRenderField'] = (schema, props) => { const renderField: ComponentProps<typeof Form<CustomField>>['customRenderField'] = (schema, props) => {
switch (schema.type) { switch (schema.type) {
case 'tool-selector': { case FormTypeEnum.toolSelector: {
const value = props.value[schema.variable] const value = props.value[schema.variable]
const onChange = (value: any) => { const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value }) props.onChange({ ...props.value, [schema.variable]: value })
@@ -154,6 +155,9 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)} tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
> >
<ToolSelector <ToolSelector
nodeId={props.nodeId || ''}
nodeOutputVars={props.nodeOutputVars || []}
availableNodes={props.availableNodes || []}
scope={schema.scope} scope={schema.scope}
value={value} value={value}
onSelect={item => onChange(item)} onSelect={item => onChange(item)}
@@ -162,13 +166,16 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
</Field> </Field>
) )
} }
case 'array[tools]': { case FormTypeEnum.multiToolSelector: {
const value = props.value[schema.variable] const value = props.value[schema.variable]
const onChange = (value: any) => { const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value }) props.onChange({ ...props.value, [schema.variable]: value })
} }
return ( return (
<MultipleToolSelector <MultipleToolSelector
nodeId={props.nodeId || ''}
nodeOutputVars={props.nodeOutputVars || []}
availableNodes={props.availableNodes || []}
scope={schema.scope} scope={schema.scope}
value={value || []} value={value || []}
label={renderI18nObject(schema.label)} label={renderI18nObject(schema.label)}
@@ -199,6 +206,9 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
fieldLabelClassName='uppercase' fieldLabelClassName='uppercase'
customRenderField={renderField} customRenderField={renderField}
override={override} override={override}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
/> />
</div> </div>
: <ListEmpty : <ListEmpty

View File

@@ -14,7 +14,7 @@ import type { ToolNodeType } from '../../../tool/types'
import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types' import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types'
import type { IterationNodeType } from '../../../iteration/types' import type { IterationNodeType } from '../../../iteration/types'
import type { ListFilterNodeType } from '../../../list-operator/types' import type { ListFilterNodeType } from '../../../list-operator/types'
import { OUTPUT_FILE_SUB_VARIABLES } from '../../../if-else/default' import { OUTPUT_FILE_SUB_VARIABLES } from '../../../constants'
import type { DocExtractorNodeType } from '../../../document-extractor/types' import type { DocExtractorNodeType } from '../../../document-extractor/types'
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types' import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'

View File

@@ -64,6 +64,7 @@ type Props = {
placeholder?: string placeholder?: string
minWidth?: number minWidth?: number
popupFor?: 'assigned' | 'toAssigned' popupFor?: 'assigned' | 'toAssigned'
zIndex?: number
} }
const VarReferencePicker: FC<Props> = ({ const VarReferencePicker: FC<Props> = ({
@@ -90,6 +91,7 @@ const VarReferencePicker: FC<Props> = ({
placeholder, placeholder,
minWidth, minWidth,
popupFor, popupFor,
zIndex,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const store = useStoreApi() const store = useStoreApi()
@@ -386,7 +388,7 @@ const VarReferencePicker: FC<Props> = ({
</> </>
</WrapElem> </WrapElem>
<PortalToFollowElemContent style={{ <PortalToFollowElemContent style={{
zIndex: 100, zIndex: zIndex || 100,
}} className='mt-1'> }} className='mt-1'>
{!isConstant && ( {!isConstant && (
<VarReferencePopup <VarReferencePopup

View File

@@ -2,6 +2,7 @@ import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plug
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks' import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import type { NodeDefault } from '../../types' import type { NodeDefault } from '../../types'
import type { AgentNodeType } from './types' import type { AgentNodeType } from './types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { renderI18nObject } from '@/hooks/use-i18n' import { renderI18nObject } from '@/hooks/use-i18n'
const nodeDefault: NodeDefault<AgentNodeType> = { const nodeDefault: NodeDefault<AgentNodeType> = {
@@ -37,6 +38,94 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
} }
} }
for (const param of strategy.parameters) { for (const param of strategy.parameters) {
// single tool
if (param.required && param.type === FormTypeEnum.toolSelector) {
// no value
const toolValue = payload.agent_parameters?.[param.name]?.value
if (!toolValue) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
// not enabled
else if (!toolValue.enabled) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
}
}
// check form of tool
else {
const schemas = toolValue.schemas || []
const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
}
})
}
}
// multiple tools
if (param.required && param.type === FormTypeEnum.multiToolSelector) {
const tools = payload.agent_parameters?.[param.name]?.value || []
// no value
if (!tools.length) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
// not enabled
else if (tools.every((tool: any) => !tool.enabled)) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
}
}
// check form of tools
else {
let validState = {
isValid: true,
errorMessage: '',
}
for (const tool of tools) {
const schemas = tool.schemas || []
const userSettings = tool.settings
const reasoningConfig = tool.parameters
schemas.forEach((schema: any) => {
if (schema?.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
return validState = {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name]?.auto === 0 && !reasoningConfig[schema.name]?.value) {
return validState = {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
}
})
}
return validState
}
}
// common params
if (param.required && !payload.agent_parameters?.[param.name]?.value) { if (param.required && !payload.agent_parameters?.[param.name]?.value) {
return { return {
isValid: false, isValid: false,

View File

@@ -103,6 +103,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
onFormValueChange={onFormChange} onFormValueChange={onFormChange}
nodeOutputVars={availableVars} nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent} availableNodes={availableNodesWithParent}
nodeId={props.id}
/> />
</Field> </Field>
<div> <div>

View File

@@ -36,6 +36,7 @@ import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel' import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node' import AgentNode from './agent/node'
import AgentPanel from './agent/panel' import AgentPanel from './agent/panel'
import { TransferMethod } from '@/types/app'
export const NodeComponentMap: Record<string, ComponentType<any>> = { export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartNode, [BlockEnum.Start]: StartNode,
@@ -82,3 +83,18 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
} }
export const CUSTOM_NODE_TYPE = 'custom' export const CUSTOM_NODE_TYPE = 'custom'
export const FILE_TYPE_OPTIONS = [
{ value: 'image', i18nKey: 'image' },
{ value: 'document', i18nKey: 'doc' },
{ value: 'audio', i18nKey: 'audio' },
{ value: 'video', i18nKey: 'video' },
]
export const TRANSFER_METHOD = [
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')

View File

@@ -9,7 +9,7 @@ import {
isComparisonOperatorNeedTranslate, isComparisonOperatorNeedTranslate,
isEmptyRelatedOperator, isEmptyRelatedOperator,
} from '../utils' } from '../utils'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default' import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../../constants'
import type { ValueSelector } from '../../../types' import type { ValueSelector } from '../../../types'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'

View File

@@ -21,7 +21,7 @@ import {
} from '../../types' } from '../../types'
import { comparisonOperatorNotRequireValue, getOperators } from '../../utils' import { comparisonOperatorNotRequireValue, getOperators } from '../../utils'
import ConditionNumberInput from '../condition-number-input' import ConditionNumberInput from '../condition-number-input'
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default' import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../../constants'
import ConditionWrap from '../condition-wrap' import ConditionWrap from '../condition-wrap'
import ConditionOperator from './condition-operator' import ConditionOperator from './condition-operator'
import ConditionInput from './condition-input' import ConditionInput from './condition-input'
@@ -39,7 +39,7 @@ import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName' const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
interface ConditionItemProps { type ConditionItemProps = {
className?: string className?: string
disabled?: boolean disabled?: boolean
caseId: string caseId: string

View File

@@ -9,7 +9,7 @@ import {
comparisonOperatorNotRequireValue, comparisonOperatorNotRequireValue,
isComparisonOperatorNeedTranslate, isComparisonOperatorNeedTranslate,
} from '../utils' } from '../utils'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default' import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../../constants'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@@ -20,7 +20,7 @@ import type {
Node, Node,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
interface ConditionValueProps { type ConditionValueProps = {
variableSelector: string[] variableSelector: string[]
labelName?: string labelName?: string
operator: ComparisonOperator operator: ComparisonOperator

View File

@@ -12,7 +12,7 @@ import type { CaseItem, HandleAddCondition, HandleAddSubVariableCondition, Handl
import type { Node, NodeOutPutVar, Var } from '../../../types' import type { Node, NodeOutPutVar, Var } from '../../../types'
import { VarType } from '../../../types' import { VarType } from '../../../types'
import { useGetAvailableVars } from '../../variable-assigner/hooks' import { useGetAvailableVars } from '../../variable-assigner/hooks'
import { SUB_VARIABLES } from '../default' import { SUB_VARIABLES } from '../../constants'
import ConditionList from './condition-list' import ConditionList from './condition-list'
import ConditionAdd from './condition-add' import ConditionAdd from './condition-add'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'

View File

@@ -1,7 +1,6 @@
import { BlockEnum, type NodeDefault } from '../../types' import { BlockEnum, type NodeDefault } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types' import { type IfElseNodeType, LogicalOperator } from './types'
import { isEmptyRelatedOperator } from './utils' import { isEmptyRelatedOperator } from './utils'
import { TransferMethod } from '@/types/app'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks' import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const i18nPrefix = 'workflow.errorMsg' const i18nPrefix = 'workflow.errorMsg'
@@ -79,18 +78,3 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
} }
export default nodeDefault export default nodeDefault
export const FILE_TYPE_OPTIONS = [
{ value: 'image', i18nKey: 'image' },
{ value: 'document', i18nKey: 'doc' },
{ value: 'audio', i18nKey: 'audio' },
{ value: 'video', i18nKey: 'video' },
]
export const TRANSFER_METHOD = [
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')

View File

@@ -9,7 +9,7 @@ import { ComparisonOperator } from '../../if-else/types'
import { comparisonOperatorNotRequireValue, getOperators } from '../../if-else/utils' import { comparisonOperatorNotRequireValue, getOperators } from '../../if-else/utils'
import SubVariablePicker from './sub-variable-picker' import SubVariablePicker from './sub-variable-picker'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/if-else/default' import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/constants'
import { SimpleSelect as Select } from '@/app/components/base/select' import { SimpleSelect as Select } from '@/app/components/base/select'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName' const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SUB_VARIABLES } from '../../if-else/default' import { SUB_VARIABLES } from '../../constants'
import type { Item } from '@/app/components/base/select' import type { Item } from '@/app/components/base/select'
import { SimpleSelect as Select } from '@/app/components/base/select' import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'

View File

@@ -168,6 +168,46 @@ const translation = {
preprocessDocument: '{{num}} Preprocess Documents', preprocessDocument: '{{num}} Preprocess Documents',
allKnowledge: 'All Knowledge', allKnowledge: 'All Knowledge',
allKnowledgeDescription: 'Select to display all knowledge in this workspace. Only the Workspace Owner can manage all knowledge.', allKnowledgeDescription: 'Select to display all knowledge in this workspace. Only the Workspace Owner can manage all knowledge.',
metadata: {
metadata: 'Metadata',
addMetadata: 'Add Metadata',
createMetadata: {
title: 'New Metadata',
back: 'Back',
type: 'Type',
name: 'Name',
namePlaceholder: 'Add metadata name',
},
batchEditMetadata: {
editMetadata: 'Edit Metadata',
editDocumentsNum: 'Editing {{num}} documents',
applyToAllSelectDocument: 'Apply to all selected documents',
applyToAllSelectDocumentTip: 'Automatically create all the above edited and new metadata for all selected documents, otherwise editing metadata will only apply to documents with it.',
multipleValue: 'Multiple Value',
},
selectMetadata: {
search: 'Search metadata',
newAction: 'New Metadata',
manageAction: 'Manage',
},
datasetMetadata: {
description: 'You can manage all metadata in this knowledge here. Modifications will be synchronized to every document.',
addMetaData: 'Add Metadata',
values: '{{num}} Values',
disabled: 'Disabled',
rename: 'Rename',
name: 'Name',
namePlaceholder: 'Metadata name',
builtIn: 'Built-in',
builtInDescription: 'Built-in metadata is automatically extracted and generated. It must be enabled before use and cannot be edited.',
},
documentMetadata: {
metadataToolTip: 'Metadata serves as a critical filter that enhances the accuracy and relevance of information retrieval. You can modify and add metadata for this document here.',
startLabeling: 'Start Labeling',
documentInformation: 'Document Information',
technicalParameters: 'Technical Parameters',
},
},
} }
export default translation export default translation

View File

@@ -78,8 +78,11 @@ const translation = {
descriptionLabel: 'Tool description', descriptionLabel: 'Tool description',
descriptionPlaceholder: 'Brief description of the tool\'s purpose, e.g., get the temperature for a specific location.', descriptionPlaceholder: 'Brief description of the tool\'s purpose, e.g., get the temperature for a specific location.',
placeholder: 'Select a tool...', placeholder: 'Select a tool...',
auth: 'AUTHORIZATION', settings: 'USER SETTINGS',
settings: 'TOOL SETTINGS', params: 'REASONING CONFIG',
paramsTip1: 'Controls LLM inference parameters.',
paramsTip2: 'When \'Automatic\' is off, the default value is used.',
auto: 'Automatic',
empty: 'Click the \'+\' button to add tools. You can add multiple tools.', empty: 'Click the \'+\' button to add tools. You can add multiple tools.',
uninstalledTitle: 'Tool not installed', uninstalledTitle: 'Tool not installed',
uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.', uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.',

View File

@@ -195,6 +195,8 @@ const translation = {
visionVariable: 'Vision Variable', visionVariable: 'Vision Variable',
}, },
invalidVariable: 'Invalid variable', invalidVariable: 'Invalid variable',
noValidTool: '{{field}} no valid tool selected',
toolParameterRequired: '{{field}}: parameter [{{param}}] is required',
}, },
singleRun: { singleRun: {
testRun: 'Test Run ', testRun: 'Test Run ',

View File

@@ -168,6 +168,46 @@ const translation = {
preprocessDocument: '{{num}} 个预处理文档', preprocessDocument: '{{num}} 个预处理文档',
allKnowledge: '所有知识库', allKnowledge: '所有知识库',
allKnowledgeDescription: '选择以显示该工作区内所有知识库。只有工作区所有者才能管理所有知识库。', allKnowledgeDescription: '选择以显示该工作区内所有知识库。只有工作区所有者才能管理所有知识库。',
metadata: {
metadata: '元数据',
addMetadata: '添加元数据',
createMetadata: {
title: '新建元数据',
back: '返回',
type: '类型',
name: '名称',
namePlaceholder: '添加元数据名称',
},
batchEditMetadata: {
editMetadata: '编辑元数据',
editDocumentsNum: '编辑 {{num}} 个文档',
applyToAllSelectDocument: '应用于所有选定文档',
applyToAllSelectDocumentTip: '自动为所有选定文档创建上述编辑和新元数据,否则仅对具有元数据的文档应用编辑。',
multipleValue: '多个值',
},
selectMetadata: {
search: '搜索元数据',
newAction: '新建元数据',
manageAction: '管理',
},
datasetMetadata: {
description: '元数据是关于文档的数据,用于描述文档的属性。元数据可以帮助您更好地组织和管理文档。',
addMetaData: '添加元数据',
values: '{{num}} 个值',
disabled: '已禁用',
rename: '重命名',
name: '名称',
namePlaceholder: '元数据名称',
builtIn: '内置',
builtInDescription: '内置元数据是系统预定义的元数据,您可以在此处查看和管理内置元数据。',
},
documentMetadata: {
metadataToolTip: '元数据是关于文档的数据,用于描述文档的属性。元数据可以帮助您更好地组织和管理文档。',
startLabeling: '开始标注',
documentInformation: '文档信息',
technicalParameters: '技术参数',
},
},
} }
export default translation export default translation

View File

@@ -78,8 +78,11 @@ const translation = {
descriptionLabel: '工具描述', descriptionLabel: '工具描述',
descriptionPlaceholder: '简要描述工具目的,例如,获取特定位置的温度。', descriptionPlaceholder: '简要描述工具目的,例如,获取特定位置的温度。',
placeholder: '选择工具', placeholder: '选择工具',
auth: '授权', settings: '用户设置',
settings: '工具设置', params: '推理配置',
paramsTip1: '控制 LLM 推理参数。',
paramsTip2: '当“自动”关闭时,使用默认值。',
auto: '自动',
empty: '点击 "+" 按钮添加工具。您可以添加多个工具。', empty: '点击 "+" 按钮添加工具。您可以添加多个工具。',
uninstalledTitle: '工具未安装', uninstalledTitle: '工具未安装',
uninstalledContent: '此插件安装自 本地 / GitHub 仓库,请安装后使用。', uninstalledContent: '此插件安装自 本地 / GitHub 仓库,请安装后使用。',

View File

@@ -195,6 +195,9 @@ const translation = {
visionVariable: '视觉变量', visionVariable: '视觉变量',
}, },
invalidVariable: '无效的变量', invalidVariable: '无效的变量',
noValidTool: '{{field}} 无可用工具',
toolParameterRequired: '{{field}}: 参数 [{{param}}] 不能为空',
}, },
singleRun: { singleRun: {
testRun: '测试运行 ', testRun: '测试运行 ',

View File

@@ -11,6 +11,7 @@ import type {
PluginDetail, PluginDetail,
PluginInfoFromMarketPlace, PluginInfoFromMarketPlace,
PluginTask, PluginTask,
PluginType,
PluginsFromMarketplaceByInfoResponse, PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse, PluginsFromMarketplaceResponse,
VersionInfo, VersionInfo,
@@ -31,6 +32,7 @@ import {
import { useInvalidateAllBuiltInTools } from './use-tools' import { useInvalidateAllBuiltInTools } from './use-tools'
import usePermission from '@/app/components/plugins/plugin-page/use-permission' import usePermission from '@/app/components/plugins/plugin-page/use-permission'
import { uninstallPlugin } from '@/service/plugins' import { uninstallPlugin } from '@/service/plugins'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
const NAME_SPACE = 'plugins' const NAME_SPACE = 'plugins'
@@ -367,10 +369,11 @@ export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[])
} }
const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList'] const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
export const usePluginTaskList = () => { export const usePluginTaskList = (category?: PluginType) => {
const { const {
canManagement, canManagement,
} = usePermission() } = usePermission()
const { refreshPluginList } = useRefreshPluginList()
const { const {
data, data,
isFetched, isFetched,
@@ -383,8 +386,12 @@ export const usePluginTaskList = () => {
refetchInterval: (lastQuery) => { refetchInterval: (lastQuery) => {
const lastData = lastQuery.state.data const lastData = lastQuery.state.data
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed) const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
if (taskDone) const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
if (taskDone) {
if (lastData?.tasks.length && !taskAllFailed)
refreshPluginList(category ? { category } as any : undefined, !category)
return false return false
}
return 5000 return 5000
}, },