| @@ -9,7 +9,7 @@ import { useMembers } from '@/service/use-common' | |||
| import type { AppIconType } from '@/types/app' | |||
| import React, { useCallback, useEffect, useRef, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import PermissionSelector from '../../settings/permission-selector' | |||
| import PermissionSelector from '@/app/components/datasets/settings/permission-selector' | |||
| import Button from '@/app/components/base/button' | |||
| import { RiCloseLine } from '@remixicon/react' | |||
| import Toast from '@/app/components/base/toast' | |||
| @@ -68,11 +68,6 @@ const CreateFromDSLModal = ({ | |||
| setFileContent('') | |||
| } | |||
| // todo: TBD billing plan | |||
| // const plan = useProviderContextSelector(state => state.plan) | |||
| // const enableBilling = useProviderContextSelector(state => state.enableBilling) | |||
| // const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | |||
| const isCreatingRef = useRef(false) | |||
| const { mutateAsync: importDSL } = useImportPipelineDSL() | |||
| @@ -1,37 +0,0 @@ | |||
| import type { RemixiconComponentType } from '@remixicon/react' | |||
| import React from 'react' | |||
| type ItemProps = { | |||
| Icon: RemixiconComponentType | |||
| title: string | |||
| description: string | |||
| onClick: () => void | |||
| } | |||
| const Item = ({ | |||
| Icon, | |||
| title, | |||
| description, | |||
| onClick, | |||
| }: ItemProps) => { | |||
| return ( | |||
| <div | |||
| className='group flex w-[337px] cursor-pointer items-center gap-x-3 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs shadow-shadow-shadow-3 hover:shadow-md hover:shadow-shadow-shadow-5' | |||
| onClick={onClick} | |||
| > | |||
| <div className='flex size-10 shrink-0 items-center justify-center rounded-[10px] border border-dashed border-divider-regular bg-background-section group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'> | |||
| <Icon className='size-5 text-text-quaternary group-hover:text-text-accent' /> | |||
| </div> | |||
| <div className='flex grow flex-col gap-y-0.5 py-px'> | |||
| <div className='system-md-semibold truncate text-text-secondary'> | |||
| {title} | |||
| </div> | |||
| <div className='system-xs-regular text-text-tertiary'> | |||
| {description} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Item) | |||
| @@ -1,20 +1,16 @@ | |||
| import React, { useCallback, useMemo, useState } from 'react' | |||
| import Item from './item' | |||
| import { RiAddCircleFill, RiFileUploadLine } from '@remixicon/react' | |||
| import CreateFromScratchModal from './create-from-scratch-modal' | |||
| import { useRouter, useSearchParams } from 'next/navigation' | |||
| import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-from-dsl-modal' | |||
| import { useProviderContextSelector } from '@/context/provider-context' | |||
| import { RiFileUploadLine } from '@remixicon/react' | |||
| import Divider from '../../base/divider' | |||
| import { useTranslation } from 'react-i18next' | |||
| import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-options/create-from-dsl-modal' | |||
| import { useRouter, useSearchParams } from 'next/navigation' | |||
| import { useResetDatasetList } from '@/service/knowledge/use-dataset' | |||
| const CreateOptions = () => { | |||
| const Footer = () => { | |||
| const { t } = useTranslation() | |||
| const [showCreateModal, setShowCreateModal] = useState(false) | |||
| const [showImportModal, setShowImportModal] = useState(false) | |||
| const onPlanInfoChanged = useProviderContextSelector(state => state.onPlanInfoChanged) | |||
| const searchParams = useSearchParams() | |||
| const { replace } = useRouter() | |||
| const dslUrl = searchParams.get('remoteInstallUrl') || undefined | |||
| @@ -27,14 +23,6 @@ const CreateOptions = () => { | |||
| return undefined | |||
| }, [dslUrl]) | |||
| const openCreateFromScratch = useCallback(() => { | |||
| setShowCreateModal(true) | |||
| }, []) | |||
| const closeCreateFromScratch = useCallback(() => { | |||
| setShowCreateModal(false) | |||
| }, []) | |||
| const openImportFromDSL = useCallback(() => { | |||
| setShowImportModal(true) | |||
| }, []) | |||
| @@ -46,28 +34,20 @@ const CreateOptions = () => { | |||
| }, [dslUrl, replace]) | |||
| const onImportFromDSLSuccess = useCallback(() => { | |||
| onPlanInfoChanged() | |||
| resetDatasetList() | |||
| }, [onPlanInfoChanged, resetDatasetList]) | |||
| }, [resetDatasetList]) | |||
| return ( | |||
| <div className='flex items-center gap-x-3 px-16 py-2'> | |||
| <Item | |||
| Icon={RiAddCircleFill} | |||
| title={t('datasetPipeline.creation.createFromScratch.title')} | |||
| description={t('datasetPipeline.creation.createFromScratch.description')} | |||
| onClick={openCreateFromScratch} | |||
| /> | |||
| <Item | |||
| Icon={RiFileUploadLine} | |||
| title={t('datasetPipeline.creation.ImportDSL.title')} | |||
| description={t('datasetPipeline.creation.ImportDSL.description')} | |||
| <div className='absolute bottom-0 left-0 right-0 z-10 flex flex-col gap-y-4 bg-knowledge-pipeline-creation-footer-bg px-16 pb-6 backdrop-blur-[6px]'> | |||
| <Divider type='horizontal' className='my-0 w-8' /> | |||
| <button | |||
| type='button' | |||
| className='system-md-medium flex items-center gap-x-3 text-text-accent' | |||
| onClick={openImportFromDSL} | |||
| /> | |||
| <CreateFromScratchModal | |||
| show={showCreateModal} | |||
| onClose={closeCreateFromScratch} | |||
| /> | |||
| > | |||
| <RiFileUploadLine className='size-5' /> | |||
| <span>{t('datasetPipeline.creation.importDSL')}</span> | |||
| </button> | |||
| <CreateFromDSLModal | |||
| show={showImportModal} | |||
| onClose={onCloseImportModal} | |||
| @@ -79,4 +59,4 @@ const CreateOptions = () => { | |||
| ) | |||
| } | |||
| export default CreateOptions | |||
| export default React.memo(Footer) | |||
| @@ -1,8 +1,8 @@ | |||
| 'use client' | |||
| import Header from './header' | |||
| import CreateOptions from './create-options' | |||
| import List from './list' | |||
| import Effect from '../../base/effect' | |||
| import Footer from './footer' | |||
| const CreateFromPipeline = () => { | |||
| return ( | |||
| @@ -11,8 +11,8 @@ const CreateFromPipeline = () => { | |||
| > | |||
| <Effect className='left-8 top-[-34px] opacity-20' /> | |||
| <Header /> | |||
| <CreateOptions /> | |||
| <List /> | |||
| <Footer /> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -1,16 +1,15 @@ | |||
| import { usePipelineTemplateList } from '@/service/use-pipeline' | |||
| import TemplateCard from './template-card' | |||
| import CreateCard from './create-card' | |||
| const BuiltInPipelineList = () => { | |||
| const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'built-in' }) | |||
| const list = pipelineList?.pipeline_templates | |||
| if (isLoading || !list) | |||
| return null | |||
| const list = pipelineList?.pipeline_templates || [] | |||
| return ( | |||
| <div className='grid grid-cols-1 gap-3 py-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'> | |||
| {list.map((pipeline, index) => ( | |||
| <div className='grid grid-cols-1 gap-3 py-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'> | |||
| <CreateCard /> | |||
| {!isLoading && list.map((pipeline, index) => ( | |||
| <TemplateCard | |||
| key={index} | |||
| type='built-in' | |||
| @@ -0,0 +1,43 @@ | |||
| import React, { useCallback, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiAddCircleLine } from '@remixicon/react' | |||
| import CreateFromScratchModal from '../create-options/create-from-scratch-modal' | |||
| const CreateCard = () => { | |||
| const { t } = useTranslation() | |||
| const [showCreateModal, setShowCreateModal] = useState(false) | |||
| const openCreateFromScratch = useCallback(() => { | |||
| setShowCreateModal(true) | |||
| }, []) | |||
| const closeCreateFromScratch = useCallback(() => { | |||
| setShowCreateModal(false) | |||
| }, []) | |||
| return ( | |||
| <div | |||
| className='group relative flex h-[132px] cursor-pointer flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-3 shadow-xs shadow-shadow-shadow-3' | |||
| onClick={openCreateFromScratch} | |||
| > | |||
| <div className='flex items-center gap-x-3 p-4 pb-2'> | |||
| <div className='flex size-10 shrink-0 items-center justify-center rounded-[10px] border border-dashed border-divider-regular bg-background-section group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'> | |||
| <RiAddCircleLine className='size-5 text-text-quaternary group-hover:text-text-accent' /> | |||
| </div> | |||
| <div className='system-md-semibold truncate text-text-primary'> | |||
| {t('datasetPipeline.creation.createFromScratch.title')} | |||
| </div> | |||
| </div> | |||
| <p className='system-xs-regular line-clamp-3 px-4 py-1 text-text-tertiary'> | |||
| {t('datasetPipeline.creation.createFromScratch.description')} | |||
| </p> | |||
| <CreateFromScratchModal | |||
| show={showCreateModal} | |||
| onClose={closeCreateFromScratch} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(CreateCard) | |||
| @@ -1,23 +1,28 @@ | |||
| import TemplateCard from './template-card' | |||
| import { usePipelineTemplateList } from '@/service/use-pipeline' | |||
| import { useTranslation } from 'react-i18next' | |||
| const CustomizedList = () => { | |||
| const { t } = useTranslation() | |||
| const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'customized' }) | |||
| const list = pipelineList?.pipeline_templates | |||
| const list = pipelineList?.pipeline_templates || [] | |||
| if (isLoading || !list) | |||
| if (isLoading || list.length === 0) | |||
| return null | |||
| return ( | |||
| <div className='grid grid-cols-1 gap-3 py-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'> | |||
| {list.map((pipeline, index) => ( | |||
| <TemplateCard | |||
| key={index} | |||
| type='customized' | |||
| pipeline={pipeline} | |||
| /> | |||
| ))} | |||
| </div> | |||
| <> | |||
| <div className='system-sm-semibold-uppercase pt-2 text-text-tertiary'>{t('datasetPipeline.templates.customized')}</div> | |||
| <div className='grid grid-cols-1 gap-3 py-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'> | |||
| {list.map((pipeline, index) => ( | |||
| <TemplateCard | |||
| key={index} | |||
| type='customized' | |||
| pipeline={pipeline} | |||
| /> | |||
| ))} | |||
| </div> | |||
| </> | |||
| ) | |||
| } | |||
| @@ -1,39 +1,11 @@ | |||
| import { useCallback, useMemo, useState } from 'react' | |||
| import Tab from './tab' | |||
| import BuiltInPipelineList from './built-in-pipeline-list' | |||
| import CustomizedList from './customized-list' | |||
| import { useTranslation } from 'react-i18next' | |||
| const List = () => { | |||
| const { t } = useTranslation() | |||
| const [activeTab, setActiveTab] = useState('built-in') | |||
| const options = useMemo(() => { | |||
| return [ | |||
| { value: 'built-in', label: t('datasetPipeline.tabs.builtInPipeline') }, | |||
| { value: 'customized', label: t('datasetPipeline.tabs.customized') }, | |||
| ] | |||
| }, [t]) | |||
| const handleTabChange = useCallback((tab: string) => { | |||
| setActiveTab(tab) | |||
| }, []) | |||
| return ( | |||
| <div className='flex grow flex-col overflow-hidden'> | |||
| <Tab | |||
| activeTab={activeTab} | |||
| handleTabChange={handleTabChange} | |||
| options={options} | |||
| /> | |||
| <div className='grow overflow-y-auto px-16'> | |||
| { | |||
| activeTab === 'built-in' && <BuiltInPipelineList /> | |||
| } | |||
| { | |||
| activeTab === 'customized' && <CustomizedList /> | |||
| } | |||
| </div> | |||
| <div className='grow gap-y-1 overflow-y-auto px-16 pb-[60px] pt-1'> | |||
| <BuiltInPipelineList /> | |||
| <CustomizedList /> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -1,32 +0,0 @@ | |||
| import React from 'react' | |||
| import Item from './item' | |||
| type TabProps = { | |||
| activeTab: string | |||
| handleTabChange: (tab: string) => void | |||
| options: { value: string; label: string; }[] | |||
| } | |||
| const Tab = ({ | |||
| activeTab, | |||
| handleTabChange, | |||
| options, | |||
| }: TabProps) => { | |||
| return ( | |||
| <div className='px-16 pt-2'> | |||
| <div className='relative flex h-10 items-center gap-x-6'> | |||
| {options.map((option, index) => ( | |||
| <Item | |||
| key={index} | |||
| option={option} | |||
| isSelected={activeTab === option.value} | |||
| onClick={handleTabChange} | |||
| /> | |||
| ))} | |||
| <div className='absolute bottom-0 left-0 h-px w-full bg-divider-subtle' /> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Tab) | |||
| @@ -1,29 +0,0 @@ | |||
| import cn from '@/utils/classnames' | |||
| import React from 'react' | |||
| type ItemProps = { | |||
| isSelected: boolean | |||
| option: { value: string; label: string } | |||
| onClick: (value: string) => void | |||
| } | |||
| const Item = ({ | |||
| isSelected, | |||
| option, | |||
| onClick, | |||
| }: ItemProps) => { | |||
| return ( | |||
| <div | |||
| className={cn( | |||
| 'system-sm-semibold-uppercase relative flex h-full cursor-pointer items-center', | |||
| isSelected ? 'text-text-primary' : 'text-text-tertiary', | |||
| )} | |||
| onClick={onClick.bind(null, option.value)} | |||
| > | |||
| <span>{option.label}</span> | |||
| {isSelected && <div className='absolute bottom-0 left-0 h-0.5 w-full bg-util-colors-blue-brand-blue-brand-600' />} | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(Item) | |||
| @@ -1,21 +1,17 @@ | |||
| const translation = { | |||
| creation: { | |||
| title: 'Create knowledge pipeline', | |||
| title: 'Blank Knowledge Pipeline', | |||
| createFromScratch: { | |||
| title: 'Create from scratch', | |||
| description: 'Blank knowledge pipeline', | |||
| }, | |||
| ImportDSL: { | |||
| title: 'Import', | |||
| description: 'Import from a DSL file', | |||
| title: 'Blank knowledge pipeline', | |||
| description: 'Create a custom pipeline from scratch with full control over data processing and structure.', | |||
| }, | |||
| importDSL: 'Import from a DSL file', | |||
| createKnowledge: 'Create Knowledge', | |||
| errorTip: 'Failed to create a Knowledge Base', | |||
| successTip: 'Successfully created a Knowledge Base', | |||
| caution: 'Caution', | |||
| }, | |||
| tabs: { | |||
| builtInPipeline: 'Built-in pipeline', | |||
| templates: { | |||
| customized: 'Customized', | |||
| }, | |||
| operations: { | |||
| @@ -1,21 +1,17 @@ | |||
| const translation = { | |||
| creation: { | |||
| title: '创建知识库 pipeline', | |||
| title: '创建知识流水线', | |||
| createFromScratch: { | |||
| title: '从零开始创建', | |||
| description: '空白知识库 pipeline', | |||
| title: '空白知识流水线', | |||
| description: '从零开始创建一个自定义知识流水线,对数据处理和结构拥有完全控制权。', | |||
| }, | |||
| ImportDSL: { | |||
| title: '导入', | |||
| description: '从 DSL 文件导入', | |||
| }, | |||
| createKnowledge: '创建知识库', | |||
| errorTip: '创建知识库', | |||
| successTip: '成功创建知识库', | |||
| importDSL: '从 DSL 文件导入', | |||
| createKnowledge: '创建知识流水线', | |||
| errorTip: '创建知识流水线失败', | |||
| successTip: '成功创建知识流水线', | |||
| caution: '注意', | |||
| }, | |||
| tabs: { | |||
| builtInPipeline: '内置 pipeline', | |||
| templates: { | |||
| customized: '自定义', | |||
| }, | |||
| operations: { | |||
| @@ -23,13 +19,13 @@ const translation = { | |||
| details: '详情', | |||
| editInfo: '编辑信息', | |||
| exportDSL: '导出 DSL', | |||
| useTemplate: '使用此知识库 pipeline', | |||
| useTemplate: '使用此知识流水线', | |||
| backToDataSource: '返回数据源', | |||
| process: '处理', | |||
| dataSource: '数据源', | |||
| saveAndProcess: '保存并处理', | |||
| preview: '预览', | |||
| exportPipeline: '导出 pipeline', | |||
| exportPipeline: '导出知识流水线', | |||
| convert: '转换', | |||
| }, | |||
| knowledgeNameAndIcon: '知识库名称和图标', | |||
| @@ -37,15 +33,15 @@ const translation = { | |||
| knowledgeDescription: '知识库描述', | |||
| knowledgeDescriptionPlaceholder: '描述知识库中的内容。详细的描述可以让 AI 更准确地访问数据集的内容。如果为空,Dify 将使用默认的命中策略。(可选)', | |||
| knowledgePermissions: '权限', | |||
| editPipelineInfo: '编辑 pipeline 信息', | |||
| pipelineNameAndIcon: 'pipeline 名称和图标', | |||
| editPipelineInfo: '编辑知识流水线信息', | |||
| pipelineNameAndIcon: '知识流水线名称和图标', | |||
| deletePipeline: { | |||
| title: '要删除此 pipeline 模板吗?', | |||
| content: '删除 pipeline 模板是不可逆的。', | |||
| title: '要删除此知识流水线模板吗?', | |||
| content: '删除知识流水线模板是不可逆的。', | |||
| }, | |||
| exportDSL: { | |||
| successTip: '成功导出 pipeline DSL', | |||
| errorTip: '导出 pipeline DSL 失败', | |||
| successTip: '成功导出知识流水线 DSL', | |||
| errorTip: '导出知识流水线 DSL 失败', | |||
| }, | |||
| details: { | |||
| createdBy: '由 {{author}} 创建', | |||
| @@ -70,7 +66,7 @@ const translation = { | |||
| inputField: '输入字段', | |||
| inputFieldPanel: { | |||
| title: '用户输入字段', | |||
| description: '用户输入字段用于定义和收集 pipeline 执行过程中所需的变量,用户可以自定义字段类型,并灵活配置输入,以满足不同数据源或文档处理的需求。', | |||
| description: '用户输入字段用于定义和收集知识流水线执行过程中所需的变量,用户可以自定义字段类型,并灵活配置输入,以满足不同数据源或文档处理的需求。', | |||
| uniqueInputs: { | |||
| title: '非共享输入', | |||
| tooltip: '非共享输入只能被选定的数据源及其下游节点访问。用户在选择其他数据源时不需要填写它。只有数据源变量引用的输入字段才会出现在第一步(数据源)中。所有其他字段将在第二步(Process Documents)中显示。', | |||
| @@ -136,16 +132,16 @@ const translation = { | |||
| }, | |||
| configurationTip: '配置 {{pluginName}}', | |||
| conversion: { | |||
| title: '转换为知识库 pipeline', | |||
| descriptionChunk1: '您现在可以将现有知识库转换为使用知识库 pipeline 来处理文档', | |||
| title: '转换为知识流水线', | |||
| descriptionChunk1: '您现在可以将现有知识库转换为使用知识流水线来处理文档', | |||
| descriptionChunk2: ' —— 这是一种更开放、更灵活的方式,可以访问我们市场中的插件。新的处理方式将应用到后续添加的所有文档。', | |||
| warning: '此操作无法撤销。', | |||
| confirm: { | |||
| title: '确认', | |||
| content: '此操作是永久性的。您将无法恢复到之前的方式。请确认转换。', | |||
| }, | |||
| errorMessage: '转换数据集为 pipeline 失败', | |||
| successMessage: '成功将数据集转换为 pipeline', | |||
| errorMessage: '转换数据集为知识流水线失败', | |||
| successMessage: '成功将数据集转换为知识流水线', | |||
| }, | |||
| } | |||
| @@ -135,6 +135,7 @@ const config = { | |||
| 'billing-plan-title-bg': 'var(--color-billing-plan-title-bg)', | |||
| 'billing-plan-card-premium-bg': 'var(--color-billing-plan-card-premium-bg)', | |||
| 'billing-plan-card-enterprise-bg': 'var(--color-billing-plan-card-enterprise-bg)', | |||
| 'knowledge-pipeline-creation-footer-bg': 'var(--color-knowledge-pipeline-creation-footer-bg)', | |||
| }, | |||
| animation: { | |||
| 'spin-slow': 'spin 2s linear infinite', | |||
| @@ -72,4 +72,5 @@ html[data-theme="dark"] { | |||
| --color-billing-plan-title-bg: linear-gradient(95deg, #0A68FF 29.47%, #03F 105.31%); | |||
| --color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%); | |||
| --color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%); | |||
| --color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, rgba(34, 34, 37, 1) 4.89%, rgba(0, 0, 0, 0) 100%); | |||
| } | |||
| @@ -72,4 +72,5 @@ html[data-theme="light"] { | |||
| --color-billing-plan-title-bg: linear-gradient(95deg, #03F 29.47%, #03F 105.31%); | |||
| --color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%); | |||
| --color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%); | |||
| --color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, #FCFCFD 4.89%, rgba(255, 255, 255, 0.00) 100%); | |||
| } | |||