diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index f0f6cd66e6..b5628fcbad 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -7,6 +7,7 @@ on: - "deploy/dev" - "deploy/enterprise" - "feat/r2" + - "feat/rag-pipeline" tags: - "*" diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 7d5d4cb52d..e3354ade13 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -43,10 +43,10 @@ const AppDetailLayout: FC = (props) => { const media = useBreakpoints() const isMobile = media === MediaType.mobile const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext() - const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({ + const { appDetail, setAppDetail, setAppSidebarExpand } = useStore(useShallow(state => ({ appDetail: state.appDetail, setAppDetail: state.setAppDetail, - setAppSiderbarExpand: state.setAppSiderbarExpand, + setAppSidebarExpand: state.setAppSidebarExpand, }))) const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false) const [appDetailRes, setAppDetailRes] = useState(null) @@ -57,8 +57,8 @@ const AppDetailLayout: FC = (props) => { selectedIcon: NavIcon }>>([]) - const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { - const navs = [ + const getNavigationConfig = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { + const navConfig = [ ...(isCurrentWorkspaceEditor ? [{ name: t('common.appMenus.promptEng'), @@ -92,8 +92,8 @@ const AppDetailLayout: FC = (props) => { selectedIcon: RiDashboard2Fill, }, ] - return navs - }, []) + return navConfig + }, [t]) useDocumentTitle(appDetail?.name || t('common.menus.appDetail')) @@ -101,10 +101,10 @@ const AppDetailLayout: FC = (props) => { if (appDetail) { const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand' const mode = isMobile ? 'collapse' : 'expand' - setAppSiderbarExpand(isMobile ? mode : localeMode) + setAppSidebarExpand(isMobile ? mode : localeMode) // TODO: consider screen size and mode // if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow')) - // setAppSiderbarExpand('collapse') + // setAppSidebarExpand('collapse') } // eslint-disable-next-line react-hooks/exhaustive-deps }, [appDetail, isMobile]) @@ -141,7 +141,7 @@ const AppDetailLayout: FC = (props) => { } else { setAppDetail({ ...res, enable_sso: false }) - setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode)) + setNavigation(getNavigationConfig(appId, isCurrentWorkspaceEditor, res.mode)) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace]) @@ -161,7 +161,9 @@ const AppDetailLayout: FC = (props) => { return (
{appDetail && ( - + )}
{children} diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/create-from-pipeline/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/create-from-pipeline/page.tsx new file mode 100644 index 0000000000..9ce86bbef4 --- /dev/null +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/documents/create-from-pipeline/page.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import CreateFromPipeline from '@/app/components/datasets/documents/create-from-pipeline' + +const CreateFromPipelinePage = async () => { + return ( + + ) +} + +export default CreateFromPipelinePage diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 94cd5ad562..43907718f3 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -2,10 +2,10 @@ import type { FC } from 'react' import React, { useEffect, useMemo } from 'react' import { usePathname } from 'next/navigation' -import useSWR from 'swr' import { useTranslation } from 'react-i18next' -import { useBoolean } from 'ahooks' +import type { RemixiconComponentType } from '@remixicon/react' import { + RiAttachmentLine, RiEqualizer2Fill, RiEqualizer2Line, RiFileTextFill, @@ -13,24 +13,22 @@ import { RiFocus2Fill, RiFocus2Line, } from '@remixicon/react' -import { - PaperClipIcon, -} from '@heroicons/react/24/outline' -import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react' +import { RiInformation2Line } from '@remixicon/react' import classNames from '@/utils/classnames' -import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets' import type { RelatedAppResponse } from '@/models/datasets' import AppSideBar from '@/app/components/app-sidebar' import Loading from '@/app/components/base/loading' import DatasetDetailContext from '@/context/dataset-detail' import { DataSourceType } from '@/models/datasets' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { LanguagesSupported } from '@/i18n/language' import { useStore } from '@/app/components/app/store' -import { getLocaleOnClient } from '@/i18n' import { useAppContext } from '@/context/app-context' import Tooltip from '@/app/components/base/tooltip' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' +import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/vender/pipeline' +import { Divider } from '@/app/components/base/icons/src/vender/knowledge' +import NoLinkedAppsPanel from '@/app/components/datasets/no-linked-apps-panel' +import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' import useDocumentTitle from '@/hooks/use-document-title' export type IAppDetailLayoutProps = { @@ -39,85 +37,71 @@ export type IAppDetailLayoutProps = { } type IExtraInfoProps = { - isMobile: boolean relatedApps?: RelatedAppResponse + documentCount?: number expand: boolean } -const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { - const locale = getLocaleOnClient() - const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile) +const ExtraInfo = React.memo(({ + relatedApps, + documentCount, + expand, +}: IExtraInfoProps) => { const { t } = useTranslation() const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0 const relatedAppsTotal = relatedApps?.data?.length || 0 - useEffect(() => { - setShowTips(!isMobile) - }, [isMobile, setShowTips]) - - return
- {hasRelatedApps && ( - <> - {!isMobile && ( - - } - > -
- {relatedAppsTotal || '--'} {t('common.datasetMenus.relatedApp')} - + return ( + <> + {!expand && ( +
+
+
+ {documentCount ?? '--'}
- - )} - - {isMobile &&
- {relatedAppsTotal || '--'} - -
} - - )} - {!hasRelatedApps && !expand && ( - -
- +
+ {t('common.datasetMenus.documents')} +
+
+
+ +
+
+
+ {relatedAppsTotal ?? '--'}
-
{t('common.datasetMenus.emptyTip')}
- + ) : } - target='_blank' rel='noopener noreferrer' > - - {t('common.datasetMenus.viewDoc')} - +
+ {t('common.datasetMenus.relatedApp')} + +
+
- } - > -
- {t('common.datasetMenus.noRelatedApp')} -
-
- )} -
-} + )} + + {expand && ( +
+ {relatedAppsTotal ?? '--'} + +
+ )} + + ) +}) const DatasetDetailLayout: FC = (props) => { const { @@ -125,70 +109,94 @@ const DatasetDetailLayout: FC = (props) => { params: { datasetId }, } = props const pathname = usePathname() - const hideSideBar = /documents\/create$/.test(pathname) + const hideSideBar = pathname.endsWith('documents/create') || pathname.endsWith('documents/create-from-pipeline') const { t } = useTranslation() const { isCurrentWorkspaceDatasetOperator } = useAppContext() const media = useBreakpoints() const isMobile = media === MediaType.mobile - const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({ - url: 'fetchDatasetDetail', - datasetId, - }, apiParams => fetchDatasetDetail(apiParams.datasetId)) + const { data: datasetRes, error, refetch: mutateDatasetRes } = useDatasetDetail(datasetId) + + const { data: relatedApps } = useDatasetRelatedApps(datasetId) - const { data: relatedApps } = useSWR({ - action: 'fetchDatasetRelatedApps', - datasetId, - }, apiParams => fetchDatasetRelatedApps(apiParams.datasetId)) + const isButtonDisabledWithPipeline = useMemo(() => { + if (!datasetRes) + return true + if (datasetRes.provider === 'external') + return false + if (!datasetRes.pipeline_id) + return false + return !datasetRes.is_published + }, [datasetRes]) const navigation = useMemo(() => { const baseNavigation = [ - { name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: RiFocus2Line, selectedIcon: RiFocus2Fill }, - { name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: RiEqualizer2Line, selectedIcon: RiEqualizer2Fill }, + { + name: t('common.datasetMenus.hitTesting'), + href: `/datasets/${datasetId}/hitTesting`, + icon: RiFocus2Line, + selectedIcon: RiFocus2Fill, + disabled: isButtonDisabledWithPipeline, + }, + { + name: t('common.datasetMenus.settings'), + href: `/datasets/${datasetId}/settings`, + icon: RiEqualizer2Line, + selectedIcon: RiEqualizer2Fill, + disabled: false, + }, ] if (datasetRes?.provider !== 'external') { - baseNavigation.unshift({ + baseNavigation.unshift(...[{ name: t('common.datasetMenus.documents'), href: `/datasets/${datasetId}/documents`, icon: RiFileTextLine, selectedIcon: RiFileTextFill, - }) + disabled: isButtonDisabledWithPipeline, + }, { + name: t('common.datasetMenus.pipeline'), + href: `/datasets/${datasetId}/pipeline`, + icon: PipelineLine as RemixiconComponentType, + selectedIcon: PipelineFill as RemixiconComponentType, + disabled: false, + }]) } return baseNavigation - }, [datasetRes?.provider, datasetId, t]) + }, [t, datasetId, isButtonDisabledWithPipeline, datasetRes?.provider]) useDocumentTitle(datasetRes?.name || t('common.menus.datasets')) - const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand) + const setAppSidebarExpand = useStore(state => state.setAppSidebarExpand) useEffect(() => { const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand' const mode = isMobile ? 'collapse' : 'expand' - setAppSiderbarExpand(isMobile ? mode : localeMode) - }, [isMobile, setAppSiderbarExpand]) + setAppSidebarExpand(isMobile ? mode : localeMode) + }, [isMobile, setAppSidebarExpand]) if (!datasetRes && !error) return return (
- {!hideSideBar && : undefined} - iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'} - />} mutateDatasetRes(), + mutateDatasetRes, }}> + {!hideSideBar && ( + + : undefined + } + iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'} + /> + )}
{children}
diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/pipeline/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/pipeline/page.tsx new file mode 100644 index 0000000000..9a18021cc0 --- /dev/null +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/pipeline/page.tsx @@ -0,0 +1,11 @@ +'use client' +import RagPipeline from '@/app/components/rag-pipeline' + +const PipelinePage = () => { + return ( +
+ +
+ ) +} +export default PipelinePage diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx index d9a196d854..164c2dc7ba 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx @@ -8,8 +8,8 @@ const Settings = async () => { return (
-
-
{t('title')}
+
+
{t('title')}
{t('desc')}
diff --git a/web/app/(commonLayout)/datasets/Datasets.tsx b/web/app/(commonLayout)/datasets/Datasets.tsx deleted file mode 100644 index 28461e8617..0000000000 --- a/web/app/(commonLayout)/datasets/Datasets.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client' - -import { useCallback, useEffect, useRef } from 'react' -import useSWRInfinite from 'swr/infinite' -import { debounce } from 'lodash-es' -import NewDatasetCard from './NewDatasetCard' -import DatasetCard from './DatasetCard' -import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets' -import { fetchDatasets } from '@/service/datasets' -import { useAppContext } from '@/context/app-context' -import { useTranslation } from 'react-i18next' - -const getKey = ( - pageIndex: number, - previousPageData: DataSetListResponse, - tags: string[], - keyword: string, - includeAll: boolean, -) => { - if (!pageIndex || previousPageData.has_more) { - const params: FetchDatasetsParams = { - url: 'datasets', - params: { - page: pageIndex + 1, - limit: 30, - include_all: includeAll, - }, - } - if (tags.length) - params.params.tag_ids = tags - if (keyword) - params.params.keyword = keyword - return params - } - return null -} - -type Props = { - containerRef: React.RefObject - tags: string[] - keywords: string - includeAll: boolean -} - -const Datasets = ({ - containerRef, - tags, - keywords, - includeAll, -}: Props) => { - const { t } = useTranslation() - const { isCurrentWorkspaceEditor } = useAppContext() - const { data, isLoading, setSize, mutate } = useSWRInfinite( - (pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll), - fetchDatasets, - { revalidateFirstPage: false, revalidateAll: true }, - ) - const loadingStateRef = useRef(false) - const anchorRef = useRef(null) - - useEffect(() => { - loadingStateRef.current = isLoading - }, [isLoading, t]) - - const onScroll = useCallback( - debounce(() => { - if (!loadingStateRef.current && containerRef.current && anchorRef.current) { - const { scrollTop, clientHeight } = containerRef.current - const anchorOffset = anchorRef.current.offsetTop - if (anchorOffset - scrollTop - clientHeight < 100) - setSize(size => size + 1) - } - }, 50), - [setSize], - ) - - useEffect(() => { - const currentContainer = containerRef.current - currentContainer?.addEventListener('scroll', onScroll) - return () => { - currentContainer?.removeEventListener('scroll', onScroll) - onScroll.cancel() - } - }, [onScroll]) - - return ( - - ) -} - -export default Datasets diff --git a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx deleted file mode 100644 index 792d9904da..0000000000 --- a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' -import { useTranslation } from 'react-i18next' -import { basePath } from '@/utils/var' -import { - RiAddLine, - RiArrowRightLine, -} from '@remixicon/react' - -const CreateAppCard = ( - { - ref, - ..._ - }, -) => { - const { t } = useTranslation() - - return ( - - ) -} - -CreateAppCard.displayName = 'CreateAppCard' - -export default CreateAppCard diff --git a/web/app/(commonLayout)/datasets/create-from-pipeline/page.tsx b/web/app/(commonLayout)/datasets/create-from-pipeline/page.tsx new file mode 100644 index 0000000000..72f5ecdfd9 --- /dev/null +++ b/web/app/(commonLayout)/datasets/create-from-pipeline/page.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import CreateFromPipeline from '@/app/components/datasets/create-from-pipeline' + +const DatasetCreation = async () => { + return ( + + ) +} + +export default DatasetCreation diff --git a/web/app/(commonLayout)/datasets/page.tsx b/web/app/(commonLayout)/datasets/page.tsx index 60a542f0a2..8388b69468 100644 --- a/web/app/(commonLayout)/datasets/page.tsx +++ b/web/app/(commonLayout)/datasets/page.tsx @@ -1,12 +1,7 @@ -'use client' -import { useTranslation } from 'react-i18next' -import Container from './Container' -import useDocumentTitle from '@/hooks/use-document-title' +import List from '../../components/datasets/list' -const AppList = () => { - const { t } = useTranslation() - useDocumentTitle(t('common.menus.datasets')) - return +const DatasetList = async () => { + return } -export default AppList +export default DatasetList diff --git a/web/app/(commonLayout)/datasets/store.ts b/web/app/(commonLayout)/datasets/store.ts deleted file mode 100644 index 40b7b15594..0000000000 --- a/web/app/(commonLayout)/datasets/store.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from 'zustand' - -type DatasetStore = { - showExternalApiPanel: boolean - setShowExternalApiPanel: (show: boolean) => void -} - -export const useDatasetStore = create(set => ({ - showExternalApiPanel: false, - setShowExternalApiPanel: show => set({ showExternalApiPanel: show }), -})) diff --git a/web/app/components/app-sidebar/dataset-info.tsx b/web/app/components/app-sidebar/dataset-info.tsx index 73740133ce..3db8789722 100644 --- a/web/app/components/app-sidebar/dataset-info.tsx +++ b/web/app/components/app-sidebar/dataset-info.tsx @@ -3,40 +3,89 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '../base/app-icon' - -const DatasetSvg = - - +import Effect from '../base/effect' +import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' +import type { DataSet } from '@/models/datasets' +import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets' +import { useKnowledge } from '@/hooks/use-knowledge' +import Badge from '../base/badge' +import cn from '@/utils/classnames' type Props = { - isExternal?: boolean - name: string - description: string expand: boolean extraInfo?: React.ReactNode } const DatasetInfo: FC = ({ - name, - description, - isExternal, expand, extraInfo, }) => { const { t } = useTranslation() + const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet + const iconInfo = dataset.icon_info || { + icon: '📙', + icon_type: 'emoji', + icon_background: '#FFF4ED', + icon_url: '', + } + const isExternalProvider = dataset.provider === 'external' + const { formatIndexingTechniqueAndMethod } = useKnowledge() + const chunkingModeIcon = dataset.doc_form ? DOC_FORM_ICON_WITH_BG[dataset.doc_form] : React.Fragment + const Icon = isExternalProvider ? DOC_FORM_ICON_WITH_BG.external : chunkingModeIcon + return ( -
-
- -
+
{expand && ( -
-
- {name} + <> + +
+
+ + {(dataset.doc_form || isExternalProvider) && ( +
+ +
+ )} +
+ <> +
+
+ {dataset.name} +
+
+ {isExternalProvider && t('dataset.externalTag')} + {!isExternalProvider && dataset.doc_form && dataset.indexing_technique && ( +
+ {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)} + {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)} +
+ )} +
+
+

+ {dataset.description} +

+
-
{isExternal ? t('dataset.externalTag') : t('dataset.localDocs')}
-
{description}
-
+ + )} + {!expand && ( + )} {extraInfo}
diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index f58985ed96..fd0fb5285f 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -3,7 +3,6 @@ import { useShallow } from 'zustand/react/shallow' import { RiLayoutLeft2Line, RiLayoutRight2Line } from '@remixicon/react' import NavLink from './navLink' import type { NavIcon } from './navLink' -import AppBasic from './basic' import AppInfo from './app-info' import DatasetInfo from './dataset-info' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -12,39 +11,39 @@ import cn from '@/utils/classnames' export type IAppDetailNavProps = { iconType?: 'app' | 'dataset' | 'notion' - title: string - desc: string - isExternal?: boolean - icon: string - icon_background: string | null navigation: Array<{ name: string href: string icon: NavIcon selectedIcon: NavIcon + disabled?: boolean }> extraInfo?: (modeState: string) => React.ReactNode } -const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => { - const { appSidebarExpand, setAppSiderbarExpand } = useAppStore(useShallow(state => ({ +const AppDetailNav = ({ + navigation, + extraInfo, + iconType = 'app', +}: IAppDetailNavProps) => { + const { appSidebarExpand, setAppSidebarExpand } = useAppStore(useShallow(state => ({ appSidebarExpand: state.appSidebarExpand, - setAppSiderbarExpand: state.setAppSiderbarExpand, + setAppSidebarExpand: state.setAppSidebarExpand, }))) const media = useBreakpoints() const isMobile = media === MediaType.mobile const expand = appSidebarExpand === 'expand' const handleToggle = (state: string) => { - setAppSiderbarExpand(state === 'expand' ? 'collapse' : 'expand') + setAppSidebarExpand(state === 'expand' ? 'collapse' : 'expand') } useEffect(() => { if (appSidebarExpand) { localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand) - setAppSiderbarExpand(appSidebarExpand) + setAppSidebarExpand(appSidebarExpand) } - }, [appSidebarExpand, setAppSiderbarExpand]) + }, [appSidebarExpand, setAppSidebarExpand]) return (
)} - {iconType === 'dataset' && ( + {iconType !== 'app' && ( )} - {!['app', 'dataset'].includes(iconType) && ( - - )}
@@ -94,7 +79,14 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati > {navigation.map((item, index) => { return ( - + ) })} diff --git a/web/app/components/app-sidebar/navLink.tsx b/web/app/components/app-sidebar/navLink.tsx index 295b553b04..a69f0bd6aa 100644 --- a/web/app/components/app-sidebar/navLink.tsx +++ b/web/app/components/app-sidebar/navLink.tsx @@ -6,10 +6,10 @@ import classNames from '@/utils/classnames' import type { RemixiconComponentType } from '@remixicon/react' export type NavIcon = React.ComponentType< -React.PropsWithoutRef> & { - title?: string | undefined - titleId?: string | undefined -}> | RemixiconComponentType + React.PropsWithoutRef> & { + title?: string | undefined + titleId?: string | undefined + }> | RemixiconComponentType export type NavLinkProps = { name: string @@ -19,6 +19,7 @@ export type NavLinkProps = { normal: NavIcon } mode?: string + disabled?: boolean } export default function NavLink({ @@ -26,6 +27,7 @@ export default function NavLink({ href, iconMap, mode = 'expand', + disabled = false, }: NavLinkProps) { const segment = useSelectedLayoutSegment() const formattedSegment = (() => { @@ -39,13 +41,38 @@ export default function NavLink({ const isActive = href.toLowerCase().split('/')?.pop() === formattedSegment const NavIcon = isActive ? iconMap.selected : iconMap.normal + if (disabled) { + return ( + + ) + } + return ( void + readonly?: boolean } const WeightedScore = ({ value, onChange = noop, + readonly = false, }: WeightedScoreProps) => { const { t } = useTranslation() @@ -37,8 +39,9 @@ const WeightedScore = ({ min={0} step={0.1} value={value.value[0]} - onChange={v => onChange({ value: [v, (10 - v * 10) / 10] })} + onChange={v => !readonly && onChange({ value: [v, (10 - v * 10) / 10] })} trackClassName='weightedScoreSliderTrack' + disabled={readonly} />
diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 3170d33a82..cc2fb061b8 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -6,7 +6,7 @@ import { isEqual } from 'lodash-es' import { RiCloseLine } from '@remixicon/react' import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development' import cn from '@/utils/classnames' -import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio' +import IndexMethod from '@/app/components/datasets/settings/index-method' import Divider from '@/app/components/base/divider' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' @@ -245,11 +245,10 @@ const SettingsModal: FC = ({
{t('datasetSettings.form.indexMethod')}
- setIndexMethod(v!)} - docForm={currentDataset.doc_form} currentValue={currentDataset.indexing_technique} />
diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 2b97a64f5b..ac054d014b 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -89,9 +89,9 @@ type PublishConfig = { const Configuration: FC = () => { const { t } = useTranslation() const { notify } = useContext(ToastContext) - const { appDetail, showAppConfigureFeaturesModal, setAppSiderbarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({ + const { appDetail, showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({ appDetail: state.appDetail, - setAppSiderbarExpand: state.setAppSiderbarExpand, + setAppSidebarExpand: state.setAppSidebarExpand, showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal, setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal, }))) @@ -823,7 +823,7 @@ const Configuration: FC = () => { { id: `${Date.now()}-no-repeat`, model: '', provider: '', parameters: {} }, ], ) - setAppSiderbarExpand('collapse') + setAppSidebarExpand('collapse') } if (isLoading) { diff --git a/web/app/components/app/store.ts b/web/app/components/app/store.ts index 5f02f92f0d..a90d560ac7 100644 --- a/web/app/components/app/store.ts +++ b/web/app/components/app/store.ts @@ -15,7 +15,7 @@ type State = { type Action = { setAppDetail: (appDetail?: App & Partial) => void - setAppSiderbarExpand: (state: string) => void + setAppSidebarExpand: (state: string) => void setCurrentLogItem: (item?: IChatItem) => void setCurrentLogModalActiveTab: (tab: string) => void setShowPromptLogModal: (showPromptLogModal: boolean) => void @@ -28,7 +28,7 @@ export const useStore = create(set => ({ appDetail: undefined, setAppDetail: appDetail => set(() => ({ appDetail })), appSidebarExpand: '', - setAppSiderbarExpand: appSidebarExpand => set(() => ({ appSidebarExpand })), + setAppSidebarExpand: appSidebarExpand => set(() => ({ appSidebarExpand })), currentLogItem: undefined, currentLogModalActiveTab: 'DETAIL', setCurrentLogItem: currentLogItem => set(() => ({ currentLogItem })), diff --git a/web/app/components/app/workflow-log/detail.tsx b/web/app/components/app/workflow-log/detail.tsx index dc3eb89a2a..bb5b268d5d 100644 --- a/web/app/components/app/workflow-log/detail.tsx +++ b/web/app/components/app/workflow-log/detail.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { RiCloseLine } from '@remixicon/react' import Run from '@/app/components/workflow/run' +import { useStore } from '@/app/components/app/store' type ILogDetail = { runID: string @@ -11,6 +12,7 @@ type ILogDetail = { const DetailPanel: FC = ({ runID, onClose }) => { const { t } = useTranslation() + const appDetail = useStore(state => state.appDetail) return (
@@ -18,7 +20,10 @@ const DetailPanel: FC = ({ runID, onClose }) => {

{t('appLog.runDetail.workflowTitle')}

- +
) } diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index ac17af1988..f7eaa20917 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -1,11 +1,12 @@ 'use client' - -import type { FC } from 'react' +import React, { type FC, useRef } from 'react' import { init } from 'emoji-mart' import data from '@emoji-mart/data' import { cva } from 'class-variance-authority' import type { AppIconType } from '@/types/app' import classNames from '@/utils/classnames' +import { useHover } from 'ahooks' +import { RiEditLine } from '@remixicon/react' init({ data }) @@ -18,20 +19,43 @@ export type AppIconProps = { imageUrl?: string | null className?: string innerIcon?: React.ReactNode + showEditIcon?: boolean onClick?: () => void } 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 grow-0 shrink-0 overflow-hidden leading-none border-[0.5px] border-divider-regular', + { + variants: { + size: { + xs: 'w-4 h-4 text-xs rounded-[4px]', + tiny: 'w-6 h-6 text-base rounded-md', + small: 'w-8 h-8 text-xl rounded-lg', + medium: 'w-9 h-9 text-[22px] rounded-[10px]', + large: 'w-10 h-10 text-[24px] rounded-[10px]', + xl: 'w-12 h-12 text-[28px] rounded-xl', + xxl: 'w-14 h-14 text-[32px] rounded-2xl', + }, + rounded: { + true: 'rounded-full', + }, + }, + defaultVariants: { + size: 'medium', + rounded: false, + }, + }) +const EditIconWrapperVariants = cva( + 'absolute left-0 top-0 z-10 flex items-center justify-center bg-background-overlay-alt', { variants: { size: { - xs: 'w-4 h-4 text-xs', - tiny: 'w-6 h-6 text-base', - small: 'w-8 h-8 text-xl', - medium: 'w-9 h-9 text-[22px]', - large: 'w-10 h-10 text-[24px]', - xl: 'w-12 h-12 text-[28px]', - xxl: 'w-14 h-14 text-[32px]', + xs: 'w-4 h-4 rounded-[4px]', + tiny: 'w-6 h-6 rounded-md', + small: 'w-8 h-8 rounded-lg', + medium: 'w-9 h-9 rounded-[10px]', + large: 'w-10 h-10 rounded-[10px]', + xl: 'w-12 h-12 rounded-xl', + xxl: 'w-14 h-14 rounded-2xl', }, rounded: { true: 'rounded-full', @@ -42,6 +66,24 @@ const appIconVariants = cva( rounded: false, }, }) +const EditIconVariants = cva( + 'text-text-primary-on-surface', + { + variants: { + size: { + xs: 'size-3', + tiny: 'size-3.5', + small: 'size-5', + medium: 'size-[22px]', + large: 'size-6', + xl: 'size-7', + xxl: 'size-8', + }, + }, + defaultVariants: { + size: 'medium', + }, + }) const AppIcon: FC = ({ size = 'medium', rounded = false, @@ -52,20 +94,34 @@ const AppIcon: FC = ({ className, innerIcon, onClick, + showEditIcon = false, }) => { const isValidImageIcon = iconType === 'image' && imageUrl + const Icon = (icon && icon !== '') ? : + const wrapperRef = useRef(null) + const isHovering = useHover(wrapperRef) - return - {isValidImageIcon - - ? app icon - : (innerIcon || ((icon && icon !== '') ? : )) - } - + return ( + + { + isValidImageIcon + ? app icon + : (innerIcon || Icon) + } + { + showEditIcon && isHovering && ( +
+ +
+ ) + } +
+ ) } -export default AppIcon +export default React.memo(AppIcon) diff --git a/web/app/components/base/corner-label/index.tsx b/web/app/components/base/corner-label/index.tsx index 9e192ed753..0807ed4659 100644 --- a/web/app/components/base/corner-label/index.tsx +++ b/web/app/components/base/corner-label/index.tsx @@ -10,8 +10,8 @@ type CornerLabelProps = { const CornerLabel: React.FC = ({ label, className, labelClassName }) => { return (
- -
+ +
{label}
diff --git a/web/app/components/base/effect/index.tsx b/web/app/components/base/effect/index.tsx new file mode 100644 index 0000000000..95afb1ba5f --- /dev/null +++ b/web/app/components/base/effect/index.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import cn from '@/utils/classnames' + +type EffectProps = { + className?: string +} + +const Effect = ({ + className, +}: EffectProps) => { + return ( +
+ ) +} + +export default React.memo(Effect) diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx index ab4e2aaa42..a9cecfbd0c 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx @@ -110,7 +110,7 @@ const FileUploaderInAttachment = ({ ) } -type FileUploaderInAttachmentWrapperProps = { +export type FileUploaderInAttachmentWrapperProps = { value?: FileEntity[] onChange: (files: FileEntity[]) => void fileConfig: FileUpload diff --git a/web/app/components/base/form/components/field/custom-select.tsx b/web/app/components/base/form/components/field/custom-select.tsx new file mode 100644 index 0000000000..0e605184dc --- /dev/null +++ b/web/app/components/base/form/components/field/custom-select.tsx @@ -0,0 +1,41 @@ +import cn from '@/utils/classnames' +import { useFieldContext } from '../..' +import type { CustomSelectProps, Option } from '../../../select/custom' +import CustomSelect from '../../../select/custom' +import type { LabelProps } from '../label' +import Label from '../label' + +type CustomSelectFieldProps = { + label: string + labelOptions?: Omit + options: T[] + className?: string +} & Omit, 'options' | 'value' | 'onChange'> + +const CustomSelectField = ({ + label, + labelOptions, + options, + className, + ...selectProps +}: CustomSelectFieldProps) => { + const field = useFieldContext() + + return ( +
+
+ ) +} + +export default CustomSelectField diff --git a/web/app/components/base/form/components/field/file-types.tsx b/web/app/components/base/form/components/field/file-types.tsx new file mode 100644 index 0000000000..44c77dc894 --- /dev/null +++ b/web/app/components/base/form/components/field/file-types.tsx @@ -0,0 +1,83 @@ +import cn from '@/utils/classnames' +import type { LabelProps } from '../label' +import { useFieldContext } from '../..' +import Label from '../label' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import FileTypeItem from '@/app/components/workflow/nodes/_base/components/file-type-item' +import { useCallback } from 'react' + +type FieldValue = { + allowedFileTypes: string[], + allowedFileExtensions: string[] +} + +type FileTypesFieldProps = { + label: string + labelOptions?: Omit + className?: string +} + +const FileTypesField = ({ + label, + labelOptions, + className, +}: FileTypesFieldProps) => { + const field = useFieldContext() + + const handleSupportFileTypeChange = useCallback((type: SupportUploadFileTypes) => { + let newAllowFileTypes = [...field.state.value.allowedFileTypes] + if (type === SupportUploadFileTypes.custom) { + if (!newAllowFileTypes.includes(SupportUploadFileTypes.custom)) + newAllowFileTypes = [SupportUploadFileTypes.custom] + else + newAllowFileTypes = newAllowFileTypes.filter(v => v !== type) + } + else { + newAllowFileTypes = newAllowFileTypes.filter(v => v !== SupportUploadFileTypes.custom) + if (newAllowFileTypes.includes(type)) + newAllowFileTypes = newAllowFileTypes.filter(v => v !== type) + else + newAllowFileTypes.push(type) + } + field.handleChange({ + ...field.state.value, + allowedFileTypes: newAllowFileTypes, + }) + }, [field]) + + const handleCustomFileTypesChange = useCallback((customFileTypes: string[]) => { + field.handleChange({ + ...field.state.value, + allowedFileExtensions: customFileTypes, + }) + }, [field]) + + return ( +
+
+ ) +} + +export default FileTypesField diff --git a/web/app/components/base/form/components/field/file-uploader.tsx b/web/app/components/base/form/components/field/file-uploader.tsx new file mode 100644 index 0000000000..2e4e26b5d6 --- /dev/null +++ b/web/app/components/base/form/components/field/file-uploader.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { useFieldContext } from '../..' +import type { LabelProps } from '../label' +import Label from '../label' +import cn from '@/utils/classnames' +import type { FileUploaderInAttachmentWrapperProps } from '../../../file-uploader/file-uploader-in-attachment' +import FileUploaderInAttachmentWrapper from '../../../file-uploader/file-uploader-in-attachment' +import type { FileEntity } from '../../../file-uploader/types' + +type FileUploaderFieldProps = { + label: string + labelOptions?: Omit + className?: string +} & Omit + +const FileUploaderField = ({ + label, + labelOptions, + className, + ...inputProps +}: FileUploaderFieldProps) => { + const field = useFieldContext() + + return ( +
+
+ ) +} + +export default FileUploaderField diff --git a/web/app/components/base/form/components/field/input-type-select/hooks.tsx b/web/app/components/base/form/components/field/input-type-select/hooks.tsx new file mode 100644 index 0000000000..b77ddad4ca --- /dev/null +++ b/web/app/components/base/form/components/field/input-type-select/hooks.tsx @@ -0,0 +1,52 @@ +import { InputTypeEnum } from './types' +import { PipelineInputVarType } from '@/models/pipeline' +import { useTranslation } from 'react-i18next' +import { + RiAlignLeft, + RiCheckboxLine, + RiFileCopy2Line, + RiFileTextLine, + RiHashtag, + RiListCheck3, + RiTextSnippet, +} from '@remixicon/react' + +const i18nFileTypeMap: Record = { + 'number-input': 'number', + 'file': 'single-file', + 'file-list': 'multi-files', +} + +const INPUT_TYPE_ICON = { + [PipelineInputVarType.textInput]: RiTextSnippet, + [PipelineInputVarType.paragraph]: RiAlignLeft, + [PipelineInputVarType.number]: RiHashtag, + [PipelineInputVarType.select]: RiListCheck3, + [PipelineInputVarType.checkbox]: RiCheckboxLine, + [PipelineInputVarType.singleFile]: RiFileTextLine, + [PipelineInputVarType.multiFiles]: RiFileCopy2Line, +} + +const DATA_TYPE = { + [PipelineInputVarType.textInput]: 'string', + [PipelineInputVarType.paragraph]: 'string', + [PipelineInputVarType.number]: 'number', + [PipelineInputVarType.select]: 'string', + [PipelineInputVarType.checkbox]: 'boolean', + [PipelineInputVarType.singleFile]: 'file', + [PipelineInputVarType.multiFiles]: 'array[file]', +} + +export const useInputTypeOptions = (supportFile: boolean) => { + const { t } = useTranslation() + const options = supportFile ? InputTypeEnum.options : InputTypeEnum.exclude(['file', 'file-list']).options + + return options.map((value) => { + return { + value, + label: t(`appDebug.variableConfig.${i18nFileTypeMap[value] || value}`), + Icon: INPUT_TYPE_ICON[value], + type: DATA_TYPE[value], + } + }) +} diff --git a/web/app/components/base/form/components/field/input-type-select/index.tsx b/web/app/components/base/form/components/field/input-type-select/index.tsx new file mode 100644 index 0000000000..d86ea13980 --- /dev/null +++ b/web/app/components/base/form/components/field/input-type-select/index.tsx @@ -0,0 +1,64 @@ +import cn from '@/utils/classnames' +import { useFieldContext } from '../../..' +import type { CustomSelectProps } from '../../../../select/custom' +import CustomSelect from '../../../../select/custom' +import type { LabelProps } from '../../label' +import Label from '../../label' +import { useCallback } from 'react' +import Trigger from './trigger' +import type { FileTypeSelectOption, InputType } from './types' +import { useInputTypeOptions } from './hooks' +import Option from './option' + +type InputTypeSelectFieldProps = { + label: string + labelOptions?: Omit + supportFile: boolean + className?: string +} & Omit, 'options' | 'value' | 'onChange' | 'CustomTrigger' | 'CustomOption'> + +const InputTypeSelectField = ({ + label, + labelOptions, + supportFile, + className, + ...customSelectProps +}: InputTypeSelectFieldProps) => { + const field = useFieldContext() + const inputTypeOptions = useInputTypeOptions(supportFile) + + const renderTrigger = useCallback((option: FileTypeSelectOption | undefined, open: boolean) => { + return + }, []) + const renderOption = useCallback((option: FileTypeSelectOption) => { + return