This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
'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'
import { noop } from 'lodash-es'
const i18nPrefix = 'dataset.metadata.createMetadata'
export type Props = {
onClose?: () => void
onSave: (data: any) => void
hasBack?: boolean
onBack?: () => void
}
const CreateContent: FC<Props> = ({
onClose = noop,
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={onClose}
onConfirm={handleSave}
hideCloseBtn={hasBack}
beforeHeader={hasBack && (
<div className='relative left-[-4px] mb-1 flex cursor-pointer items-center space-x-1 py-1 text-text-accent' 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,45 @@
'use client'
import type { FC } from 'react'
import React 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 = {
open: boolean
setOpen: (open: boolean) => void
onSave: (data: any) => void
trigger: React.ReactNode
popupLeft?: number
} & CreateContentProps
const CreateMetadataModal: FC<Props> = ({
open,
setOpen,
trigger,
popupLeft = 20,
...createContentProps
}) => {
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} onClose={() => setOpen(false)} onBack={() => setOpen(false)} />
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(CreateMetadataModal)

View File

@@ -0,0 +1,248 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import type { BuiltInMetadataItem, MetadataItemWithValueLength } from '../types'
import Drawer from '@/app/components/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 '@/app/components/base/modal'
import Field from './field'
import Input from '@/app/components/base/input'
import { useTranslation } from 'react-i18next'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
import { useBoolean, useHover } from 'ahooks'
import Confirm from '@/app/components/base/confirm'
import Toast from '@/app/components/base/toast'
const i18nPrefix = 'dataset.metadata.datasetMetadata'
type Props = {
userMetadata: MetadataItemWithValueLength[]
builtInMetadata: BuiltInMetadataItem[]
isBuiltInEnabled: boolean
onIsBuiltInEnabledChange: (value: boolean) => void
onClose: () => void
onAdd: (payload: BuiltInMetadataItem) => void
onRename: (payload: MetadataItemWithValueLength) => void
onRemove: (metaDataId: string) => 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.name}
className={cn(
!readonly && !disabled && 'group/item cursor-pointer hover:shadow-xs',
'rounded-md border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg',
isDeleteHovering && 'border border-state-destructive-border bg-state-destructive-hover',
)}
>
<div
className={cn(
'flex h-8 items-center justify-between px-2',
disabled && 'opacity-30', // not include border and bg
)}
>
<div className='flex h-full items-center space-x-1 text-text-tertiary'>
<Icon className='size-4 shrink-0' />
<div className='system-sm-medium max-w-[250px] truncate text-text-primary'>{payload.name}</div>
<div className='system-xs-regular shrink-0'>{payload.type}</div>
</div>
{(!readonly || disabled) && (
<div className='system-xs-regular ml-2 shrink-0 text-text-tertiary group-hover/item:hidden'>
{disabled ? t(`${i18nPrefix}.disabled`) : t(`${i18nPrefix}.values`, { num: payload.count || 0 })}
</div>
)}
<div className='ml-2 hidden items-center space-x-1 text-text-tertiary group-hover/item:flex'>
<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={t('dataset.metadata.datasetMetadata.deleteTitle')}
content={t('dataset.metadata.datasetMetadata.deleteContent', { name: payload.name })}
onConfirm={handleDelete}
onCancel={hideDeleteConfirm}
/>
)}
</div>
</div>
)
}
const DatasetMetadataDrawer: FC<Props> = ({
userMetadata,
builtInMetadata,
isBuiltInEnabled,
onIsBuiltInEnabledChange,
onClose,
onAdd,
onRename,
onRemove,
}) => {
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 [open, setOpen] = useState(false)
const handleAdd = useCallback(async (data: MetadataItemWithValueLength) => {
await onAdd(data)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setOpen(false)
}, [onAdd, t])
const handleRenamed = useCallback(async () => {
const item = userMetadata.find(p => p.id === currPayload?.id)
if (item) {
await onRename({
...item,
name: templeName,
})
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}
setIsShowRenameModal(false)
}, [userMetadata, currPayload?.id, onRename, templeName, t])
const handleDelete = useCallback((payload: MetadataItemWithValueLength) => {
return async () => {
await onRemove(payload.id)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}
}, [onRemove, t])
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='h-full overflow-y-auto'>
<div className='system-sm-regular text-text-tertiary'>{t(`${i18nPrefix}.description`)}</div>
<CreateModal
open={open}
setOpen={setOpen}
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='system-sm-semibold ml-2 mr-0.5 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.name}
readonly
disabled={!isBuiltInEnabled}
payload={payload as MetadataItemWithValueLength}
/>
))}
</div>
{isShowRenameModal && (
<Modal isShow title={t(`${i18nPrefix}.rename`)} onClose={() => setIsShowRenameModal(false)}>
<Field label={t(`${i18nPrefix}.name`)} className='mt-4'>
<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>
)}
</div>
</Drawer>
)
}
export default React.memo(DatasetMetadataDrawer)

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='system-sm-semibold py-1 text-text-secondary'>{label}</div>
<div className='mt-1'>{children}</div>
</div>
)
}
export default React.memo(Field)

