Knowledge optimization (#3755)

Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
Jyong
2024-04-24 15:02:29 +08:00
committed by GitHub
parent 3cd8e6f5c6
commit f257f2c396
75 changed files with 2756 additions and 266 deletions

View File

@@ -2,7 +2,7 @@
import { useContext, useContextSelector } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css'
@@ -22,9 +22,12 @@ import { useProviderContext } from '@/context/provider-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import EditAppModal from '@/app/components/explore/create-app-modal'
import SwitchAppModal from '@/app/components/app/switch-app-modal'
import type { Tag } from '@/app/components/base/tag-management/constant'
import TagSelector from '@/app/components/base/tag-management/selector'
export type AppCardProps = {
app: App
@@ -142,6 +145,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
}
const Operations = (props: HtmlContentProps) => {
const onMouseLeave = async () => {
props.onClose?.()
}
const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
@@ -173,7 +179,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setShowConfirmDelete(true)
}
return (
<div className="relative w-full py-1">
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
<button className={s.actionItem} onClick={onClickSettings}>
<span className={s.actionName}>{t('app.editApp')}</span>
</button>
@@ -208,6 +214,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
)
}
const [tags, setTags] = useState<Tag[]>(app.tags)
useEffect(() => {
setTags(app.tags)
}, [app.tags])
return (
<>
<div
@@ -254,27 +265,71 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
</div>
</div>
{isCurrentWorkspaceManager && <CustomPopover
htmlContent={<Operations />}
position="br"
trigger="click"
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
btnClassName={open =>
cn(
open ? '!bg-gray-100 !shadow-none' : '!bg-transparent',
'!hidden h-8 w-8 !p-2 rounded-md border-none hover:!bg-gray-100 group-hover:!inline-flex',
)
}
className={'!w-[128px] h-fit !z-20'}
popupClassName={
(app.mode === 'completion' || app.mode === 'chat')
? '!w-[238px] translate-x-[-110px]'
: ''
}
manualClose
/>}
</div>
<div className='mb-1 px-[14px] text-xs leading-normal text-gray-500 line-clamp-4'>{app.description}</div>
<div
className={cn(
'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500 group-hover:line-clamp-2 group-hover:max-h-[36px]',
tags.length ? 'line-clamp-2' : 'line-clamp-4',
)}
title={app.description}
>
{app.description}
</div>
<div className={cn(
'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
tags.length ? 'flex' : '!hidden group-hover:!flex',
)}>
<div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}>
<div className={cn(
'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
tags.length ? '!block' : '!hidden',
)}>
<TagSelector
position='bl'
type='app'
targetID={app.id}
value={tags.map(tag => tag.id)}
selectedTags={tags}
onCacheUpdate={setTags}
onChange={onRefresh}
/>
</div>
</div>
{isCurrentWorkspaceManager && (
<>
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/>
<div className='!hidden group-hover:!flex shrink-0'>
<CustomPopover
htmlContent={<Operations />}
position="br"
trigger="click"
btnElement={
<div
className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
>
<DotsHorizontal className='w-4 h-4 text-gray-700' />
</div>
}
btnClassName={open =>
cn(
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
)
}
popupClassName={
(app.mode === 'completion' || app.mode === 'chat')
? '!w-[238px] translate-x-[-110px]'
: ''
}
className={'!w-[128px] h-fit !z-20'}
/>
</div>
</>
)}
</div>
{showEditModal && (
<EditAppModal
isEditModal

View File

@@ -13,19 +13,22 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { DotsGrid, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { DotsGrid } from '@/app/components/base/icons/src/vender/line/general'
import {
// AiText,
ChatBot,
CuteRobot,
} from '@/app/components/base/icons/src/vender/line/communication'
import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
import SearchInput from '@/app/components/base/search-input'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
tags: string[],
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
@@ -36,6 +39,9 @@ const getKey = (
else
delete params.params.mode
if (tags.length)
params.params.tag_ids = tags
return params
}
return null
@@ -44,14 +50,17 @@ const getKey = (
const Apps = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'all',
})
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
const [tagIDs, setTagIDs] = useState<string[]>([])
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const { data, isLoading, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, searchKeywords),
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords),
fetchAppList,
{ revalidateFirstPage: true },
)
@@ -61,7 +70,6 @@ const Apps = () => {
{ value: 'all', text: t('app.types.all'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1'/> },
{ value: 'chat', text: t('app.types.chatbot'), icon: <ChatBot className='w-[14px] h-[14px] mr-1'/> },
{ value: 'agent-chat', text: t('app.types.agent'), icon: <CuteRobot className='w-[14px] h-[14px] mr-1'/> },
// { value: 'completion', text: t('app.newApp.completeApp'), icon: <AiText className='w-[14px] h-[14px] mr-1'/> },
{ value: 'workflow', text: t('app.types.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1'/> },
]
@@ -88,14 +96,17 @@ const Apps = () => {
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
const handleKeywordsChange = (value: string) => {
setKeywords(value)
handleSearch()
}
const handleClear = () => {
handleKeywordsChange('')
const { run: handleTagsUpdate } = useDebounceFn(() => {
setTagIDs(tagFilterValue)
}, { wait: 500 })
const handleTagsChange = (value: string[]) => {
setTagFilterValue(value)
handleTagsUpdate()
}
return (
@@ -106,31 +117,9 @@ const Apps = () => {
onChange={setActiveTab}
options={options}
/>
<div className="flex items-center px-2 w-[200px] h-8 rounded-lg bg-gray-200">
<div className="pointer-events-none shrink-0 flex items-center mr-1.5 justify-center w-4 h-4">
<SearchLg className="h-3.5 w-3.5 text-gray-500" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="grow block h-[18px] bg-gray-200 rounded-md border-0 text-gray-600 text-[13px] placeholder:text-gray-500 appearance-none outline-none"
placeholder={t('common.operation.search')!}
value={keywords}
onChange={(e) => {
handleKeywordsChange(e.target.value)
}}
autoComplete="off"
/>
{
keywords && (
<div
className='shrink-0 flex items-center justify-center w-4 h-4 cursor-pointer'
onClick={handleClear}
>
<XCircle className='w-3.5 h-3.5 text-gray-400' />
</div>
)
}
<div className='flex items-center gap-2'>
<TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
</div>
</div>
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
@@ -142,6 +131,9 @@ const Apps = () => {
<CheckModal />
</nav>
<div ref={anchorRef} className='h-0'> </div>
{showTagManagementModal && (
<TagManagementModal type='app' show={showTagManagementModal} />
)}
</>
)
}

View File

@@ -1,8 +1,9 @@
'use client'
// Libraries
import { useRef } from 'react'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import useSWR from 'swr'
// Components
@@ -11,15 +12,20 @@ import DatasetFooter from './DatasetFooter'
import ApiServer from './ApiServer'
import Doc from './Doc'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import SearchInput from '@/app/components/base/search-input'
import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
// Hooks
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
const Container = () => {
const { t } = useTranslation()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
const options = [
{ value: 'dataset', text: t('dataset.datasets') },
@@ -32,6 +38,25 @@ const Container = () => {
const containerRef = useRef<HTMLDivElement>(null)
const { data } = useSWR(activeTab === 'dataset' ? null : '/datasets/api-base-info', fetchDatasetApiBaseUrl)
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
const handleKeywordsChange = (value: string) => {
setKeywords(value)
handleSearch()
}
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
const [tagIDs, setTagIDs] = useState<string[]>([])
const { run: handleTagsUpdate } = useDebounceFn(() => {
setTagIDs(tagFilterValue)
}, { wait: 500 })
const handleTagsChange = (value: string[]) => {
setTagFilterValue(value)
handleTagsUpdate()
}
return (
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
@@ -40,13 +65,22 @@ const Container = () => {
onChange={newActiveTab => setActiveTab(newActiveTab)}
options={options}
/>
{activeTab === 'dataset' && (
<div className='flex items-center gap-2'>
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
</div>
)}
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
</div>
{activeTab === 'dataset' && (
<>
<Datasets containerRef={containerRef} />
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} />
<DatasetFooter />
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
)}
</>
)}

View File

@@ -2,41 +2,44 @@
import { useContext } from 'use-context-selector'
import Link from 'next/link'
import type { MouseEventHandler } from 'react'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import style from '../list.module.css'
import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast'
import { deleteDataset } from '@/service/datasets'
import AppIcon from '@/app/components/base/app-icon'
import type { DataSet } from '@/models/datasets'
import Tooltip from '@/app/components/base/tooltip'
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
import type { HtmlContentProps } from '@/app/components/base/popover'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
import RenameDatasetModal from '@/app/components/datasets/rename-modal'
import type { Tag } from '@/app/components/base/tag-management/constant'
import TagSelector from '@/app/components/base/tag-management/selector'
export type DatasetCardProps = {
dataset: DataSet
onDelete?: () => void
onSuccess?: () => void
}
const DatasetCard = ({
dataset,
onDelete,
onSuccess,
}: DatasetCardProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [tags, setTags] = useState<Tag[]>(dataset.tags)
const [showRenameModal, setShowRenameModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const onDeleteClick: MouseEventHandler = useCallback((e) => {
e.preventDefault()
setShowConfirmDelete(true)
}, [])
const onConfirmDelete = useCallback(async () => {
try {
await deleteDataset(dataset.id)
notify({ type: 'success', message: t('dataset.datasetDeleted') })
if (onDelete)
onDelete()
if (onSuccess)
onSuccess()
}
catch (e: any) {
notify({ type: 'error', message: `${t('dataset.datasetDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
@@ -44,53 +47,158 @@ const DatasetCard = ({
setShowConfirmDelete(false)
}, [dataset.id])
const Operations = (props: HtmlContentProps) => {
const onMouseLeave = async () => {
props.onClose?.()
}
const onClickRename = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowRenameModal(true)
}
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowConfirmDelete(true)
}
return (
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
<div className='h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer' onClick={onClickRename}>
<span className='text-gray-700 text-sm'>{t('common.operation.settings')}</span>
</div>
<Divider className="!my-1" />
<div
className='group h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-red-50 rounded-lg cursor-pointer'
onClick={onClickDelete}
>
<span className={cn('text-gray-700 text-sm', 'group-hover:text-red-500')}>
{t('common.operation.delete')}
</span>
</div>
</div>
)
}
useEffect(() => {
setTags(dataset.tags)
}, [dataset])
return (
<>
<Link href={`/datasets/${dataset.id}/documents`} className={cn(style.listItem)} data-disable-nprogress={true}>
<div className={style.listItemTitle}>
<AppIcon size='small' className={cn(!dataset.embedding_available && style.unavailable)} />
<div className={cn(style.listItemHeading, !dataset.embedding_available && style.unavailable)}>
<div className={style.listItemHeadingContent}>
{dataset.name}
<Link
href={`/datasets/${dataset.id}/documents`}
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
data-disable-nprogress={true}
>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className={cn(
'shrink-0 flex items-center justify-center p-2.5 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]',
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
)}>
<Folder className='w-5 h-5 text-[#444CE7]' />
</div>
<div className='grow w-0 py-[1px]'>
<div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
<div className={cn('truncate', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} title={dataset.name}>{dataset.name}</div>
{!dataset.embedding_available && (
<Tooltip
selector={`dataset-tag-${dataset.id}`}
htmlContent={t('dataset.unavailableTip')}
>
<span className='shrink-0 inline-flex w-max ml-1 px-1 border boder-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
</Tooltip>
)}
</div>
<div className='flex items-center mt-[1px] text-xs leading-[18px] text-gray-500'>
<div
className={cn('truncate', (!dataset.embedding_available || !dataset.document_count) && 'opacity-50')}
title={`${dataset.document_count}${t('dataset.documentCount')} · ${Math.round(dataset.word_count / 1000)}${t('dataset.wordCount')} · ${dataset.app_count}${t('dataset.appCount')}`}
>
<span>{dataset.document_count}{t('dataset.documentCount')}</span>
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
<span>{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}</span>
<span className='shrink-0 mx-0.5 w-1 text-gray-400'>·</span>
<span>{dataset.app_count}{t('dataset.appCount')}</span>
</div>
</div>
</div>
{!dataset.embedding_available && (
<Tooltip
selector={`dataset-tag-${dataset.id}`}
htmlContent={t('dataset.unavailableTip')}
>
<span className='px-1 border boder-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
</Tooltip>
</div>
<div
className={cn(
'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500 group-hover:line-clamp-2 group-hover:max-h-[36px]',
tags.length ? 'line-clamp-2' : 'line-clamp-4',
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
)}
<span className={style.deleteDatasetIcon} onClick={onDeleteClick} />
title={dataset.description}>
{dataset.description}
</div>
<div className={cn(style.listItemDescription, !dataset.embedding_available && style.unavailable)}>{dataset.description}</div>
<div className={cn(style.listItemFooter, style.datasetCardFooter, !dataset.embedding_available && style.unavailable)}>
<span className={style.listItemStats}>
<span className={cn(style.listItemFooterIcon, style.docIcon)} />
{dataset.document_count}{t('dataset.documentCount')}
</span>
<span className={style.listItemStats}>
<span className={cn(style.listItemFooterIcon, style.textIcon)} />
{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}
</span>
<span className={style.listItemStats}>
<span className={cn(style.listItemFooterIcon, style.applicationIcon)} />
{dataset.app_count}{t('dataset.appCount')}
</span>
<div className={cn(
'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
tags.length ? 'flex' : '!hidden group-hover:!flex',
)}>
<div className={cn('grow flex items-center gap-1 w-0', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}>
<div className={cn(
'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
tags.length ? '!block' : '!hidden',
)}>
<TagSelector
position='bl'
type='knowledge'
targetID={dataset.id}
value={tags.map(tag => tag.id)}
selectedTags={tags}
onCacheUpdate={setTags}
onChange={onSuccess}
/>
</div>
</div>
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/>
<div className='!hidden group-hover:!flex shrink-0'>
<CustomPopover
htmlContent={<Operations />}
position="br"
trigger="click"
btnElement={
<div
className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
>
<DotsHorizontal className='w-4 h-4 text-gray-700' />
</div>
}
btnClassName={open =>
cn(
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
)
}
className={'!w-[128px] h-fit !z-20'}
/>
</div>
</div>
{showConfirmDelete && (
<Confirm
title={t('dataset.deleteDatasetConfirmTitle')}
content={t('dataset.deleteDatasetConfirmContent')}
isShow={showConfirmDelete}
onClose={() => setShowConfirmDelete(false)}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</Link>
{showRenameModal && (
<RenameDatasetModal
show={showRenameModal}
dataset={dataset}
onClose={() => setShowRenameModal(false)}
onSuccess={onSuccess}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('dataset.deleteDatasetConfirmTitle')}
content={t('dataset.deleteDatasetConfirmContent')}
isShow={showConfirmDelete}
onClose={() => setShowConfirmDelete(false)}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</>
)
}

View File

@@ -10,21 +10,46 @@ import type { DataSetListResponse } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
import { useAppContext } from '@/context/app-context'
const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
if (!pageIndex || previousPageData.has_more)
return { url: 'datasets', params: { page: pageIndex + 1, limit: 30 } }
const getKey = (
pageIndex: number,
previousPageData: DataSetListResponse,
tags: string[],
keyword: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = {
url: 'datasets',
params: {
page: pageIndex + 1,
limit: 30,
},
}
if (tags.length)
params.params.tag_ids = tags
if (keyword)
params.params.keyword = keyword
return params
}
return null
}
type Props = {
containerRef: React.RefObject<HTMLDivElement>
tags: string[]
keywords: string
}
const Datasets = ({
containerRef,
tags,
keywords,
}: Props) => {
const { isCurrentWorkspaceManager } = useAppContext()
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchDatasets, { revalidateFirstPage: false, revalidateAll: true })
const { data, isLoading, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords),
fetchDatasets,
{ revalidateFirstPage: false, revalidateAll: true },
)
const loadingStateRef = useRef(false)
const anchorRef = useRef<HTMLAnchorElement>(null)
@@ -53,7 +78,7 @@ const Datasets = ({
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{ isCurrentWorkspaceManager && <NewDatasetCard ref={anchorRef} /> }
{data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onDelete={mutate} />),
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
))}
</nav>
)

View File

@@ -1,27 +1,22 @@
'use client'
import { forwardRef } from 'react'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import style from '../list.module.css'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
const CreateAppCard = forwardRef<HTMLAnchorElement>((_, ref) => {
const { t } = useTranslation()
return (
<Link ref={ref} className={classNames(style.listItem, style.newItemCard)} href='/datasets/create'>
<div className={style.listItemTitle}>
<span className={style.newItemIcon}>
<span className={classNames(style.newItemIconImage, style.newItemIconAdd)} />
</span>
<div className={classNames(style.listItemHeading, style.newItemCardHeading)}>
{t('dataset.createDataset')}
<a ref={ref} className='group flex flex-col col-span-1 bg-gray-200 border-[0.5px] border-black/5 rounded-xl min-h-[160px] transition-all duration-200 ease-in-out cursor-pointer hover:bg-white hover:shadow-lg' href='/datasets/create'>
<div className='shrnik-0 flex items-center p-4 pb-3'>
<div className='w-10 h-10 flex items-center justify-center border border-gray-200 bg-gray-100 rounded-lg'>
<Plus className='w-4 h-4 text-gray-500'/>
</div>
<div className='ml-3 text-sm font-semibold leading-5 text-gray-800 group-hover:text-primary-600'>{t('dataset.createDataset')}</div>
</div>
<div className={style.listItemDescription}>{t('dataset.createDatasetIntro')}</div>
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
</Link>
<div className='mb-1 px-4 text-xs leading-normal text-gray-500 line-clamp-4'>{t('dataset.createDatasetIntro')}</div>
</a>
)
})

View File

@@ -1,6 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.2 1.5H2.3C2.01997 1.5 1.87996 1.5 1.773 1.5545C1.67892 1.60243 1.60243 1.67892 1.5545 1.773C1.5 1.87996 1.5 2.01997 1.5 2.3V4.2C1.5 4.48003 1.5 4.62004 1.5545 4.727C1.60243 4.82108 1.67892 4.89757 1.773 4.9455C1.87996 5 2.01997 5 2.3 5H4.2C4.48003 5 4.62004 5 4.727 4.9455C4.82108 4.89757 4.89757 4.82108 4.9455 4.727C5 4.62004 5 4.48003 5 4.2V2.3C5 2.01997 5 1.87996 4.9455 1.773C4.89757 1.67892 4.82108 1.60243 4.727 1.5545C4.62004 1.5 4.48003 1.5 4.2 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.7 1.5H7.8C7.51997 1.5 7.37996 1.5 7.273 1.5545C7.17892 1.60243 7.10243 1.67892 7.0545 1.773C7 1.87996 7 2.01997 7 2.3V4.2C7 4.48003 7 4.62004 7.0545 4.727C7.10243 4.82108 7.17892 4.89757 7.273 4.9455C7.37996 5 7.51997 5 7.8 5H9.7C9.98003 5 10.12 5 10.227 4.9455C10.3211 4.89757 10.3976 4.82108 10.4455 4.727C10.5 4.62004 10.5 4.48003 10.5 4.2V2.3C10.5 2.01997 10.5 1.87996 10.4455 1.773C10.3976 1.67892 10.3211 1.60243 10.227 1.5545C10.12 1.5 9.98003 1.5 9.7 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.7 7H7.8C7.51997 7 7.37996 7 7.273 7.0545C7.17892 7.10243 7.10243 7.17892 7.0545 7.273C7 7.37996 7 7.51997 7 7.8V9.7C7 9.98003 7 10.12 7.0545 10.227C7.10243 10.3211 7.17892 10.3976 7.273 10.4455C7.37996 10.5 7.51997 10.5 7.8 10.5H9.7C9.98003 10.5 10.12 10.5 10.227 10.4455C10.3211 10.3976 10.3976 10.3211 10.4455 10.227C10.5 10.12 10.5 9.98003 10.5 9.7V7.8C10.5 7.51997 10.5 7.37996 10.4455 7.273C10.3976 7.17892 10.3211 7.10243 10.227 7.0545C10.12 7 9.98003 7 9.7 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.2 7H2.3C2.01997 7 1.87996 7 1.773 7.0545C1.67892 7.10243 1.60243 7.17892 1.5545 7.273C1.5 7.37996 1.5 7.51997 1.5 7.8V9.7C1.5 9.98003 1.5 10.12 1.5545 10.227C1.60243 10.3211 1.67892 10.3976 1.773 10.4455C1.87996 10.5 2.01997 10.5 2.3 10.5H4.2C4.48003 10.5 4.62004 10.5 4.727 10.4455C4.82108 10.3976 4.89757 10.3211 4.9455 10.227C5 10.12 5 9.98003 5 9.7V7.8C5 7.51997 5 7.37996 4.9455 7.273C4.89757 7.17892 4.82108 7.10243 4.727 7.0545C4.62004 7 4.48003 7 4.2 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,3 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 457 B

View File

@@ -1,3 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 457 B

View File

@@ -159,18 +159,6 @@
background-image: url("./apps/assets/completion-solid.svg");
}
.docIcon {
background-image: url("./datasets/assets/doc.svg");
}
.textIcon {
background-image: url("./datasets/assets/text.svg");
}
.applicationIcon {
background-image: url("./datasets/assets/application.svg");
}
.newItemCardHeading {
@apply transition-colors duration-200 ease-in-out;
}

View File

@@ -0,0 +1,10 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon" clip-path="url(#clip0_17795_9693)">
<path id="Icon_2" d="M4.66699 4.6665H4.67283M1.16699 3.03317L1.16699 5.6433C1.16699 5.92866 1.16699 6.07134 1.19923 6.20561C1.22781 6.32465 1.27495 6.43845 1.33891 6.54284C1.41106 6.66057 1.51195 6.76146 1.71373 6.96324L6.18709 11.4366C6.88012 12.1296 7.22664 12.4761 7.62621 12.606C7.97769 12.7202 8.35629 12.7202 8.70777 12.606C9.10735 12.4761 9.45386 12.1296 10.1469 11.4366L11.4371 10.1464C12.1301 9.45337 12.4766 9.10686 12.6065 8.70728C12.7207 8.35581 12.7207 7.9772 12.6065 7.62572C12.4766 7.22615 12.1301 6.87963 11.4371 6.1866L6.96372 1.71324C6.76195 1.51146 6.66106 1.41057 6.54332 1.33842C6.43894 1.27446 6.32514 1.22732 6.20609 1.19874C6.07183 1.1665 5.92915 1.1665 5.64379 1.1665L3.03366 1.1665C2.38026 1.1665 2.05357 1.1665 1.804 1.29366C1.58448 1.40552 1.406 1.58399 1.29415 1.80352C1.16699 2.05308 1.16699 2.37978 1.16699 3.03317ZM4.95866 4.6665C4.95866 4.82759 4.82808 4.95817 4.66699 4.95817C4.50591 4.95817 4.37533 4.82759 4.37533 4.6665C4.37533 4.50542 4.50591 4.37484 4.66699 4.37484C4.82808 4.37484 4.95866 4.50542 4.95866 4.6665Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_17795_9693">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="tag-03">
<path id="Icon" d="M14 7.3335L8.93726 2.27075C8.59135 1.92485 8.4184 1.7519 8.21657 1.62822C8.03762 1.51856 7.84254 1.43775 7.63846 1.38876C7.40829 1.3335 7.16369 1.3335 6.67452 1.3335L4 1.3335M2 5.80016L2 7.11651C2 7.44263 2 7.60569 2.03684 7.75914C2.0695 7.89519 2.12337 8.02525 2.19648 8.14454C2.27894 8.2791 2.39424 8.3944 2.62484 8.625L7.82484 13.825C8.35286 14.353 8.61687 14.617 8.92131 14.716C9.1891 14.803 9.47757 14.803 9.74536 14.716C10.0498 14.617 10.3138 14.353 10.8418 13.825L12.4915 12.1753C13.0195 11.6473 13.2835 11.3833 13.3825 11.0789C13.4695 10.8111 13.4695 10.5226 13.3825 10.2548C13.2835 9.95037 13.0195 9.68636 12.4915 9.15834L7.62484 4.29167C7.39424 4.06107 7.27894 3.94577 7.14438 3.86331C7.02508 3.7902 6.89502 3.73633 6.75898 3.70367C6.60553 3.66683 6.44247 3.66683 6.11634 3.66683H4.13333C3.3866 3.66683 3.01323 3.66683 2.72801 3.81215C2.47713 3.93999 2.27316 4.14396 2.14532 4.39484C2 4.68006 2 5.05343 2 5.80016Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,66 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon",
"clip-path": "url(#clip0_17795_9693)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon_2",
"d": "M4.66699 4.6665H4.67283M1.16699 3.03317L1.16699 5.6433C1.16699 5.92866 1.16699 6.07134 1.19923 6.20561C1.22781 6.32465 1.27495 6.43845 1.33891 6.54284C1.41106 6.66057 1.51195 6.76146 1.71373 6.96324L6.18709 11.4366C6.88012 12.1296 7.22664 12.4761 7.62621 12.606C7.97769 12.7202 8.35629 12.7202 8.70777 12.606C9.10735 12.4761 9.45386 12.1296 10.1469 11.4366L11.4371 10.1464C12.1301 9.45337 12.4766 9.10686 12.6065 8.70728C12.7207 8.35581 12.7207 7.9772 12.6065 7.62572C12.4766 7.22615 12.1301 6.87963 11.4371 6.1866L6.96372 1.71324C6.76195 1.51146 6.66106 1.41057 6.54332 1.33842C6.43894 1.27446 6.32514 1.22732 6.20609 1.19874C6.07183 1.1665 5.92915 1.1665 5.64379 1.1665L3.03366 1.1665C2.38026 1.1665 2.05357 1.1665 1.804 1.29366C1.58448 1.40552 1.406 1.58399 1.29415 1.80352C1.16699 2.05308 1.16699 2.37978 1.16699 3.03317ZM4.95866 4.6665C4.95866 4.82759 4.82808 4.95817 4.66699 4.95817C4.50591 4.95817 4.37533 4.82759 4.37533 4.6665C4.37533 4.50542 4.50591 4.37484 4.66699 4.37484C4.82808 4.37484 4.95866 4.50542 4.95866 4.6665Z",
"stroke": "currentColor",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_17795_9693"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "14",
"height": "14",
"fill": "white"
},
"children": []
}
]
}
]
}
]
},
"name": "Tag01"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Tag01.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Tag01'
export default Icon

View File

@@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "tag-03"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M14 7.3335L8.93726 2.27075C8.59135 1.92485 8.4184 1.7519 8.21657 1.62822C8.03762 1.51856 7.84254 1.43775 7.63846 1.38876C7.40829 1.3335 7.16369 1.3335 6.67452 1.3335L4 1.3335M2 5.80016L2 7.11651C2 7.44263 2 7.60569 2.03684 7.75914C2.0695 7.89519 2.12337 8.02525 2.19648 8.14454C2.27894 8.2791 2.39424 8.3944 2.62484 8.625L7.82484 13.825C8.35286 14.353 8.61687 14.617 8.92131 14.716C9.1891 14.803 9.47757 14.803 9.74536 14.716C10.0498 14.617 10.3138 14.353 10.8418 13.825L12.4915 12.1753C13.0195 11.6473 13.2835 11.3833 13.3825 11.0789C13.4695 10.8111 13.4695 10.5226 13.3825 10.2548C13.2835 9.95037 13.0195 9.68636 12.4915 9.15834L7.62484 4.29167C7.39424 4.06107 7.27894 3.94577 7.14438 3.86331C7.02508 3.7902 6.89502 3.73633 6.75898 3.70367C6.60553 3.66683 6.44247 3.66683 6.11634 3.66683H4.13333C3.3866 3.66683 3.01323 3.66683 2.72801 3.81215C2.47713 3.93999 2.27316 4.14396 2.14532 4.39484C2 4.68006 2 5.05343 2 5.80016Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "Tag03"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Tag03.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Tag03'
export default Icon

View File

@@ -1,3 +1,5 @@
export { default as CoinsStacked01 } from './CoinsStacked01'
export { default as GoldCoin } from './GoldCoin'
export { default as ReceiptList } from './ReceiptList'
export { default as Tag01 } from './Tag01'
export { default as Tag03 } from './Tag03'

View File

@@ -13,7 +13,7 @@ type IPopover = {
htmlContent: React.ReactElement<HtmlContentProps>
popupClassName?: string
trigger?: 'click' | 'hover'
position?: 'bottom' | 'br'
position?: 'bottom' | 'br' | 'bl'
btnElement?: string | React.ReactNode
btnClassName?: string | ((open: boolean) => string)
manualClose?: boolean
@@ -71,7 +71,13 @@ export default function CustomPopover({
</Popover.Button>
<Transition as={Fragment}>
<Popover.Panel
className={`${s.popupPanel} ${position === 'br' ? 'right-0' : 'translate-x-1/2 left-1/2'} ${className}`}
className={cn(
s.popupPanel,
position === 'bottom' && '-translate-x-1/2 left-1/2',
position === 'bl' && 'left-0',
position === 'br' && 'right-0',
className,
)}
{...(trigger !== 'hover'
? {}
: {

View File

@@ -0,0 +1,85 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useReducer } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import useSWR from 'swr'
import s from './style.module.css'
import Divider from '@/app/components/base/divider'
import { getErrorDocs, retryErrorDocs } from '@/service/datasets'
import type { IndexingStatusResponse } from '@/models/datasets'
const WarningIcon = () =>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000 /svg">
<path fillRule="evenodd" clipRule="evenodd" d="M6.40616 0.834307C6.14751 0.719294 5.85222 0.719294 5.59356 0.834307C5.3938 0.923133 5.26403 1.07959 5.17373 1.20708C5.08495 1.33242 4.9899 1.49664 4.88536 1.67723L0.751783 8.81705C0.646828 8.9983 0.551451 9.16302 0.486781 9.3028C0.421056 9.44487 0.349754 9.63584 0.372478 9.85381C0.401884 10.1359 0.549654 10.3922 0.779012 10.5589C0.956259 10.6878 1.15726 10.7218 1.31314 10.7361C1.46651 10.7501 1.65684 10.7501 1.86628 10.7501H10.1334C10.3429 10.7501 10.5332 10.7501 10.6866 10.7361C10.8425 10.7218 11.0435 10.6878 11.2207 10.5589C11.4501 10.3922 11.5978 10.1359 11.6272 9.85381C11.65 9.63584 11.5787 9.44487 11.5129 9.3028C11.4483 9.16303 11.3529 8.99833 11.248 8.81709L7.11436 1.67722C7.00983 1.49663 6.91477 1.33242 6.82599 1.20708C6.73569 1.07959 6.60593 0.923133 6.40616 0.834307ZM6.49988 4.50012C6.49988 4.22398 6.27602 4.00012 5.99988 4.00012C5.72374 4.00012 5.49988 4.22398 5.49988 4.50012V6.50012C5.49988 6.77626 5.72374 7.00012 5.99988 7.00012C6.27602 7.00012 6.49988 6.77626 6.49988 6.50012V4.50012ZM5.99988 8.00012C5.72374 8.00012 5.49988 8.22398 5.49988 8.50012C5.49988 8.77626 5.72374 9.00012 5.99988 9.00012H6.00488C6.28102 9.00012 6.50488 8.77626 6.50488 8.50012C6.50488 8.22398 6.28102 8.00012 6.00488 8.00012H5.99988Z" fill="#F79009" />
</svg>
type Props = {
datasetId: string
}
type IIndexState = {
value: string
}
type ActionType = 'retry' | 'success' | 'error'
type IAction = {
type: ActionType
}
const indexStateReducer = (state: IIndexState, action: IAction) => {
const actionMap = {
retry: 'retry',
success: 'success',
error: 'error',
}
return {
...state,
value: actionMap[action.type] || state.value,
}
}
const RetryButton: FC<Props> = ({ datasetId }) => {
const { t } = useTranslation()
const [indexState, dispatch] = useReducer(indexStateReducer, { value: 'success' })
const { data: errorDocs } = useSWR({ datasetId }, getErrorDocs)
const onRetryErrorDocs = async () => {
dispatch({ type: 'retry' })
const document_ids = errorDocs?.data.map((doc: IndexingStatusResponse) => doc.id) || []
const res = await retryErrorDocs({ datasetId, document_ids })
if (res.result === 'success')
dispatch({ type: 'success' })
else
dispatch({ type: 'error' })
}
useEffect(() => {
if (errorDocs?.total === 0)
dispatch({ type: 'success' })
else
dispatch({ type: 'error' })
}, [errorDocs?.total])
if (indexState.value === 'success')
return null
return (
<div className={classNames('inline-flex justify-center items-center gap-2', s.retryBtn)}>
<WarningIcon />
<span className='flex shrink-0 text-sm text-gray-500'>
{errorDocs?.total} {t('dataset.docsFailedNotice')}
</span>
<Divider type='vertical' className='!h-4' />
<span
className={classNames(
'text-primary-600 font-semibold text-sm cursor-pointer',
indexState.value === 'retry' && '!text-gray-500 !cursor-not-allowed',
)}
onClick={indexState.value === 'error' ? onRetryErrorDocs : undefined}
>
{t('dataset.retry')}
</span>
</div>
)
}
export default RetryButton

View File

@@ -0,0 +1,4 @@
.retryBtn {
@apply inline-flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base;
@apply border-solid border border-gray-200 text-gray-500 hover:bg-white hover:shadow-sm hover:border-gray-300;
}

View File

@@ -0,0 +1,66 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
type SearchInputProps = {
placeholder?: string
className?: string
value: string
onChange: (v: string) => void
white?: boolean
}
const SearchInput: FC<SearchInputProps> = ({
placeholder,
className,
value,
onChange,
white,
}) => {
const { t } = useTranslation()
const [focus, setFocus] = useState<boolean>(false)
return (
<div className={cn(
'group flex items-center px-2 h-8 rounded-lg bg-gray-200 hover:bg-gray-300 border border-transparent overflow-hidden',
focus && '!bg-white hover:bg-white shawdow-xs !border-gray-300',
!focus && value && 'hover:!bg-gray-200 hover:!shawdow-xs hover:!border-black/5',
white && '!bg-white hover:!bg-white shawdow-xs !border-gray-300 hover:!border-gray-300',
className,
)}>
<div className="pointer-events-none shrink-0 flex items-center mr-1.5 justify-center w-4 h-4">
<SearchLg className="h-3.5 w-3.5 text-gray-500" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className={cn(
'grow block h-[18px] bg-gray-200 rounded-md border-0 text-gray-700 text-[13px] placeholder:text-gray-500 appearance-none outline-none group-hover:bg-gray-300 caret-blue-600',
focus && '!bg-white hover:bg-white group-hover:bg-white placeholder:!text-gray-400',
!focus && value && 'hover:!bg-gray-200 group-hover:!bg-gray-200',
white && '!bg-white hover:!bg-white group-hover:!bg-white placeholder:!text-gray-400',
)}
placeholder={placeholder || t('common.operation.search')!}
value={value}
onChange={(e) => {
onChange(e.target.value)
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
autoComplete="off"
/>
{value && (
<div
className='shrink-0 flex items-center justify-center w-4 h-4 cursor-pointer group/clear'
onClick={() => onChange('')}
>
<XCircle className='w-3.5 h-3.5 text-gray-400 group-hover/clear:text-gray-600' />
</div>
)}
</div>
)
}
export default SearchInput

View File

@@ -0,0 +1,6 @@
export type Tag = {
id: string
name: string
type: string
binding_count: number
}

View File

@@ -0,0 +1,142 @@
import type { FC } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDebounceFn, useMount } from 'ahooks'
import cn from 'classnames'
import { useStore as useTagStore } from './store'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import SearchInput from '@/app/components/base/search-input'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import type { Tag } from '@/app/components/base/tag-management/constant'
import { fetchTagList } from '@/service/tag'
type TagFilterProps = {
type: 'knowledge' | 'app'
value: string[]
onChange: (v: string[]) => void
}
const TagFilter: FC<TagFilterProps> = ({
type,
value,
onChange,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const tagList = useTagStore(s => s.tagList)
const setTagList = useTagStore(s => s.setTagList)
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
const handleKeywordsChange = (value: string) => {
setKeywords(value)
handleSearch()
}
const filteredTagList = useMemo(() => {
return tagList.filter(tag => tag.type === type && tag.name.includes(searchKeywords))
}, [type, tagList, searchKeywords])
const currentTag = useMemo(() => {
return tagList.find(tag => tag.id === value[0])
}, [value, tagList])
const selectTag = (tag: Tag) => {
if (value.includes(tag.id))
onChange(value.filter(v => v !== tag.id))
else
onChange([...value, tag.id])
}
useMount(() => {
fetchTagList(type).then((res) => {
setTagList(res)
})
})
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
className='block'
>
<div className={cn(
'flex items-center gap-1 px-2 h-8 rounded-lg border-[0.5px] border-transparent bg-gray-200 cursor-pointer hover:bg-gray-300',
open && !value.length && '!bg-gray-300 hover:bg-gray-300',
!open && !!value.length && '!bg-white/80 shadow-xs !border-black/5 hover:!bg-gray-200',
open && !!value.length && '!bg-gray-200 !border-black/5 shadow-xs hover:!bg-gray-200',
)}>
<div className='p-[1px]'>
<Tag01 className='h-3.5 w-3.5 text-gray-700' />
</div>
<div className='text-[13px] leading-[18px] text-gray-700'>
{!value.length && t('common.tag.placeholder')}
{!!value.length && currentTag?.name}
</div>
{value.length > 1 && (
<div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div>
)}
{!value.length && (
<div className='p-[1px]'>
<ChevronDown className='h-3.5 w-3.5 text-gray-700'/>
</div>
)}
{!!value.length && (
<div className='p-[1px] cursor-pointer group/clear' onClick={(e) => {
e.stopPropagation()
onChange([])
}}>
<XCircle className='h-3.5 w-3.5 text-gray-400 group-hover/clear:text-gray-600'/>
</div>
)}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='relative w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
<div className='p-2 border-b-[0.5px] border-black/5'>
<SearchInput white value={keywords} onChange={handleKeywordsChange} />
</div>
<div className='p-1'>
{filteredTagList.map(tag => (
<div
key={tag.id}
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
onClick={() => selectTag(tag)}
>
<div title={tag.name} className='grow text-sm text-gray-700 leading-5 truncate'>{tag.name}</div>
{value.includes(tag.id) && <Check className='shrink-0 w-4 h-4 text-primary-600'/>}
</div>
))}
{!filteredTagList.length && (
<div className='p-3 flex flex-col items-center gap-1'>
<Tag03 className='h-6 w-6 text-gray-300' />
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
</div>
)}
</div>
</div>
</PortalToFollowElemContent>
</div>
</PortalToFollowElem>
)
}
export default TagFilter

View File

@@ -0,0 +1,93 @@
'use client'
import { useEffect, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { useStore as useTagStore } from './store'
import TagItemEditor from './tag-item-editor'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
import {
createTag,
fetchTagList,
} from '@/service/tag'
type TagManagementModalProps = {
type: 'knowledge' | 'app'
show: boolean
}
const TagManagementModal = ({ show, type }: TagManagementModalProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const tagList = useTagStore(s => s.tagList)
const setTagList = useTagStore(s => s.setTagList)
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
const getTagList = async (type: 'knowledge' | 'app') => {
const res = await fetchTagList(type)
setTagList(res)
}
const [pending, setPending] = useState<Boolean>(false)
const [name, setName] = useState<string>('')
const createNewTag = async () => {
if (!name)
return
if (pending)
return
try {
setPending(true)
const newTag = await createTag(name, type)
notify({ type: 'success', message: t('common.tag.created') })
setTagList([
newTag,
...tagList,
])
setName('')
setPending(false)
}
catch (e: any) {
notify({ type: 'error', message: t('common.tag.failed') })
setPending(false)
}
}
useEffect(() => {
getTagList(type)
}, [type])
return (
<Modal
wrapperClassName='!z-[1020]'
className='px-8 py-6 !max-w-[600px] !w-[600px] rounded-xl'
isShow={show}
onClose={() => setShowTagManagementModal(false)}
>
<div className='relative pb-2 text-xl font-semibold leading-[30px] text-gray-900'>{t('common.tag.manageTags')}</div>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={() => setShowTagManagementModal(false)}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
<div className='mt-3 flex flex-wrap gap-2'>
<input
className='shrink-0 w-[100px] px-2 py-1 rounded-lg border border-dashed border-gray-200 text-sm leading-5 text-gray-700 outline-none appearance-none placeholder:text-gray-300 caret-primary-600 focus:border-solid'
placeholder={t('common.tag.addNew') || ''}
autoFocus
value={name}
onChange={e => setName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && createNewTag()}
onBlur={createNewTag}
/>
{tagList.map(tag => (
<TagItemEditor
key={tag.id}
tag={tag}
/>
))}
</div>
</Modal>
)
}
export default TagManagementModal

View File

@@ -0,0 +1,272 @@
import type { FC } from 'react'
import { useMemo, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { useUnmount } from 'ahooks'
import cn from 'classnames'
import { useStore as useTagStore } from './store'
import type { HtmlContentProps } from '@/app/components/base/popover'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
import SearchInput from '@/app/components/base/search-input'
import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
import type { Tag } from '@/app/components/base/tag-management/constant'
import Checkbox from '@/app/components/base/checkbox'
import { bindTag, createTag, fetchTagList, unBindTag } from '@/service/tag'
import { ToastContext } from '@/app/components/base/toast'
type TagSelectorProps = {
targetID: string
isPopover?: boolean
position?: 'bl' | 'br'
type: 'knowledge' | 'app'
value: string[]
selectedTags: Tag[]
onCacheUpdate: (tags: Tag[]) => void
onChange?: () => void
}
type PanelProps = {
onCreate: () => void
} & HtmlContentProps & TagSelectorProps
const Panel = (props: PanelProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { targetID, type, value, selectedTags, onCacheUpdate, onChange, onCreate } = props
const tagList = useTagStore(s => s.tagList)
const setTagList = useTagStore(s => s.setTagList)
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
const [selectedTagIDs, setSelectedTagIDs] = useState<string[]>(value)
const [keywords, setKeywords] = useState('')
const handleKeywordsChange = (value: string) => {
setKeywords(value)
}
const notExisted = useMemo(() => {
return tagList.every(tag => tag.type === type && tag.name !== keywords)
}, [type, tagList, keywords])
const filteredSelectedTagList = useMemo(() => {
return selectedTags.filter(tag => tag.name.includes(keywords))
}, [keywords, selectedTags])
const filteredTagList = useMemo(() => {
return tagList.filter(tag => tag.type === type && !value.includes(tag.id) && tag.name.includes(keywords))
}, [type, tagList, value, keywords])
const [creating, setCreating] = useState<Boolean>(false)
const createNewTag = async () => {
if (!keywords)
return
if (creating)
return
try {
setCreating(true)
const newTag = await createTag(keywords, type)
notify({ type: 'success', message: t('common.tag.created') })
setTagList([
...tagList,
newTag,
])
setCreating(false)
onCreate()
}
catch (e: any) {
notify({ type: 'error', message: t('common.tag.failed') })
setCreating(false)
}
}
const bind = async (tagIDs: string[]) => {
try {
await bindTag(tagIDs, targetID, type)
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
}
catch (e: any) {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
}
const unbind = async (tagID: string) => {
try {
await unBindTag(tagID, targetID, type)
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
}
catch (e: any) {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
}
const selectTag = (tag: Tag) => {
if (selectedTagIDs.includes(tag.id))
setSelectedTagIDs(selectedTagIDs.filter(v => v !== tag.id))
else
setSelectedTagIDs([...selectedTagIDs, tag.id])
}
const valueNotChanged = useMemo(() => {
return value.length === selectedTagIDs.length && value.every(v => selectedTagIDs.includes(v)) && selectedTagIDs.every(v => value.includes(v))
}, [value, selectedTagIDs])
const handleValueChange = () => {
const addTagIDs = selectedTagIDs.filter(v => !value.includes(v))
const removeTagIDs = value.filter(v => !selectedTagIDs.includes(v))
const selectedTags = tagList.filter(tag => selectedTagIDs.includes(tag.id))
onCacheUpdate(selectedTags)
Promise.all([
...(addTagIDs.length ? [bind(addTagIDs)] : []),
...[removeTagIDs.length ? removeTagIDs.map(tagID => unbind(tagID)) : []],
]).finally(() => {
if (onChange)
onChange()
})
}
useUnmount(() => {
if (valueNotChanged)
return
handleValueChange()
})
const onMouseLeave = async () => {
props.onClose?.()
}
return (
<div className='relative w-full bg-white rounded-lg border-[0.5px] border-gray-200' onMouseLeave={onMouseLeave}>
<div className='p-2 border-b-[0.5px] border-black/5'>
<SearchInput placeholder={t('common.tag.selectorPlaceholder') || ''} white value={keywords} onChange={handleKeywordsChange} />
</div>
{keywords && notExisted && (
<div className='p-1'>
<div className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100' onClick={createNewTag}>
<Plus className='h-4 w-4 text-gray-500' />
<div className='grow text-sm text-gray-700 leading-5 truncate'>
{`${t('common.tag.create')} `}
<span className='font-medium'>{`"${keywords}"`}</span>
</div>
</div>
</div>
)}
{keywords && notExisted && filteredTagList.length > 0 && (
<Divider className='!h-[1px] !my-0' />
)}
{(filteredTagList.length > 0 || filteredSelectedTagList.length > 0) && (
<div className='p-1 max-h-[172px] overflow-y-auto'>
{filteredSelectedTagList.map(tag => (
<div
key={tag.id}
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
onClick={() => selectTag(tag)}
>
<Checkbox
className='shrink-0'
checked={selectedTagIDs.includes(tag.id)}
onCheck={() => {}}
/>
<div title={tag.name} className='grow text-sm text-gray-700 leading-5 truncate'>{tag.name}</div>
</div>
))}
{filteredTagList.map(tag => (
<div
key={tag.id}
className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100'
onClick={() => selectTag(tag)}
>
<Checkbox
className='shrink-0'
checked={selectedTagIDs.includes(tag.id)}
onCheck={() => {}}
/>
<div title={tag.name} className='grow text-sm text-gray-700 leading-5 truncate'>{tag.name}</div>
</div>
))}
</div>
)}
{!keywords && !filteredTagList.length && !filteredSelectedTagList.length && (
<div className='p-1'>
<div className='p-3 flex flex-col items-center gap-1'>
<Tag03 className='h-6 w-6 text-gray-300' />
<div className='text-gray-500 text-xs leading-[14px]'>{t('common.tag.noTag')}</div>
</div>
</div>
)}
<Divider className='!h-[1px] !my-0' />
<div className='p-1'>
<div className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100' onClick={() => setShowTagManagementModal(true)}>
<Tag03 className='h-4 w-4 text-gray-500' />
<div className='grow text-sm text-gray-700 leading-5 truncate'>
{t('common.tag.manageTags')}
</div>
</div>
</div>
</div>
)
}
const TagSelector: FC<TagSelectorProps> = ({
targetID,
isPopover = true,
position,
type,
value,
selectedTags,
onCacheUpdate,
onChange,
}) => {
const { t } = useTranslation()
const setTagList = useTagStore(s => s.setTagList)
const getTagList = async () => {
const res = await fetchTagList(type)
setTagList(res)
}
const triggerContent = useMemo(() => {
if (selectedTags?.length)
return selectedTags.map(tag => tag.name).join(', ')
return ''
}, [selectedTags])
const Trigger = () => {
return (
<div className={cn(
'group/tip relative w-full flex items-center gap-1 px-2 py-[7px] rounded-md cursor-pointer hover:bg-gray-100',
)}>
<Tag01 className='shrink-0 w-3 h-3' />
<div className='grow text-xs text-start leading-[18px] font-normal truncate'>
{!triggerContent ? t('common.tag.addTag') : triggerContent}
</div>
<span className='hidden absolute top-[-21px] left-[50%] translate-x-[-50%] px-2 py-[3px] border-[0.5px] border-black/5 rounded-md bg-gray-25 text-gray-700 text-xs font-medium leading-[18px] group-hover/tip:block'>{t('common.tag.editTag')}</span>
</div>
)
}
return (
<>
{isPopover && (
<CustomPopover
htmlContent={
<Panel
type={type}
targetID={targetID}
value={value}
selectedTags={selectedTags}
onCacheUpdate={onCacheUpdate}
onChange={onChange}
onCreate={getTagList}
/>
}
position={position}
trigger="click"
btnElement={<Trigger />}
btnClassName={open =>
cn(
open ? '!bg-gray-100 !text-gray-700' : '!bg-transparent',
'!w-full !p-0 !border-0 !text-gray-500 hover:!bg-gray-100 hover:!text-gray-700',
)
}
popupClassName='!w-full !ring-0'
className={'!w-full h-fit !z-20'}
/>
)}
</>
)
}
export default TagSelector

View File

@@ -0,0 +1,19 @@
import { create } from 'zustand'
import type { Tag } from './constant'
type State = {
tagList: Tag[]
showTagManagementModal: boolean
}
type Action = {
setTagList: (tagList?: Tag[]) => void
setShowTagManagementModal: (showTagManagementModal: boolean) => void
}
export const useStore = create<State & Action>(set => ({
tagList: [],
setTagList: tagList => set(() => ({ tagList })),
showTagManagementModal: false,
setShowTagManagementModal: showTagManagementModal => set(() => ({ showTagManagementModal })),
}))

View File

@@ -0,0 +1,3 @@
.bg {
background: linear-gradient(180deg, rgba(247, 144, 9, 0.05) 0%, rgba(247, 144, 9, 0.00) 24.41%), #F9FAFB;
}

View File

@@ -0,0 +1,147 @@
import type { FC } from 'react'
import { useState } from 'react'
import cn from 'classnames'
import { useDebounceFn } from 'ahooks'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { useStore as useTagStore } from './store'
import TagRemoveModal from './tag-remove-modal'
import { Edit03, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
import type { Tag } from '@/app/components/base/tag-management/constant'
import { ToastContext } from '@/app/components/base/toast'
import {
deleteTag,
updateTag,
} from '@/service/tag'
type TagItemEditorProps = {
tag: Tag
}
const TagItemEditor: FC<TagItemEditorProps> = ({
tag,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const tagList = useTagStore(s => s.tagList)
const setTagList = useTagStore(s => s.setTagList)
const [isEditing, setIsEditing] = useState(false)
const [name, setName] = useState(tag.name)
const editTag = async (tagID: string, name: string) => {
if (name === tag.name) {
setIsEditing(false)
return
}
if (!name) {
notify({ type: 'error', message: 'tag name is empty' })
setName(tag.name)
setIsEditing(false)
return
}
try {
const newList = tagList.map((tag) => {
if (tag.id === tagID) {
return {
...tag,
name,
}
}
return tag
})
setTagList([
...newList,
])
setIsEditing(false)
await updateTag(tagID, name)
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
setName(name)
}
catch (e: any) {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setName(tag.name)
const recoverList = tagList.map((tag) => {
if (tag.id === tagID) {
return {
...tag,
name: tag.name,
}
}
return tag
})
setTagList([
...recoverList,
])
setIsEditing(false)
}
}
const [showRemoveModal, setShowRemoveModal] = useState(false)
const [pending, setPending] = useState<Boolean>(false)
const removeTag = async (tagID: string) => {
if (pending)
return
try {
setPending(true)
await deleteTag(tagID)
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
const newList = tagList.filter(tag => tag.id !== tagID)
setTagList([
...newList,
])
setPending(false)
}
catch (e: any) {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setPending(false)
}
}
const { run: handleRemove } = useDebounceFn(() => {
removeTag(tag.id)
}, { wait: 200 })
return (
<>
<div className={cn('shrink-0 flex items-center gap-0.5 pr-1 pl-2 py-1 rounded-lg border border-gray-200 text-sm leading-5 text-gray-700')}>
{!isEditing && (
<>
<div className='text-sm leading-5 text-gray-700'>
{tag.name}
</div>
<div className='shrink-0 px-1 text-sm leading-4.5 text-gray-500 font-medium'>{tag.binding_count}</div>
<div className='group/edit shrink-0 p-1 rounded-md cursor-pointer hover:bg-black/5' onClick={() => setIsEditing(true)}>
<Edit03 className='w-3 h-3 text-gray-500 group-hover/edit:text-gray-800' />
</div>
<div className='group/remove shrink-0 p-1 rounded-md cursor-pointer hover:bg-black/5' onClick={() => {
if (tag.binding_count)
setShowRemoveModal(true)
else
handleRemove()
}}>
<Trash03 className='w-3 h-3 text-gray-500 group-hover/remove:text-gray-800' />
</div>
</>
)}
{isEditing && (
<input
className='shrink-0 outline-none appearance-none placeholder:text-gray-300 caret-primary-600'
autoFocus
value={name}
onChange={e => setName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && editTag(tag.id, name)}
onBlur={() => editTag(tag.id, name)}
/>
)}
</div>
<TagRemoveModal
tag={tag}
show={showRemoveModal}
onConfirm={() => {
handleRemove()
setShowRemoveModal(false)
}}
onClose={() => setShowRemoveModal(false)}
/>
</>
)
}
export default TagItemEditor

View File

@@ -0,0 +1,50 @@
'use client'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import s from './style.module.css'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import type { Tag } from '@/app/components/base/tag-management/constant'
type TagRemoveModalProps = {
show: boolean
tag: Tag
onConfirm: () => void
onClose: () => void
}
const TagRemoveModal = ({ show, tag, onConfirm, onClose }: TagRemoveModalProps) => {
const { t } = useTranslation()
return (
<Modal
wrapperClassName='!z-[1020]'
className={cn('p-8 max-w-[480px] w-[480px]', s.bg)}
isShow={show}
onClose={() => {}}
>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
<div className='w-12 h-12 p-3 bg-white rounded-xl border-[0.5px] border-gray-100 shadow-xl'>
<AlertTriangle className='w-6 h-6 text-[rgb(247,144,9)]' />
</div>
<div className='mt-3 text-xl font-semibold leading-[30px] text-gray-900'>
{`${t('common.tag.delete')} `}
<span>{`"${tag.name}"`}</span>
</div>
<div className='my-1 text-gray-500 text-sm leading-5'>
{t('common.tag.deleteTip')}
</div>
<div className='pt-6 flex items-center justify-end'>
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button className='text-sm font-medium border-red-700 border-[0.5px]' type="warning" onClick={onConfirm}>{t('common.operation.delete')}</Button>
</div>
</Modal>
)
}
export default TagRemoveModal

View File

@@ -5,7 +5,6 @@ import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { debounce, groupBy, omit } from 'lodash-es'
// import Link from 'next/link'
import { PlusIcon } from '@heroicons/react/24/solid'
import List from './list'
import s from './style.module.css'
@@ -20,7 +19,7 @@ import { NotionPageSelectorModal } from '@/app/components/base/notion-page-selec
import type { NotionPage } from '@/models/common'
import type { CreateDocumentReq } from '@/models/datasets'
import { DataSourceType } from '@/models/datasets'
import RetryButton from '@/app/components/base/retry-button'
// Custom page count is not currently supported.
const limit = 15
@@ -198,7 +197,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
<p className={s.desc}>{t('datasetDocuments.list.desc')}</p>
</div>
<div className='flex flex-col px-6 py-4 flex-1'>
<div className='flex items-center justify-between flex-wrap gap-y-2 '>
<div className='flex items-center justify-between flex-wrap'>
<Input
showPrefix
wrapperClassName='!w-[200px]'
@@ -206,13 +205,16 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
onChange={debounce(setSearchValue, 500)}
value={searchValue}
/>
{embeddingAvailable && (
<Button type='primary' onClick={routeToDocCreate} className='!h-8 !text-[13px] !shrink-0'>
<PlusIcon className='h-4 w-4 mr-2 stroke-current' />
{isDataSourceNotion && t('datasetDocuments.list.addPages')}
{!isDataSourceNotion && t('datasetDocuments.list.addFile')}
</Button>
)}
<div className='flex gap-2 justify-center items-center !h-8'>
<RetryButton datasetId={datasetId} />
{embeddingAvailable && (
<Button type='primary' onClick={routeToDocCreate} className='!h-8 !text-[13px] !shrink-0'>
<PlusIcon className='h-4 w-4 mr-2 stroke-current' />
{isDataSourceNotion && t('datasetDocuments.list.addPages')}
{!isDataSourceNotion && t('datasetDocuments.list.addFile')}
</Button>
)}
</div>
</div>
{isLoading
? <Loading type='app' />

View File

@@ -332,7 +332,7 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
<td className='w-12'>#</td>
<td>{t('datasetDocuments.list.table.header.fileName')}</td>
<td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td>
<td className='w-24'>{t('datasetDocuments.list.table.header.hitCount')}</td>
<td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td>
<td className='w-44'>
<div className='flex justify-between items-center'>
{t('datasetDocuments.list.table.header.uploadTime')}

View File

@@ -0,0 +1,106 @@
'use client'
import type { MouseEventHandler } from 'react'
import cn from 'classnames'
import { useState } from 'react'
import { BookOpenIcon } from '@heroicons/react/24/outline'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
import type { DataSet } from '@/models/datasets'
import { updateDatasetSetting } from '@/service/datasets'
type RenameDatasetModalProps = {
show: boolean
dataset: DataSet
onSuccess?: () => void
onClose: () => void
}
const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDatasetModalProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [loading, setLoading] = useState(false)
const [name, setName] = useState<string>(dataset.name)
const [description, setDescription] = useState<string>(dataset.description)
const onConfirm: MouseEventHandler = async () => {
if (!name.trim()) {
notify({ type: 'error', message: t('datasetSettings.form.nameError') })
return
}
try {
setLoading(true)
await updateDatasetSetting({
datasetId: dataset.id,
body: {
name,
description,
},
})
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
if (onSuccess)
onSuccess()
onClose()
}
catch (e) {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
finally {
setLoading(false)
}
}
return (
<Modal
wrapperClassName='z-20'
className='px-8 py-6 max-w-[520px] w-[520px] rounded-xl'
isShow={show}
onClose={() => {}}
>
<div className='relative pb-2 text-xl font-medium leading-[30px] text-gray-900'>{t('datasetSettings.title')}</div>
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
<div>
<div className={cn('flex justify-between py-4 flex-wrap items-center')}>
<div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-gray-900'>
{t('datasetSettings.form.name')}
</div>
<input
value={name}
onChange={e => setName(e.target.value)}
className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
placeholder={t('datasetSettings.form.namePlaceholder') || ''}
/>
</div>
<div className={cn('flex justify-between py-4 flex-wrap items-center')}>
<div className='shrink-0 py-2 text-sm font-medium leading-[20px] text-gray-900'>
{t('datasetSettings.form.desc')}
</div>
<div className='w-full'>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
className='block px-3 py-2 w-full h-[88px] rounded-lg bg-gray-100 text-sm outline-none appearance-none resize-none'
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
/>
<a className='mt-2 flex items-center h-[18px] px-3 text-xs text-gray-500 hover:text-primary-600' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'>
<BookOpenIcon className='w-3 h-[18px] mr-1' />
{t('datasetSettings.form.descWrite')}
</a>
</div>
</div>
</div>
<div className='pt-6 flex justify-end'>
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button className='text-sm font-medium' disabled={loading} type="primary" onClick={onConfirm}>{t('common.operation.save')}</Button>
</div>
</Modal>
)
}
export default RenameDatasetModal

View File

@@ -59,7 +59,17 @@ const Form = () => {
const [permission, setPermission] = useState(currentDataset?.permission)
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
const [embeddingModel, setEmbeddingModel] = useState<DefaultModel>(
currentDataset?.embedding_model
? {
provider: currentDataset.embedding_model_provider,
model: currentDataset.embedding_model,
}
: {
provider: '',
model: '',
},
)
const {
modelList: rerankModelList,
defaultModel: rerankDefaultModel,
@@ -101,6 +111,8 @@ const Form = () => {
permission,
indexing_technique: indexMethod,
retrieval_model: postRetrievalConfig,
embedding_model: embeddingModel.model,
embedding_model_provider: embeddingModel.provider,
},
})
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
@@ -189,18 +201,13 @@ const Form = () => {
</div>
<div className='w-[480px]'>
<ModelSelector
readonly
triggerClassName='!h-9 !cursor-not-allowed opacity-60'
defaultModel={{
provider: currentDataset.embedding_model_provider,
model: currentDataset.embedding_model,
}}
triggerClassName=''
defaultModel={embeddingModel}
modelList={embeddingModelList}
onSelect={(model: DefaultModel) => {
setEmbeddingModel(model)
}}
/>
<div className='mt-2 w-full text-xs leading-6 text-gray-500'>
{t('datasetSettings.form.embeddingModelTip')}
<span className='text-[#155eef] cursor-pointer' onClick={() => setShowAccountSettingModal({ payload: 'provider' })}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
</div>
</div>
</div>
)}