mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 21:58:00 +08:00
feat: add dataset creation components and functionality
This commit is contained in:
parent
e689f21a60
commit
fc9556e057
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import CreateFromPipeline from '@/app/components/datasets/create-from-pipeline'
|
||||||
|
|
||||||
|
const DatasetCreation = async () => {
|
||||||
|
return (
|
||||||
|
<CreateFromPipeline />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatasetCreation
|
||||||
@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import React, { type FC, useRef } from 'react'
|
||||||
import type { FC } from 'react'
|
|
||||||
import { init } from 'emoji-mart'
|
import { init } from 'emoji-mart'
|
||||||
import data from '@emoji-mart/data'
|
import data from '@emoji-mart/data'
|
||||||
import { cva } from 'class-variance-authority'
|
import { cva } from 'class-variance-authority'
|
||||||
import type { AppIconType } from '@/types/app'
|
import type { AppIconType } from '@/types/app'
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
|
import { useHover } from 'ahooks'
|
||||||
|
import { RiEditLine } from '@remixicon/react'
|
||||||
|
|
||||||
init({ data })
|
init({ data })
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ export type AppIconProps = {
|
|||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
const appIconVariants = cva(
|
const appIconVariants = cva(
|
||||||
'flex items-center justify-center relative text-lg rounded-lg grow-0 shrink-0 overflow-hidden leading-none',
|
'flex items-center justify-center relative text-lg rounded-2xl grow-0 shrink-0 overflow-hidden leading-none border-[0.5px] border-divider-regular',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
@ -54,18 +55,31 @@ const AppIcon: FC<AppIconProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||||
|
const Icon = (icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />
|
||||||
|
const wrapperRef = useRef<HTMLSpanElement>(null)
|
||||||
|
const isHovering = useHover(wrapperRef)
|
||||||
|
|
||||||
return <span
|
return (
|
||||||
className={classNames(appIconVariants({ size, rounded }), className)}
|
<span
|
||||||
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
ref={wrapperRef}
|
||||||
onClick={onClick}
|
className={classNames(appIconVariants({ size, rounded }), className)}
|
||||||
>
|
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
||||||
{isValidImageIcon
|
onClick={onClick}
|
||||||
|
>
|
||||||
? <img src={imageUrl} className="h-full w-full" alt="app icon" />
|
{
|
||||||
: (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
|
isValidImageIcon
|
||||||
}
|
? <img src={imageUrl} className='h-full w-full' alt='app icon' />
|
||||||
</span>
|
: (innerIcon || Icon)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isHovering && (
|
||||||
|
<div className='absolute left-0 top-0 z-10 flex size-14 items-center justify-center rounded-2xl bg-background-overlay-alt'>
|
||||||
|
<RiEditLine className='size-6 text-text-primary-on-surface' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppIcon
|
export default React.memo(AppIcon)
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
.appIcon {
|
|
||||||
@apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appIcon.large {
|
|
||||||
@apply w-10 h-10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appIcon.small {
|
|
||||||
@apply w-8 h-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appIcon.tiny {
|
|
||||||
@apply w-6 h-6 text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appIcon.xs {
|
|
||||||
@apply w-5 h-5 text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appIcon.rounded {
|
|
||||||
@apply rounded-full;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
|
||||||
|
type DSLConfirmModalProps = {
|
||||||
|
versions?: {
|
||||||
|
importedVersion: string
|
||||||
|
systemVersion: string
|
||||||
|
}
|
||||||
|
onCancel: () => void
|
||||||
|
onConfirm: () => void
|
||||||
|
confirmDisabled?: boolean
|
||||||
|
}
|
||||||
|
const DSLConfirmModal = ({
|
||||||
|
versions = { importedVersion: '', systemVersion: '' },
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
confirmDisabled = false,
|
||||||
|
}: DSLConfirmModalProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isShow
|
||||||
|
onClose={() => onCancel()}
|
||||||
|
className='w-[480px]'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
|
||||||
|
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||||
|
<div className='system-md-regular flex grow flex-col text-text-secondary'>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||||
|
<br />
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions.importedVersion}</span></div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions.systemVersion}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
|
||||||
|
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
|
||||||
|
<Button variant='primary' destructive onClick={onConfirm} disabled={confirmDisabled}>{t('app.newApp.Confirm')}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DSLConfirmModal
|
||||||
@ -0,0 +1,327 @@
|
|||||||
|
'use client'
|
||||||
|
import { useMemo, useRef, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
|
||||||
|
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import {
|
||||||
|
importDSL,
|
||||||
|
importDSLConfirm,
|
||||||
|
} from '@/service/apps'
|
||||||
|
import {
|
||||||
|
DSLImportMode,
|
||||||
|
DSLImportStatus,
|
||||||
|
} from '@/models/app'
|
||||||
|
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||||
|
import { useProviderContextSelector } from '@/context/provider-context'
|
||||||
|
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||||
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||||
|
import { noop } from 'lodash-es'
|
||||||
|
import Uploader from './uploader'
|
||||||
|
|
||||||
|
type CreateFromDSLModalProps = {
|
||||||
|
show: boolean
|
||||||
|
onSuccess?: () => void
|
||||||
|
onClose: () => void
|
||||||
|
activeTab?: string
|
||||||
|
dslUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CreateFromDSLModalTab {
|
||||||
|
FROM_FILE = 'from-file',
|
||||||
|
FROM_URL = 'from-url',
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateFromDSLModal = ({
|
||||||
|
show,
|
||||||
|
onSuccess,
|
||||||
|
onClose,
|
||||||
|
activeTab = CreateFromDSLModalTab.FROM_FILE,
|
||||||
|
dslUrl = '',
|
||||||
|
}: CreateFromDSLModalProps) => {
|
||||||
|
const { push } = useRouter()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useContext(ToastContext)
|
||||||
|
const [currentFile, setDSLFile] = useState<File>()
|
||||||
|
const [fileContent, setFileContent] = useState<string>()
|
||||||
|
const [currentTab, setCurrentTab] = useState(activeTab)
|
||||||
|
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
|
||||||
|
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||||
|
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||||
|
const [importId, setImportId] = useState<string>()
|
||||||
|
const { handleCheckPluginDependencies } = usePluginDependencies()
|
||||||
|
|
||||||
|
const readFile = (file: File) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = function (event) {
|
||||||
|
const content = event.target?.result
|
||||||
|
setFileContent(content as string)
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFile = (file?: File) => {
|
||||||
|
setDSLFile(file)
|
||||||
|
if (file)
|
||||||
|
readFile(file)
|
||||||
|
if (!file)
|
||||||
|
setFileContent('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentWorkspaceEditor = useAppContextWithSelector(state => state.isCurrentWorkspaceEditor)
|
||||||
|
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 onCreate = async () => {
|
||||||
|
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
|
||||||
|
return
|
||||||
|
if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue)
|
||||||
|
return
|
||||||
|
if (isCreatingRef.current)
|
||||||
|
return
|
||||||
|
isCreatingRef.current = true
|
||||||
|
try {
|
||||||
|
let response
|
||||||
|
|
||||||
|
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
|
||||||
|
response = await importDSL({
|
||||||
|
mode: DSLImportMode.YAML_CONTENT,
|
||||||
|
yaml_content: fileContent || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
|
||||||
|
response = await importDSL({
|
||||||
|
mode: DSLImportMode.YAML_URL,
|
||||||
|
yaml_url: dslUrlValue || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response)
|
||||||
|
return
|
||||||
|
const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
|
||||||
|
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||||
|
if (onSuccess)
|
||||||
|
onSuccess()
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
|
||||||
|
notify({
|
||||||
|
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||||
|
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
|
||||||
|
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
|
||||||
|
})
|
||||||
|
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||||
|
if (app_id)
|
||||||
|
await handleCheckPluginDependencies(app_id)
|
||||||
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||||
|
}
|
||||||
|
else if (status === DSLImportStatus.PENDING) {
|
||||||
|
setVersions({
|
||||||
|
importedVersion: imported_dsl_version ?? '',
|
||||||
|
systemVersion: current_dsl_version ?? '',
|
||||||
|
})
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowErrorModal(true)
|
||||||
|
}, 300)
|
||||||
|
setImportId(id)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
catch (e) {
|
||||||
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isCreatingRef.current = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
|
||||||
|
|
||||||
|
useKeyPress(['meta.enter', 'ctrl.enter'], () => {
|
||||||
|
if (show && !isAppsFull && ((currentTab === CreateFromDSLModalTab.FROM_FILE && currentFile) || (currentTab === CreateFromDSLModalTab.FROM_URL && dslUrlValue)))
|
||||||
|
handleCreateApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyPress('esc', () => {
|
||||||
|
if (show && !showErrorModal)
|
||||||
|
onClose()
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDSLConfirm = async () => {
|
||||||
|
try {
|
||||||
|
if (!importId)
|
||||||
|
return
|
||||||
|
const response = await importDSLConfirm({
|
||||||
|
import_id: importId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { status, app_id, app_mode } = response
|
||||||
|
|
||||||
|
if (status === DSLImportStatus.COMPLETED) {
|
||||||
|
if (onSuccess)
|
||||||
|
onSuccess()
|
||||||
|
if (onClose)
|
||||||
|
onClose()
|
||||||
|
|
||||||
|
notify({
|
||||||
|
type: 'success',
|
||||||
|
message: t('app.newApp.appCreated'),
|
||||||
|
})
|
||||||
|
if (app_id)
|
||||||
|
await handleCheckPluginDependencies(app_id)
|
||||||
|
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||||
|
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)
|
||||||
|
}
|
||||||
|
else if (status === DSLImportStatus.FAILED) {
|
||||||
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||||
|
catch (e) {
|
||||||
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
key: CreateFromDSLModalTab.FROM_FILE,
|
||||||
|
label: t('app.importFromDSLFile'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: CreateFromDSLModalTab.FROM_URL,
|
||||||
|
label: t('app.importFromDSLUrl'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const buttonDisabled = useMemo(() => {
|
||||||
|
if (isAppsFull)
|
||||||
|
return true
|
||||||
|
if (currentTab === CreateFromDSLModalTab.FROM_FILE)
|
||||||
|
return !currentFile
|
||||||
|
if (currentTab === CreateFromDSLModalTab.FROM_URL)
|
||||||
|
return !dslUrlValue
|
||||||
|
return false
|
||||||
|
}, [isAppsFull, currentTab, currentFile, dslUrlValue])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
className='w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl'
|
||||||
|
isShow={show}
|
||||||
|
onClose={noop}
|
||||||
|
>
|
||||||
|
<div className='title-2xl-semi-bold flex items-center justify-between pb-3 pl-6 pr-5 pt-6 text-text-primary'>
|
||||||
|
{t('app.importFromDSL')}
|
||||||
|
<div
|
||||||
|
className='flex h-8 w-8 cursor-pointer items-center'
|
||||||
|
onClick={() => onClose()}
|
||||||
|
>
|
||||||
|
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='system-md-semibold flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 text-text-tertiary'>
|
||||||
|
{
|
||||||
|
tabs.map(tab => (
|
||||||
|
<div
|
||||||
|
key={tab.key}
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-full cursor-pointer items-center',
|
||||||
|
currentTab === tab.key && 'text-text-primary',
|
||||||
|
)}
|
||||||
|
onClick={() => setCurrentTab(tab.key)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
{
|
||||||
|
currentTab === tab.key && (
|
||||||
|
<div className='absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600'></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className='px-6 py-4'>
|
||||||
|
{
|
||||||
|
currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||||
|
<Uploader
|
||||||
|
className='mt-0'
|
||||||
|
file={currentFile}
|
||||||
|
updateFile={handleFile}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||||
|
<div>
|
||||||
|
<div className='system-md-semibold leading6 mb-1'>DSL URL</div>
|
||||||
|
<Input
|
||||||
|
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
|
||||||
|
value={dslUrlValue}
|
||||||
|
onChange={e => setDslUrlValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{isAppsFull && (
|
||||||
|
<div className='px-6'>
|
||||||
|
<AppsFull className='mt-0' loc='app-create-dsl' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='flex justify-end px-6 py-5'>
|
||||||
|
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||||
|
<Button
|
||||||
|
disabled={buttonDisabled}
|
||||||
|
variant='primary'
|
||||||
|
onClick={handleCreateApp}
|
||||||
|
className='gap-1'
|
||||||
|
>
|
||||||
|
<span>{t('app.newApp.Create')}</span>
|
||||||
|
<div className='flex gap-0.5'>
|
||||||
|
<RiCommandLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
|
||||||
|
<RiCornerDownLeftLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isShow={showErrorModal}
|
||||||
|
onClose={() => setShowErrorModal(false)}
|
||||||
|
className='w-[480px]'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
|
||||||
|
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||||
|
<div className='system-md-regular flex grow flex-col text-text-secondary'>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||||
|
<br />
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
|
||||||
|
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
|
||||||
|
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
|
||||||
|
<Button variant='primary' destructive onClick={onDSLConfirm}>{t('app.newApp.Confirm')}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateFromDSLModal
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import {
|
||||||
|
RiDeleteBinLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { formatFileSize } from '@/utils/format'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import { UploadCloud01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
file: File | undefined
|
||||||
|
updateFile: (file?: File) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uploader: FC<Props> = ({
|
||||||
|
file,
|
||||||
|
updateFile,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useContext(ToastContext)
|
||||||
|
const [dragging, setDragging] = useState(false)
|
||||||
|
const dropRef = useRef<HTMLDivElement>(null)
|
||||||
|
const dragRef = useRef<HTMLDivElement>(null)
|
||||||
|
const fileUploader = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
e.target !== dragRef.current && setDragging(true)
|
||||||
|
}
|
||||||
|
const handleDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
const handleDragLeave = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
e.target === dragRef.current && setDragging(false)
|
||||||
|
}
|
||||||
|
const handleDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setDragging(false)
|
||||||
|
if (!e.dataTransfer)
|
||||||
|
return
|
||||||
|
const files = [...e.dataTransfer.files]
|
||||||
|
if (files.length > 1) {
|
||||||
|
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateFile(files[0])
|
||||||
|
}
|
||||||
|
const selectHandle = () => {
|
||||||
|
const originalFile = file
|
||||||
|
if (fileUploader.current) {
|
||||||
|
fileUploader.current.value = ''
|
||||||
|
fileUploader.current.click()
|
||||||
|
// If no file is selected, restore the original file
|
||||||
|
fileUploader.current.oncancel = () => updateFile(originalFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const removeFile = () => {
|
||||||
|
if (fileUploader.current)
|
||||||
|
fileUploader.current.value = ''
|
||||||
|
updateFile()
|
||||||
|
}
|
||||||
|
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const currentFile = e.target.files?.[0]
|
||||||
|
updateFile(currentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dropArea = dropRef.current
|
||||||
|
dropArea?.addEventListener('dragenter', handleDragEnter)
|
||||||
|
dropArea?.addEventListener('dragover', handleDragOver)
|
||||||
|
dropArea?.addEventListener('dragleave', handleDragLeave)
|
||||||
|
dropArea?.addEventListener('drop', handleDrop)
|
||||||
|
return () => {
|
||||||
|
dropArea?.removeEventListener('dragenter', handleDragEnter)
|
||||||
|
dropArea?.removeEventListener('dragover', handleDragOver)
|
||||||
|
dropArea?.removeEventListener('dragleave', handleDragLeave)
|
||||||
|
dropArea?.removeEventListener('drop', handleDrop)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('mt-6', className)}>
|
||||||
|
<input
|
||||||
|
ref={fileUploader}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
type='file'
|
||||||
|
id='fileUploader'
|
||||||
|
accept='.yaml,.yml'
|
||||||
|
onChange={fileChangeHandle}
|
||||||
|
/>
|
||||||
|
<div ref={dropRef}>
|
||||||
|
{!file && (
|
||||||
|
<div className={cn('flex h-12 items-center rounded-xl border border-dashed border-gray-200 bg-gray-50 text-sm font-normal', dragging && 'border border-[#B2CCFF] bg-[#F5F8FF]')}>
|
||||||
|
<div className='flex w-full items-center justify-center space-x-2'>
|
||||||
|
<UploadCloud01 className='mr-2 h-6 w-6' />
|
||||||
|
<div className='text-text-tertiary'>
|
||||||
|
{t('datasetCreation.stepOne.uploader.button')}
|
||||||
|
<span
|
||||||
|
className='cursor-pointer pl-1 text-text-accent'
|
||||||
|
onClick={selectHandle}
|
||||||
|
>
|
||||||
|
{t('datasetDocuments.list.batchModal.browse')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{file && (
|
||||||
|
<div className={cn('group flex items-center rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', 'hover:border-[#B2CCFF] hover:bg-[#F5F8FF]')}>
|
||||||
|
<div className='flex items-center justify-center p-3'>
|
||||||
|
<YamlIcon className='h-6 w-6 shrink-0' />
|
||||||
|
</div>
|
||||||
|
<div className='flex grow flex-col items-start gap-0.5 py-1 pr-2'>
|
||||||
|
<span className='font-inter max-w-[calc(100%_-_30px)] overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-medium leading-4 text-text-secondary'>{file.name}</span>
|
||||||
|
<div className='font-inter flex h-3 items-center gap-1 self-stretch text-[10px] font-medium uppercase leading-3 text-text-tertiary'>
|
||||||
|
<span>YAML</span>
|
||||||
|
<span className='text-text-quaternary'>·</span>
|
||||||
|
<span>{formatFileSize(file.size)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='hidden items-center group-hover:flex'>
|
||||||
|
<Button onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||||
|
<div className='mx-2 h-4 w-px bg-gray-200' />
|
||||||
|
<div className='cursor-pointer p-2' onClick={removeFile}>
|
||||||
|
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Uploader)
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
|
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||||
|
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import Textarea from '@/app/components/base/textarea'
|
||||||
|
import type { AppIconType } from '@/types/app'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
|
import PermissionSelector from '../../settings/permission-selector'
|
||||||
|
import { DatasetPermission } from '@/models/datasets'
|
||||||
|
import { useMembers } from '@/service/use-common'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
|
||||||
|
type CreateFromScratchProps = {
|
||||||
|
onClose: () => void
|
||||||
|
onCreate: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_APP_ICON: AppIconSelection = {
|
||||||
|
type: 'emoji',
|
||||||
|
icon: '📙',
|
||||||
|
background: '#FFF4ED',
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateFromScratch = ({
|
||||||
|
onClose,
|
||||||
|
onCreate,
|
||||||
|
}: CreateFromScratchProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [appIcon, setAppIcon] = useState<AppIconSelection>(DEFAULT_APP_ICON)
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [permission, setPermission] = useState(DatasetPermission.onlyMe)
|
||||||
|
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||||
|
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>([])
|
||||||
|
const previousAppIcon = useRef<AppIconSelection>(DEFAULT_APP_ICON)
|
||||||
|
|
||||||
|
const { data: memberList } = useMembers()
|
||||||
|
|
||||||
|
const handleAppNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value
|
||||||
|
setName(value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleOpenAppIconPicker = useCallback(() => {
|
||||||
|
setShowAppIconPicker(true)
|
||||||
|
previousAppIcon.current = appIcon
|
||||||
|
}, [appIcon])
|
||||||
|
|
||||||
|
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
|
||||||
|
setAppIcon(icon)
|
||||||
|
setShowAppIconPicker(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCloseAppIconPicker = useCallback(() => {
|
||||||
|
setAppIcon(previousAppIcon.current)
|
||||||
|
setShowAppIconPicker(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const value = event.target.value
|
||||||
|
setDescription(value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handlePermissionChange = useCallback((value?: DatasetPermission) => {
|
||||||
|
setPermission(value!)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCreate = useCallback(() => {
|
||||||
|
if (!name) {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Please enter a name for the Knowledge Base.',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onCreate()
|
||||||
|
onClose()
|
||||||
|
}, [name, onCreate, onClose])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative flex flex-col'>
|
||||||
|
{/* Header */}
|
||||||
|
<div className='pb-3 pl-6 pr-14 pt-6'>
|
||||||
|
<span className='title-2xl-semi-bold text-text-primary'>
|
||||||
|
Create Knowledge
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className='absolute right-5 top-5 flex size-8 items-center justify-center'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<RiCloseLine className='size-5 text-text-tertiary' />
|
||||||
|
</button>
|
||||||
|
{/* Form */}
|
||||||
|
<div className='flex flex-col gap-y-5 px-6 py-3'>
|
||||||
|
<div className='flex items-end gap-x-3 self-stretch'>
|
||||||
|
<div className='flex grow flex-col gap-y-1 pb-1'>
|
||||||
|
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Knowledge name & icon</label>
|
||||||
|
<Input
|
||||||
|
onChange={handleAppNameChange}
|
||||||
|
value={name}
|
||||||
|
placeholder='Please enter the name of the Knowledge Base'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AppIcon
|
||||||
|
size='xxl'
|
||||||
|
onClick={handleOpenAppIconPicker}
|
||||||
|
className='cursor-pointer'
|
||||||
|
iconType={appIcon.type as AppIconType}
|
||||||
|
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
|
||||||
|
background={appIcon.type === 'image' ? undefined : appIcon.background}
|
||||||
|
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-y-1'>
|
||||||
|
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Knowledge description</label>
|
||||||
|
<Textarea
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
value={description}
|
||||||
|
placeholder='Describe what is in this Knowledge Base. A detailed description allows AI to access the content of the dataset more accurately. If empty, Dify will use the default hit strategy. (Optional)'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-y-1'>
|
||||||
|
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Permissions</label>
|
||||||
|
<PermissionSelector
|
||||||
|
permission={permission}
|
||||||
|
value={selectedMemberIDs}
|
||||||
|
onChange={handlePermissionChange}
|
||||||
|
onMemberSelect={setSelectedMemberIDs}
|
||||||
|
memberList={memberList?.accounts || []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Actions */}
|
||||||
|
<div className='flex items-center justify-end gap-x-2 p-6 pt-5'>
|
||||||
|
<Button
|
||||||
|
variant='secondary'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
onClick={handleCreate}
|
||||||
|
>
|
||||||
|
{t('common.operation.create')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{showAppIconPicker && (
|
||||||
|
<AppIconPicker
|
||||||
|
onSelect={handleSelectAppIcon}
|
||||||
|
onClose={handleCloseAppIconPicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(CreateFromScratch)
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
|
import Item from './item'
|
||||||
|
import { RiAddCircleFill, RiFileUploadLine } from '@remixicon/react'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import CreateFromScratch from './create-from-scratch'
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-from-dsl-modal'
|
||||||
|
import { useProviderContextSelector } from '@/context/provider-context'
|
||||||
|
|
||||||
|
const CreateOptions = () => {
|
||||||
|
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
|
||||||
|
|
||||||
|
const activeTab = useMemo(() => {
|
||||||
|
if (dslUrl)
|
||||||
|
return CreateFromDSLModalTab.FROM_URL
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}, [dslUrl])
|
||||||
|
|
||||||
|
const openCreateFromScratch = useCallback(() => {
|
||||||
|
setShowCreateModal(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const closeCreateFromScratch = useCallback(() => {
|
||||||
|
setShowCreateModal(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCreateFromScratch = useCallback(() => {
|
||||||
|
setShowCreateModal(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openImportFromDSL = useCallback(() => {
|
||||||
|
setShowImportModal(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onCloseImportModal = useCallback(() => {
|
||||||
|
setShowImportModal(false)
|
||||||
|
if (dslUrl)
|
||||||
|
replace('/datasets/create-from-pipeline')
|
||||||
|
}, [dslUrl, replace])
|
||||||
|
|
||||||
|
const onImportFromDSLSuccess = useCallback(() => {
|
||||||
|
onPlanInfoChanged()
|
||||||
|
}, [onPlanInfoChanged])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center gap-x-3 px-16 py-2'>
|
||||||
|
<Item
|
||||||
|
Icon={RiAddCircleFill}
|
||||||
|
title='Create from scratch'
|
||||||
|
description='Blank knowledge pipeline'
|
||||||
|
onClick={openCreateFromScratch}
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
Icon={RiFileUploadLine}
|
||||||
|
title='Import'
|
||||||
|
description='Import from a DSL file'
|
||||||
|
onClick={openImportFromDSL}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
isShow={showCreateModal}
|
||||||
|
onClose={closeCreateFromScratch}
|
||||||
|
className='max-w-[520px] p-0'
|
||||||
|
>
|
||||||
|
<CreateFromScratch
|
||||||
|
onClose={closeCreateFromScratch}
|
||||||
|
onCreate={handleCreateFromScratch}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<CreateFromDSLModal
|
||||||
|
show={showImportModal}
|
||||||
|
onClose={onCloseImportModal}
|
||||||
|
activeTab={activeTab}
|
||||||
|
dslUrl={dslUrl}
|
||||||
|
onSuccess={onImportFromDSLSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(CreateOptions)
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
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] 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)
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const HeaderEffect = () => {
|
||||||
|
return (
|
||||||
|
<div className='absolute left-8 top-[-34px] size-[112px] bg-util-colors-blue-brand-blue-brand-500 opacity-20 blur-[80px]' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(HeaderEffect)
|
||||||
21
web/app/components/datasets/create-from-pipeline/header.tsx
Normal file
21
web/app/components/datasets/create-from-pipeline/header.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { RiArrowLeftLine } from '@remixicon/react'
|
||||||
|
import Button from '../../base/button'
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
return (
|
||||||
|
<div className='system-md-semibold relative flex px-16 pb-2 pt-5 text-text-primary'>
|
||||||
|
<span>Create knowledge pipeline</span>
|
||||||
|
<a
|
||||||
|
className='absolute bottom-0 left-5'
|
||||||
|
href='/datasets'
|
||||||
|
>
|
||||||
|
<Button variant='secondary-accent' className='size-9 rounded-full p-0'>
|
||||||
|
<RiArrowLeftLine className='size-5 ' />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Header)
|
||||||
19
web/app/components/datasets/create-from-pipeline/index.tsx
Normal file
19
web/app/components/datasets/create-from-pipeline/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use client'
|
||||||
|
import HeaderEffect from './header-effect'
|
||||||
|
import Header from './header'
|
||||||
|
import CreateOptions from './create-options'
|
||||||
|
|
||||||
|
const CreateFromPipeline = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='relative flex flex-col rounded-t-2xl border-t border-effects-highlight bg-background-default-subtle'
|
||||||
|
style={{ height: 'calc(100vh - 56px)' }}
|
||||||
|
>
|
||||||
|
<HeaderEffect />
|
||||||
|
<Header />
|
||||||
|
<CreateOptions />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateFromPipeline
|
||||||
@ -155,7 +155,10 @@ const DatasetCard = ({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={tagSelectorRef}
|
ref={tagSelectorRef}
|
||||||
className={'invisible w-full group-hover:visible'}
|
className={cn(
|
||||||
|
'invisible w-full group-hover:visible',
|
||||||
|
tags.length > 0 && 'visible',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<TagSelector
|
<TagSelector
|
||||||
position='bl'
|
position='bl'
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { get, post } from './base'
|
|||||||
import type {
|
import type {
|
||||||
DataSourceNotion,
|
DataSourceNotion,
|
||||||
FileUploadConfigResponse,
|
FileUploadConfigResponse,
|
||||||
|
Member,
|
||||||
StructuredOutputRulesRequestBody,
|
StructuredOutputRulesRequestBody,
|
||||||
StructuredOutputRulesResponse,
|
StructuredOutputRulesResponse,
|
||||||
} from '@/models/common'
|
} from '@/models/common'
|
||||||
@ -46,3 +47,16 @@ export const useDataSources = () => {
|
|||||||
queryFn: () => get<DataSourcesResponse>('/data-source/integrates'),
|
queryFn: () => get<DataSourcesResponse>('/data-source/integrates'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MemberResponse = {
|
||||||
|
accounts: Member[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMembers = () => {
|
||||||
|
return useQuery<MemberResponse>({
|
||||||
|
queryKey: [NAME_SPACE, 'members'],
|
||||||
|
queryFn: (params: Record<string, any>) => get<MemberResponse>('/workspaces/current/members', {
|
||||||
|
params,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user