View File

@@ -0,0 +1,82 @@
'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 { useDatasetMetaData } from '@/service/knowledge/use-metadata'
type Props = {
datasetId: string
popupPlacement?: Placement
popupOffset?: { mainAxis: number, crossAxis: number }
onSelect: (data: MetadataItem) => void
onSave: (data: MetadataItem) => void
trigger: React.ReactNode
onManage: () => void
} & CreateContentProps
enum Step {
select = 'select',
create = 'create',
}
const SelectMetadataModal: FC<Props> = ({
datasetId,
popupPlacement = 'left-start',
popupOffset = { mainAxis: -38, crossAxis: 4 },
trigger,
onSelect,
onSave,
onManage,
}) => {
const { data: datasetMetaData } = useDatasetMetaData(datasetId)
const [open, setOpen] = useState(false)
const [step, setStep] = useState(Step.select)
const handleSave = useCallback(async (data: MetadataItem) => {
await onSave(data)
setStep(Step.select)
}, [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={(data) => {
onSelect(data)
setOpen(false)
}}
list={datasetMetaData?.doc_metadata || []}
onNew={() => setStep(Step.create)}
onManage={onManage}
/>
) : (
<CreateContent
onSave={handleSave}
hasBack
onBack={() => setStep(Step.select)}
onClose={() => setStep(Step.select)}
/>
)}
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(SelectMetadataModal)

View File

@@ -0,0 +1,82 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import type { MetadataItem } from '../types'
import SearchInput from '@/app/components/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: MetadataItem) => void
onNew: () => void
onManage: () => void
}
const SelectMetadata: FC<Props> = ({
list: notFilteredList,
onSelect,
onNew,
onManage,
}) => {
const { t } = useTranslation()
const [query, setQuery] = useState('')
const list = useMemo(() => {
if (!query) return notFilteredList
return notFilteredList.filter((item) => {
return item.name.toLowerCase().includes(query.toLowerCase())
})
}, [query, notFilteredList])
return (
<div className='w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-0 pt-2 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 h-6 cursor-pointer items-center justify-between rounded-md px-3 hover:bg-state-base-hover'
onClick={() => onSelect({
id: item.id,
name: item.name,
type: item.type,
})}
>
<div className='flex h-full w-0 grow items-center text-text-secondary'>
<Icon className='mr-[5px] size-3.5 shrink-0' />
<div className='system-sm-medium w-0 grow truncate'>{item.name}</div>
</div>
<div className='system-xs-regular ml-1 shrink-0 text-text-tertiary'>
{item.type}
</div>
</div>
)
})}
</div>
<div className='mt-1 flex justify-between border-t border-divider-subtle p-1'>
<div className='flex h-6 cursor-pointer items-center space-x-1 rounded-md px-3 text-text-secondary hover:bg-state-base-hover' onClick={onNew}>
<RiAddLine className='size-3.5' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.newAction`)}</div>
</div>
<div className='flex h-6 items-center text-text-secondary '>
<div className='mr-[3px] h-3 w-px bg-divider-regular'></div>
<div className='flex h-full cursor-pointer items-center rounded-md px-1.5 hover:bg-state-base-hover' onClick={onManage}>
<div className='system-sm-medium mr-1'>{t(`${i18nPrefix}.manageAction`)}</div>
<RiArrowRightUpLine className='size-3.5' />
</div>
</div>
</div>
</div>
)
}
export default React.memo(SelectMetadata)