= ({ embeddingAvailable, documents =
- {
- doc?.data_source_type === DataSourceType.NOTION
- ?
- :
+ {doc?.data_source_type === DataSourceType.NOTION &&
+ }
+ {doc?.data_source_type === DataSourceType.FILE &&
}
+ {doc?.data_source_type === DataSourceType.WEB &&
}
{
doc.name
diff --git a/web/app/components/develop/secret-key/secret-key-modal.tsx b/web/app/components/develop/secret-key/secret-key-modal.tsx
index a291c1fdc56c04..37adaf8822cbdd 100644
--- a/web/app/components/develop/secret-key/secret-key-modal.tsx
+++ b/web/app/components/develop/secret-key/secret-key-modal.tsx
@@ -143,7 +143,7 @@ const SecretKeyModal = ({
)
}
-
+
{t('appApi.apiKeyModal.createNewSecretKey')}
diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx
index 5f06a6f4e87e81..8a69d2f5cbf57e 100644
--- a/web/app/components/explore/app-list/index.tsx
+++ b/web/app/components/explore/app-list/index.tsx
@@ -38,7 +38,7 @@ const Apps = ({
onSuccess,
}: AppsProps) => {
const { t } = useTranslation()
- const { isCurrentWorkspaceManager } = useAppContext()
+ const { isCurrentWorkspaceEditor } = useAppContext()
const { push } = useRouter()
const { hasEditPermission } = useContext(ExploreContext)
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
@@ -116,7 +116,7 @@ const Apps = ({
if (onSuccess)
onSuccess()
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
- getRedirection(isCurrentWorkspaceManager, app, push)
+ getRedirection(isCurrentWorkspaceEditor, app, push)
}
catch (e) {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx
index 8f72421451eae5..f5541999a492bc 100644
--- a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx
+++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx
@@ -1,23 +1,34 @@
-import { useEffect, useState } from 'react'
+'use client'
+import type { FC } from 'react'
+import React, { useEffect, useState } from 'react'
import useSWR from 'swr'
-import { useTranslation } from 'react-i18next'
-import { PlusIcon } from '@heroicons/react/24/solid'
-import cn from 'classnames'
-import Indicator from '../../../indicator'
-import Operate from './operate'
-import s from './style.module.css'
-import NotionIcon from '@/app/components/base/notion-icon'
+import Panel from '../panel'
+import { DataSourceType } from '../panel/types'
import type { DataSourceNotion as TDataSourceNotion } from '@/models/common'
import { useAppContext } from '@/context/app-context'
import { fetchNotionConnection } from '@/service/common'
+import NotionIcon from '@/app/components/base/notion-icon'
-type DataSourceNotionProps = {
+const Icon: FC<{
+ src: string
+ name: string
+ className: string
+}> = ({ src, name, className }) => {
+ return (
+
+ )
+}
+type Props = {
workspaces: TDataSourceNotion[]
}
-const DataSourceNotion = ({
+
+const DataSourceNotion: FC = ({
workspaces,
-}: DataSourceNotionProps) => {
- const { t } = useTranslation()
+}) => {
const { isCurrentWorkspaceManager } = useAppContext()
const [canConnectNotion, setCanConnectNotion] = useState(false)
const { data } = useSWR(canConnectNotion ? '/oauth/data-source/notion' : null, fetchNotionConnection)
@@ -42,95 +53,32 @@ const DataSourceNotion = ({
if (data?.data)
window.location.href = data.data
}, [data])
-
return (
-
-
-
-
-
- {t('common.dataSource.notion.title')}
-
- {
- !connected && (
-
- {t('common.dataSource.notion.description')}
-
- )
- }
-
- {
- connected
- ? (
-
- {t('common.dataSource.connect')}
-
- )
- : (
-
-
- {t('common.dataSource.notion.addWorkspace')}
-
- )
- }
-
- {
- connected && (
-
-
- {t('common.dataSource.notion.connectedWorkspace')}
-
-
-
- )
- }
- {
- connected && (
-
- {
- workspaces.map(workspace => (
-
-
-
{workspace.source_info.workspace_name}
- {
- workspace.is_bound
- ?
- :
- }
-
- {
- workspace.is_bound
- ? t('common.dataSource.notion.connected')
- : t('common.dataSource.notion.disconnected')
- }
-
-
-
-
- ))
- }
-
- )
- }
-
+ ({
+ id: workspace.id,
+ logo: ({ className }: { className: string }) => (
+ ),
+ name: workspace.source_info.workspace_name,
+ isActive: workspace.is_bound,
+ notionConfig: {
+ total: workspace.source_info.total || 0,
+ },
+ }))}
+ onRemove={() => { }} // handled in operation/index.tsx
+ notionActions={{
+ onChangeAuthorizedPage: handleAuthAgain,
+ }}
+ />
)
}
-
-export default DataSourceNotion
+export default React.memo(DataSourceNotion)
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/operate/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/operate/index.tsx
index e115034ff733aa..7b20e5e0a61e13 100644
--- a/web/app/components/header/account-setting/data-source-page/data-source-notion/operate/index.tsx
+++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/operate/index.tsx
@@ -6,17 +6,19 @@ import { EllipsisHorizontalIcon } from '@heroicons/react/24/solid'
import { Menu, Transition } from '@headlessui/react'
import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common'
import Toast from '@/app/components/base/toast'
-import type { DataSourceNotion } from '@/models/common'
import { FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
import { RefreshCw05 } from '@/app/components/base/icons/src/vender/line/arrows'
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
type OperateProps = {
- workspace: DataSourceNotion
+ payload: {
+ id: string
+ total: number
+ }
onAuthAgain: () => void
}
export default function Operate({
- workspace,
+ payload,
onAuthAgain,
}: OperateProps) {
const itemClassName = `
@@ -37,11 +39,11 @@ export default function Operate({
mutate({ url: 'data-source/integrates' })
}
const handleSync = async () => {
- await syncDataSourceNotion({ url: `/oauth/data-source/notion/${workspace.id}/sync` })
+ await syncDataSourceNotion({ url: `/oauth/data-source/notion/${payload.id}/sync` })
updateIntegrates()
}
const handleRemove = async () => {
- await updateDataSourceNotionAction({ url: `/data-source/integrates/${workspace.id}/disable` })
+ await updateDataSourceNotionAction({ url: `/data-source/integrates/${payload.id}/disable` })
updateIntegrates()
}
@@ -79,7 +81,7 @@ export default function Operate({
{t('common.dataSource.notion.changeAuthorizedPages')}
- {workspace.source_info.total} {t('common.dataSource.notion.pagesAuthorized')}
+ {payload.total} {t('common.dataSource.notion.pagesAuthorized')}
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx
new file mode 100644
index 00000000000000..21277c8ec1d442
--- /dev/null
+++ b/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx
@@ -0,0 +1,163 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+} from '@/app/components/base/portal-to-follow-elem'
+import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
+import Button from '@/app/components/base/button'
+import type { FirecrawlConfig } from '@/models/common'
+import Field from '@/app/components/datasets/create/website/firecrawl/base/field'
+import Toast from '@/app/components/base/toast'
+import { createFirecrawlApiKey } from '@/service/datasets'
+import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
+type Props = {
+ onCancel: () => void
+ onSaved: () => void
+}
+
+const I18N_PREFIX = 'datasetCreation.firecrawl'
+
+const DEFAULT_BASE_URL = 'https://api.firecrawl.dev'
+
+const ConfigFirecrawlModal: FC = ({
+ onCancel,
+ onSaved,
+}) => {
+ const { t } = useTranslation()
+ const [isSaving, setIsSaving] = useState(false)
+ const [config, setConfig] = useState({
+ api_key: '',
+ base_url: '',
+ })
+
+ const handleConfigChange = useCallback((key: string) => {
+ return (value: string | number) => {
+ setConfig(prev => ({ ...prev, [key]: value as string }))
+ }
+ }, [])
+
+ const handleSave = useCallback(async () => {
+ if (isSaving)
+ return
+ let errorMsg = ''
+ if (config.base_url && !((config.base_url.startsWith('http://') || config.base_url.startsWith('https://'))))
+ errorMsg = t('common.errorMsg.urlError')
+ if (!errorMsg) {
+ if (!config.api_key) {
+ errorMsg = t('common.errorMsg.fieldRequired', {
+ field: 'API Key',
+ })
+ }
+ else if (!config.api_key.startsWith('fc-')) {
+ errorMsg = t(`${I18N_PREFIX}.apiKeyFormatError`)
+ }
+ }
+
+ if (errorMsg) {
+ Toast.notify({
+ type: 'error',
+ message: errorMsg,
+ })
+ return
+ }
+ const postData = {
+ category: 'website',
+ provider: 'firecrawl',
+ credentials: {
+ auth_type: 'bearer',
+ config: {
+ api_key: config.api_key,
+ base_url: config.base_url || DEFAULT_BASE_URL,
+ },
+ },
+ }
+ try {
+ setIsSaving(true)
+ await createFirecrawlApiKey(postData)
+ Toast.notify({
+ type: 'success',
+ message: t('common.api.success'),
+ })
+ }
+ finally {
+ setIsSaving(false)
+ }
+
+ onSaved()
+ }, [config.api_key, config.base_url, onSaved, t, isSaving])
+
+ return (
+
+
+
+
+
+
+
{t(`${I18N_PREFIX}.configFirecrawl`)}
+
+
+
+
+
+
+
+
+
+
+
+ {t('common.modelProvider.encrypted.front')}
+
+ PKCS1_OAEP
+
+ {t('common.modelProvider.encrypted.back')}
+
+
+
+
+
+
+ )
+}
+export default React.memo(ConfigFirecrawlModal)
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx
new file mode 100644
index 00000000000000..b6ac22436cd91f
--- /dev/null
+++ b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx
@@ -0,0 +1,82 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import cn from 'classnames'
+import Panel from '../panel'
+import { DataSourceType } from '../panel/types'
+import ConfigFirecrawlModal from './config-firecrawl-modal'
+import { fetchFirecrawlApiKey, removeFirecrawlApiKey } from '@/service/datasets'
+
+import type {
+ DataSourceWebsiteItem,
+} from '@/models/common'
+import { useAppContext } from '@/context/app-context'
+
+import {
+ WebsiteProvider,
+} from '@/models/common'
+import Toast from '@/app/components/base/toast'
+
+type Props = {}
+
+const DataSourceWebsite: FC = () => {
+ const { t } = useTranslation()
+ const { isCurrentWorkspaceManager } = useAppContext()
+ const [list, setList] = useState([])
+ const checkSetApiKey = useCallback(async () => {
+ const res = await fetchFirecrawlApiKey() as any
+ const list = res.settings.filter((item: DataSourceWebsiteItem) => item.provider === WebsiteProvider.fireCrawl && !item.disabled)
+ setList(list)
+ }, [])
+
+ useEffect(() => {
+ checkSetApiKey()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const [isShowConfig, {
+ setTrue: showConfig,
+ setFalse: hideConfig,
+ }] = useBoolean(false)
+
+ const handleAdded = useCallback(() => {
+ checkSetApiKey()
+ hideConfig()
+ }, [checkSetApiKey, hideConfig])
+
+ const handleRemove = useCallback(async () => {
+ await removeFirecrawlApiKey(list[0].id)
+ setList([])
+ Toast.notify({
+ type: 'success',
+ message: t('common.api.remove'),
+ })
+ }, [list, t])
+
+ return (
+ <>
+ 0}
+ onConfigure={showConfig}
+ readonly={!isCurrentWorkspaceManager}
+ configuredList={list.map(item => ({
+ id: item.id,
+ logo: ({ className }: { className: string }) => (
+ 🔥
+ ),
+ name: 'FireCrawl',
+ isActive: true,
+ }))}
+ onRemove={handleRemove}
+ />
+ {isShowConfig && (
+
+ )}
+ >
+
+ )
+}
+export default React.memo(DataSourceWebsite)
diff --git a/web/app/components/header/account-setting/data-source-page/index.tsx b/web/app/components/header/account-setting/data-source-page/index.tsx
index 761d9cbfe3ddef..ede83152b223e1 100644
--- a/web/app/components/header/account-setting/data-source-page/index.tsx
+++ b/web/app/components/header/account-setting/data-source-page/index.tsx
@@ -1,6 +1,7 @@
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import DataSourceNotion from './data-source-notion'
+import DataSourceWebsite from './data-source-website'
import { fetchDataSource } from '@/service/common'
export default function DataSourcePage() {
@@ -12,6 +13,7 @@ export default function DataSourcePage() {
{t('common.dataSource.add')}
+
)
}
diff --git a/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx
new file mode 100644
index 00000000000000..376c4aea7b3318
--- /dev/null
+++ b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx
@@ -0,0 +1,78 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import Indicator from '../../../indicator'
+import Operate from '../data-source-notion/operate'
+import { DataSourceType } from './types'
+import s from './style.module.css'
+import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
+
+export type ConfigItemType = {
+ id: string
+ logo: any
+ name: string
+ isActive: boolean
+ notionConfig?: {
+ total: number
+ }
+}
+
+type Props = {
+ type: DataSourceType
+ payload: ConfigItemType
+ onRemove: () => void
+ notionActions?: {
+ onChangeAuthorizedPage: () => void
+ }
+}
+
+const ConfigItem: FC = ({
+ type,
+ payload,
+ onRemove,
+ notionActions,
+}) => {
+ const { t } = useTranslation()
+ const isNotion = type === DataSourceType.notion
+ const isWebsite = type === DataSourceType.website
+ const onChangeAuthorizedPage = notionActions?.onChangeAuthorizedPage || function () { }
+
+ return (
+
+
+
{payload.name}
+ {
+ payload.isActive
+ ?
+ :
+ }
+
+ {
+ payload.isActive
+ ? t(isNotion ? 'common.dataSource.notion.connected' : 'common.dataSource.website.active')
+ : t(isNotion ? 'common.dataSource.notion.disconnected' : 'common.dataSource.website.inactive')
+ }
+
+
+ {isNotion && (
+
+ )}
+
+ {
+ isWebsite && (
+
+
+
+ )
+ }
+
+
+ )
+}
+export default React.memo(ConfigItem)
diff --git a/web/app/components/header/account-setting/data-source-page/panel/index.tsx b/web/app/components/header/account-setting/data-source-page/panel/index.tsx
new file mode 100644
index 00000000000000..b0f6f4ad13ac95
--- /dev/null
+++ b/web/app/components/header/account-setting/data-source-page/panel/index.tsx
@@ -0,0 +1,138 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { PlusIcon } from '@heroicons/react/24/solid'
+import cn from 'classnames'
+import type { ConfigItemType } from './config-item'
+import ConfigItem from './config-item'
+
+import s from './style.module.css'
+import { DataSourceType } from './types'
+
+type Props = {
+ type: DataSourceType
+ isConfigured: boolean
+ onConfigure: () => void
+ readonly: boolean
+ isSupportList?: boolean
+ configuredList: ConfigItemType[]
+ onRemove: () => void
+ notionActions?: {
+ onChangeAuthorizedPage: () => void
+ }
+}
+
+const Panel: FC = ({
+ type,
+ isConfigured,
+ onConfigure,
+ readonly,
+ configuredList,
+ isSupportList,
+ onRemove,
+ notionActions,
+}) => {
+ const { t } = useTranslation()
+ const isNotion = type === DataSourceType.notion
+ const isWebsite = type === DataSourceType.website
+
+ return (
+
+
+
+
+
+
{t(`common.dataSource.${type}.title`)}
+ {isWebsite && (
+
+ {t('common.dataSource.website.with')} 🔥 FireCrawl
+
+ )}
+
+ {
+ !isConfigured && (
+
+ {t(`common.dataSource.${type}.description`)}
+
+ )
+ }
+
+ {isNotion && (
+ <>
+ {
+ isConfigured
+ ? (
+
+ {t('common.dataSource.configure')}
+
+ )
+ : (
+ <>
+ {isSupportList &&
+
+ {t('common.dataSource.notion.addWorkspace')}
+
}
+ >
+ )
+ }
+ >
+ )}
+
+ {isWebsite && !isConfigured && (
+
+ {t('common.dataSource.configure')}
+
+ )}
+
+
+ {
+ isConfigured && (
+
+
+ {isNotion ? t('common.dataSource.notion.connectedWorkspace') : t('common.dataSource.website.configuredCrawlers')}
+
+
+
+ )
+ }
+ {
+ isConfigured && (
+
+ {
+ configuredList.map(item => (
+
+ ))
+ }
+
+ )
+ }
+
+ )
+}
+export default React.memo(Panel)
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/style.module.css b/web/app/components/header/account-setting/data-source-page/panel/style.module.css
similarity index 63%
rename from web/app/components/header/account-setting/data-source-page/data-source-notion/style.module.css
rename to web/app/components/header/account-setting/data-source-page/panel/style.module.css
index ede323072dad24..a11d4758fbaec8 100644
--- a/web/app/components/header/account-setting/data-source-page/data-source-notion/style.module.css
+++ b/web/app/components/header/account-setting/data-source-page/panel/style.module.css
@@ -3,6 +3,11 @@
background-size: 20px 20px;
}
+.website-icon {
+ background: #ffffff url(../../../../datasets/create/assets/web.svg) center center no-repeat;
+ background-size: 20px 20px;
+}
+
.workspace-item {
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
}
diff --git a/web/app/components/header/account-setting/data-source-page/panel/types.ts b/web/app/components/header/account-setting/data-source-page/panel/types.ts
new file mode 100644
index 00000000000000..345bc10f8121dc
--- /dev/null
+++ b/web/app/components/header/account-setting/data-source-page/panel/types.ts
@@ -0,0 +1,4 @@
+export enum DataSourceType {
+ notion = 'notion',
+ website = 'website',
+}
diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx
index 2c10bc24fab6f9..3057e59481112d 100644
--- a/web/app/components/header/account-setting/members-page/index.tsx
+++ b/web/app/components/header/account-setting/members-page/index.tsx
@@ -28,6 +28,7 @@ const MembersPage = () => {
const RoleMap = {
owner: t('common.members.owner'),
admin: t('common.members.admin'),
+ editor: t('common.members.editor'),
normal: t('common.members.normal'),
}
const { locale } = useContext(I18n)
@@ -104,7 +105,7 @@ const MembersPage = () => {
{account.email}
- {dayjs(Number((account.last_login_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}
+ {dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}
{
(owner && account.role !== 'owner')
diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx
index 3b8cb8c699bae0..66830ab79515ed 100644
--- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx
+++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx
@@ -37,6 +37,10 @@ const InviteModal = ({
name: 'normal',
description: t('common.members.normalTip'),
},
+ {
+ name: 'editor',
+ description: t('common.members.editorTip'),
+ },
{
name: 'admin',
description: t('common.members.adminTip'),
@@ -120,7 +124,7 @@ const InviteModal = ({
@@ -130,7 +134,7 @@ const InviteModal = ({
{t(`common.members.${role.name}`)}
-
+
{role.description}
diff --git a/web/app/components/header/account-setting/members-page/operation/index.tsx b/web/app/components/header/account-setting/members-page/operation/index.tsx
index 8201eed0585622..b0e057c2f7a701 100644
--- a/web/app/components/header/account-setting/members-page/operation/index.tsx
+++ b/web/app/components/header/account-setting/members-page/operation/index.tsx
@@ -36,6 +36,7 @@ const Operation = ({
const RoleMap = {
owner: t('common.members.owner'),
admin: t('common.members.admin'),
+ editor: t('common.members.editor'),
normal: t('common.members.normal'),
}
const { notify } = useContext(ToastContext)
@@ -98,7 +99,7 @@ const Operation = ({
>
{
- ['admin', 'normal'].map(role => (
+ ['admin', 'editor', 'normal'].map(role => (
handleUpdateMemberRole(role)}>
{
diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx
index 0174d6a1561540..3d920cc5ecbfa7 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx
@@ -47,6 +47,7 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import { useToastContext } from '@/app/components/base/toast'
import ConfirmCommon from '@/app/components/base/confirm/common'
+import { useAppContext } from '@/context/app-context'
type ModelModalProps = {
provider: ModelProvider
@@ -74,7 +75,8 @@ const ModelModal: FC
= ({
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
currentCustomConfigurationModelFixedFields,
)
- const isEditMode = !!formSchemasValue
+ const { isCurrentWorkspaceManager } = useAppContext()
+ const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
const { t } = useTranslation()
const { notify } = useToastContext()
const language = useLanguage()
@@ -205,7 +207,7 @@ const ModelModal: FC = ({
const encodeSecretValues = useCallback((v: FormValue) => {
const result = { ...v }
extendedSecretFormSchemas.forEach(({ variable }) => {
- if (result[variable] === formSchemasValue?.[variable])
+ if (result[variable] === formSchemasValue?.[variable] && result[variable] !== undefined)
result[variable] = '[__HIDDEN__]'
})
return result
@@ -344,6 +346,7 @@ const ModelModal: FC = ({
|| filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
}
+
>
{t('common.operation.save')}
diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx
index 88cefe147cd892..29696ba5cce88b 100644
--- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx
@@ -23,6 +23,7 @@ import Button from '@/app/components/base/button'
import { useProviderContext } from '@/context/provider-context'
import { updateDefaultModel } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast'
+import { useAppContext } from '@/context/app-context'
type SystemModelSelectorProps = {
textGenerationDefaultModel: DefaultModelResponse | undefined
@@ -40,6 +41,7 @@ const SystemModel: FC = ({
}) => {
const { t } = useTranslation()
const { notify } = useToastContext()
+ const { isCurrentWorkspaceManager } = useAppContext()
const { textGenerationModelList } = useProviderContext()
const updateModelList = useUpdateModelList()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
@@ -248,6 +250,7 @@ const SystemModel: FC = ({
type='primary'
className='!h-8 !text-[13px]'
onClick={handleSave}
+ disabled={!isCurrentWorkspaceManager}
>
{t('common.operation.save')}
diff --git a/web/app/components/header/app-nav/index.tsx b/web/app/components/header/app-nav/index.tsx
index bbf96ec2eac569..8d16235a701f6c 100644
--- a/web/app/components/header/app-nav/index.tsx
+++ b/web/app/components/header/app-nav/index.tsx
@@ -39,7 +39,7 @@ const getKey = (
const AppNav = () => {
const { t } = useTranslation()
const { appId } = useParams()
- const { isCurrentWorkspaceManager } = useAppContext()
+ const { isCurrentWorkspaceEditor } = useAppContext()
const appDetail = useAppStore(state => state.appDetail)
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
@@ -71,8 +71,8 @@ const AppNav = () => {
if (appsData) {
const appItems = flatten(appsData?.map(appData => appData.data))
const navItems = appItems.map((app) => {
- const link = ((isCurrentWorkspaceManager, app) => {
- if (!isCurrentWorkspaceManager) {
+ const link = ((isCurrentWorkspaceEditor, app) => {
+ if (!isCurrentWorkspaceEditor) {
return `/app/${app.id}/overview`
}
else {
@@ -81,7 +81,7 @@ const AppNav = () => {
else
return `/app/${app.id}/configuration`
}
- })(isCurrentWorkspaceManager, app)
+ })(isCurrentWorkspaceEditor, app)
return {
id: app.id,
icon: app.icon,
@@ -93,7 +93,7 @@ const AppNav = () => {
})
setNavItems(navItems)
}
- }, [appsData, isCurrentWorkspaceManager, setNavItems])
+ }, [appsData, isCurrentWorkspaceEditor, setNavItems])
// update current app name
useEffect(() => {
diff --git a/web/app/components/header/app-selector/index.tsx b/web/app/components/header/app-selector/index.tsx
index f9cc1624330ead..38451f2605d48a 100644
--- a/web/app/components/header/app-selector/index.tsx
+++ b/web/app/components/header/app-selector/index.tsx
@@ -17,7 +17,7 @@ type IAppSelectorProps = {
export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
const router = useRouter()
- const { isCurrentWorkspaceManager } = useAppContext()
+ const { isCurrentWorkspaceEditor } = useAppContext()
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
const { t } = useTranslation()
@@ -65,7 +65,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
appItems.map((app: AppDetailResponse) => (
- router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
+ router.push(`/app/${app.id}/${isCurrentWorkspaceEditor ? 'configuration' : 'overview'}`)
}>
@@ -79,7 +79,7 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
))
}
)}
- {isCurrentWorkspaceManager &&
+ {isCurrentWorkspaceEditor &&
setShowNewAppDialog(true)}>
{
- const { isCurrentWorkspaceManager } = useAppContext()
+ const { isCurrentWorkspaceEditor } = useAppContext()
const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints()
@@ -74,7 +74,7 @@ const Header = () => {
- {isCurrentWorkspaceManager &&
}
+ {isCurrentWorkspaceEditor &&
}
)}
@@ -93,7 +93,7 @@ const Header = () => {
- {isCurrentWorkspaceManager &&
}
+ {isCurrentWorkspaceEditor &&
}
)}
diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx
index 6e36677f0e3cb8..7c4de8d0e732db 100644
--- a/web/app/components/header/nav/nav-selector/index.tsx
+++ b/web/app/components/header/nav/nav-selector/index.tsx
@@ -34,7 +34,7 @@ export type INavSelectorProps = {
const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }: INavSelectorProps) => {
const { t } = useTranslation()
const router = useRouter()
- const { isCurrentWorkspaceManager } = useAppContext()
+ const { isCurrentWorkspaceEditor } = useAppContext()
const setAppDetail = useAppStore(state => state.setAppDetail)
const handleScroll = useCallback(debounce((e) => {
@@ -122,7 +122,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
)}
- {isApp && isCurrentWorkspaceManager && (
+ {isApp && isCurrentWorkspaceEditor && (
{({ open }) => (
<>
diff --git a/web/app/components/share/chat/index.tsx b/web/app/components/share/chat/index.tsx
index 6bd554f5cdc397..f2efaa58b72e00 100644
--- a/web/app/components/share/chat/index.tsx
+++ b/web/app/components/share/chat/index.tsx
@@ -73,7 +73,7 @@ const Main: FC = ({
* app info
*/
const [appUnavailable, setAppUnavailable] = useState(false)
- const [isUnknwonReason, setIsUnknwonReason] = useState(false)
+ const [isUnknownReason, setIsUnknwonReason] = useState(false)
const [appId, setAppId] = useState('')
const [isPublicVersion, setIsPublicVersion] = useState(true)
const [siteInfo, setSiteInfo] = useState()
@@ -839,7 +839,7 @@ const Main: FC = ({
}, [appId, messageTaskId, isInstalledApp, installedAppInfo?.id])
if (appUnavailable)
- return
+ return
if (!appId || !siteInfo || !promptConfig) {
return
diff --git a/web/app/components/share/chatbot/index.tsx b/web/app/components/share/chatbot/index.tsx
index 46c4cafd79a5e5..4bff6bbcb1c381 100644
--- a/web/app/components/share/chatbot/index.tsx
+++ b/web/app/components/share/chatbot/index.tsx
@@ -60,7 +60,7 @@ const Main: FC
= ({
* app info
*/
const [appUnavailable, setAppUnavailable] = useState(false)
- const [isUnknwonReason, setIsUnknwonReason] = useState(false)
+ const [isUnknownReason, setIsUnknwonReason] = useState(false)
const [appId, setAppId] = useState('')
const [isPublicVersion, setIsPublicVersion] = useState(true)
const [siteInfo, setSiteInfo] = useState()
@@ -715,7 +715,7 @@ const Main: FC = ({
)
if (appUnavailable)
- return
+ return
if (!appId || !siteInfo || !promptConfig) {
return
diff --git a/web/app/components/tools/provider/custom-create-card.tsx b/web/app/components/tools/provider/custom-create-card.tsx
index f85cd21b1d246f..acfdc3b6ebb0cc 100644
--- a/web/app/components/tools/provider/custom-create-card.tsx
+++ b/web/app/components/tools/provider/custom-create-card.tsx
@@ -11,6 +11,7 @@ import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import { createCustomCollection } from '@/service/tools'
import Toast from '@/app/components/base/toast'
+import { useAppContext } from '@/context/app-context'
type Props = {
onRefreshData: () => void
@@ -20,6 +21,7 @@ const Contribute = ({ onRefreshData }: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const language = getLanguage(locale)
+ const { isCurrentWorkspaceManager } = useAppContext()
const linkUrl = useMemo(() => {
if (language.startsWith('zh_'))
@@ -40,23 +42,25 @@ const Contribute = ({ onRefreshData }: Props) => {
return (
<>
-
-
setIsShowEditCustomCollectionModal(true)}>
-
-
-
+ {isCurrentWorkspaceManager && (
+
+
setIsShowEditCustomCollectionModal(true)}>
+
+
+
{t('tools.createCustomTool')}
-
{t('tools.createCustomTool')}
+
+
-
-
+ )}
{isShowEditCollectionToolModal && (
{isAuthed && }
@@ -251,6 +254,7 @@ const ProviderDetail = ({
setIsShowEditWorkflowToolModal(true)}
+ disabled={!isCurrentWorkspaceManager}
>
{t('tools.createTool.editAction')}
diff --git a/web/app/components/tools/setting/build-in/config-credentials.tsx b/web/app/components/tools/setting/build-in/config-credentials.tsx
index fcfca8f2398dd9..b315852e02593e 100644
--- a/web/app/components/tools/setting/build-in/config-credentials.tsx
+++ b/web/app/components/tools/setting/build-in/config-credentials.tsx
@@ -11,6 +11,7 @@ import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/
import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
+import { useAppContext } from '@/context/app-context'
type Props = {
collection: Collection
@@ -29,6 +30,7 @@ const ConfigCredential: FC
= ({
}) => {
const { t } = useTranslation()
const [credentialSchema, setCredentialSchema] = useState(null)
+ const { isCurrentWorkspaceManager } = useAppContext()
const { name: collectionName } = collection
const [tempCredential, setTempCredential] = React.useState({})
useEffect(() => {
diff --git a/web/app/components/tools/workflow-tool/configure-button.tsx b/web/app/components/tools/workflow-tool/configure-button.tsx
index 38e3b869df3e77..4e6d4bce010655 100644
--- a/web/app/components/tools/workflow-tool/configure-button.tsx
+++ b/web/app/components/tools/workflow-tool/configure-button.tsx
@@ -13,6 +13,7 @@ import Toast from '@/app/components/base/toast'
import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflowToolProvider } from '@/service/tools'
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
import type { InputVar } from '@/app/components/workflow/types'
+import { useAppContext } from '@/context/app-context'
type Props = {
disabled: boolean
@@ -44,6 +45,7 @@ const WorkflowToolConfigureButton = ({
const [showModal, setShowModal] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [detail, setDetail] = useState()
+ const { isCurrentWorkspaceManager } = useAppContext()
const outdated = useMemo(() => {
if (!detail)
@@ -175,22 +177,33 @@ const WorkflowToolConfigureButton = ({
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'cursor-pointer',
!published && 'hover:bg-primary-50',
)}>
- !published && setShowModal(true)}
- >
-
-
{t('workflow.common.workflowAsTool')}
- {!published && (
-
{t('workflow.common.configureRequired').toLocaleUpperCase()}
+ {isCurrentWorkspaceManager
+ ? (
+
!published && setShowModal(true)}
+ >
+
+
{t('workflow.common.workflowAsTool')}
+ {!published && (
+
{t('workflow.common.configureRequired').toLocaleUpperCase()}
+ )}
+
)
+ : (
+
+
+
{t('workflow.common.workflowAsTool')}
+
)}
-
{published && (
setShowModal(true)}
+ disabled={!isCurrentWorkspaceManager}
>
{t('workflow.common.configure')}
{outdated && }
@@ -208,7 +221,7 @@ const WorkflowToolConfigureButton = ({
)}
)}
- {published && isLoading &&
}
+ {published && isLoading &&
}
{showModal && (
{
const store = useStoreApi()
@@ -21,6 +25,7 @@ const CandidateNode = () => {
const candidateNode = useStore(s => s.candidateNode)
const mousePosition = useStore(s => s.mousePosition)
const { zoom } = useViewport()
+ const { handleNodeSelect } = useNodesInteractions()
useEventListener('click', (e) => {
const { candidateNode, mousePosition } = workflowStore.getState()
@@ -49,6 +54,9 @@ const CandidateNode = () => {
})
setNodes(newNodes)
workflowStore.setState({ candidateNode: undefined })
+
+ if (candidateNode.type === CUSTOM_NOTE_NODE)
+ handleNodeSelect(candidateNode.id)
}
})
@@ -73,7 +81,16 @@ const CandidateNode = () => {
transformOrigin: '0 0',
}}
>
-
+ {
+ candidateNode.type === CUSTOM_NODE && (
+
+ )
+ }
+ {
+ candidateNode.type === CUSTOM_NOTE_NODE && (
+
+ )
+ }
)
}
diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts
index 918649a26bbe3b..a6f313e98e4434 100644
--- a/web/app/components/workflow/constants.ts
+++ b/web/app/components/workflow/constants.ts
@@ -391,3 +391,4 @@ export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [
]
export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE'
+export const CUSTOM_NODE = 'custom'
diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts
index 818505d0fe444d..142f96ed2a15b9 100644
--- a/web/app/components/workflow/hooks/use-checklist.ts
+++ b/web/app/components/workflow/hooks/use-checklist.ts
@@ -14,7 +14,10 @@ import {
getToolCheckParams,
getValidTreeNodes,
} from '../utils'
-import { MAX_TREE_DEEPTH } from '../constants'
+import {
+ CUSTOM_NODE,
+ MAX_TREE_DEEPTH,
+} from '../constants'
import type { ToolNodeType } from '../nodes/tool/types'
import { useIsChatMode } from './use-workflow'
import { useNodesExtraData } from './use-nodes-data'
@@ -33,7 +36,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const needWarningNodes = useMemo(() => {
const list = []
- const { validNodes } = getValidTreeNodes(nodes, edges)
+ const { validNodes } = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges)
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
@@ -53,17 +56,20 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
if (provider_type === CollectionType.workflow)
toolIcon = workflowTools.find(tool => tool.id === node.data.provider_id)?.icon
}
- const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
-
- if (errorMessage || !validNodes.find(n => n.id === node.id)) {
- list.push({
- id: node.id,
- type: node.data.type,
- title: node.data.title,
- toolIcon,
- unConnected: !validNodes.find(n => n.id === node.id),
- errorMessage,
- })
+
+ if (node.type === CUSTOM_NODE) {
+ const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
+
+ if (errorMessage || !validNodes.find(n => n.id === node.id)) {
+ list.push({
+ id: node.id,
+ type: node.data.type,
+ title: node.data.title,
+ toolIcon,
+ unConnected: !validNodes.find(n => n.id === node.id),
+ errorMessage,
+ })
+ }
}
}
@@ -107,11 +113,11 @@ export const useChecklistBeforePublish = () => {
getNodes,
edges,
} = store.getState()
- const nodes = getNodes()
+ const nodes = getNodes().filter(node => node.type === CUSTOM_NODE)
const {
validNodes,
maxDepth,
- } = getValidTreeNodes(nodes, edges)
+ } = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges)
if (maxDepth > MAX_TREE_DEEPTH) {
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEEPTH }) })
diff --git a/web/app/components/workflow/hooks/use-node-data-update.ts b/web/app/components/workflow/hooks/use-node-data-update.ts
index 4cb4d6b26bcf94..c59c858184120d 100644
--- a/web/app/components/workflow/hooks/use-node-data-update.ts
+++ b/web/app/components/workflow/hooks/use-node-data-update.ts
@@ -22,7 +22,8 @@ export const useNodeDataUpdate = () => {
const newNodes = produce(getNodes(), (draft) => {
const currentNode = draft.find(node => node.id === id)!
- currentNode.data = { ...currentNode?.data, ...data }
+ if (currentNode)
+ currentNode.data = { ...currentNode.data, ...data }
})
setNodes(newNodes)
}, [store])
diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts
index 64cba1c790dfcb..c63d662e013271 100644
--- a/web/app/components/workflow/hooks/use-nodes-interactions.ts
+++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts
@@ -38,6 +38,7 @@ import {
getNodesConnectedSourceOrTargetHandleIdsMap,
getTopLeftNodePosition,
} from '../utils'
+import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
@@ -71,7 +72,7 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
- if (node.data.isIterationStart)
+ if (node.data.isIterationStart || node.type === CUSTOM_NOTE_NODE)
return
dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
@@ -143,6 +144,9 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
+ if (node.type === CUSTOM_NOTE_NODE)
+ return
+
const {
getNodes,
setNodes,
@@ -193,10 +197,13 @@ export const useNodesInteractions = () => {
setEdges(newEdges)
}, [store, workflowStore, getNodesReadOnly])
- const handleNodeLeave = useCallback(() => {
+ const handleNodeLeave = useCallback((_, node) => {
if (getNodesReadOnly())
return
+ if (node.type === CUSTOM_NOTE_NODE)
+ return
+
const {
setEnteringNodePayload,
} = workflowStore.getState()
@@ -298,6 +305,9 @@ export const useNodesInteractions = () => {
if (targetNode?.data.isIterationStart)
return
+ if (sourceNode?.type === CUSTOM_NOTE_NODE || targetNode?.type === CUSTOM_NOTE_NODE)
+ return
+
const needDeleteEdges = edges.filter((edge) => {
if (
(edge.source === source && edge.sourceHandle === sourceHandle)
@@ -361,6 +371,9 @@ export const useNodesInteractions = () => {
const { getNodes } = store.getState()
const node = getNodes().find(n => n.id === nodeId)!
+ if (node.type === CUSTOM_NOTE_NODE)
+ return
+
if (node.data.type === BlockEnum.VariableAggregator || node.data.type === BlockEnum.VariableAssigner) {
if (handleType === 'target')
return
@@ -975,6 +988,9 @@ export const useNodesInteractions = () => {
}, [store])
const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
+ if (node.type === CUSTOM_NOTE_NODE)
+ return
+
e.preventDefault()
const container = document.querySelector('#workflow-container')
const { x, y } = container!.getBoundingClientRect()
@@ -1051,6 +1067,7 @@ export const useNodesInteractions = () => {
const nodeType = nodeToPaste.data.type
const newNode = generateNewNode({
+ type: nodeToPaste.type,
data: {
...NODES_INITIAL_DATA[nodeType],
...nodeToPaste.data,
diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts
index d7759eb998d2c3..fc995ed976dad8 100644
--- a/web/app/components/workflow/hooks/use-workflow.ts
+++ b/web/app/components/workflow/hooks/use-workflow.ts
@@ -34,8 +34,10 @@ import {
useWorkflowStore,
} from '../store'
import {
+ CUSTOM_NODE,
SUPPORT_OUTPUT_VARS_NODE,
} from '../constants'
+import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
import { useNodesExtraData } from './use-nodes-data'
import { useWorkflowTemplate } from './use-workflow-template'
@@ -88,7 +90,7 @@ export const useWorkflow = () => {
const rankMap = {} as Record
nodes.forEach((node) => {
- if (!node.parentId) {
+ if (!node.parentId && node.type === CUSTOM_NODE) {
const rank = layout.node(node.id).rank!
if (!rankMap[rank]) {
@@ -103,7 +105,7 @@ export const useWorkflow = () => {
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
- if (!node.parentId) {
+ if (!node.parentId && node.type === CUSTOM_NODE) {
const nodeWithPosition = layout.node(node.id)
node.position = {
@@ -345,6 +347,9 @@ export const useWorkflow = () => {
if (targetNode.data.isIterationStart)
return false
+ if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE)
+ return false
+
if (sourceNode && targetNode) {
const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]
diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx
index e9ae8dd84fc8c1..23807e36ffe903 100644
--- a/web/app/components/workflow/index.tsx
+++ b/web/app/components/workflow/index.tsx
@@ -46,6 +46,8 @@ import {
} from './hooks'
import Header from './header'
import CustomNode from './nodes'
+import CustomNoteNode from './note-node'
+import { CUSTOM_NOTE_NODE } from './note-node/constants'
import Operator from './operator'
import CustomEdge from './custom-edge'
import CustomConnectionLine from './custom-connection-line'
@@ -66,6 +68,7 @@ import {
initialNodes,
} from './utils'
import {
+ CUSTOM_NODE,
ITERATION_CHILDREN_Z_INDEX,
WORKFLOW_DATA_UPDATE,
} from './constants'
@@ -76,10 +79,11 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import Confirm from '@/app/components/base/confirm/common'
const nodeTypes = {
- custom: CustomNode,
+ [CUSTOM_NODE]: CustomNode,
+ [CUSTOM_NOTE_NODE]: CustomNoteNode,
}
const edgeTypes = {
- custom: CustomEdge,
+ [CUSTOM_NODE]: CustomEdge,
}
type WorkflowProps = {
diff --git a/web/app/components/workflow/nodes/_base/components/node-resizer.tsx b/web/app/components/workflow/nodes/_base/components/node-resizer.tsx
index 377f5f4c2d8d21..9de94845810887 100644
--- a/web/app/components/workflow/nodes/_base/components/node-resizer.tsx
+++ b/web/app/components/workflow/nodes/_base/components/node-resizer.tsx
@@ -19,10 +19,18 @@ const Icon = () => {
type NodeResizerProps = {
nodeId: string
nodeData: CommonNodeType
+ icon?: JSX.Element
+ minWidth?: number
+ minHeight?: number
+ maxWidth?: number
}
const NodeResizer = ({
nodeId,
nodeData,
+ icon = ,
+ minWidth = 272,
+ minHeight = 176,
+ maxWidth,
}: NodeResizerProps) => {
const { handleNodeResize } = useNodesInteractions()
@@ -39,10 +47,11 @@ const NodeResizer = ({
position='bottom-right'
className='!border-none !bg-transparent'
onResize={handleResize}
- minWidth={272}
- minHeight={176}
+ minWidth={minWidth}
+ minHeight={minHeight}
+ maxWidth={maxWidth}
>
-
+ {icon}
)
diff --git a/web/app/components/workflow/nodes/constants.ts b/web/app/components/workflow/nodes/constants.ts
index c67ce757ab2ce2..a97aa086edac9f 100644
--- a/web/app/components/workflow/nodes/constants.ts
+++ b/web/app/components/workflow/nodes/constants.ts
@@ -64,3 +64,5 @@ export const PanelComponentMap: Record
> = {
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
[BlockEnum.Iteration]: IterationPanel,
}
+
+export const CUSTOM_NODE_TYPE = 'custom'
diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx
index a79651af7007fe..bebc140414fd9b 100644
--- a/web/app/components/workflow/nodes/index.tsx
+++ b/web/app/components/workflow/nodes/index.tsx
@@ -1,6 +1,10 @@
-import { memo } from 'react'
+import {
+ memo,
+ useMemo,
+} from 'react'
import type { NodeProps } from 'reactflow'
import type { Node } from '../types'
+import { CUSTOM_NODE } from '../constants'
import {
NodeComponentMap,
PanelComponentMap,
@@ -23,14 +27,24 @@ const CustomNode = (props: NodeProps) => {
CustomNode.displayName = 'CustomNode'
export const Panel = memo((props: Node) => {
+ const nodeClass = props.type
const nodeData = props.data
- const PanelComponent = PanelComponentMap[nodeData.type]
+ const PanelComponent = useMemo(() => {
+ if (nodeClass === CUSTOM_NODE)
+ return PanelComponentMap[nodeData.type]
- return (
-
-
-
- )
+ return () => null
+ }, [nodeClass, nodeData.type])
+
+ if (nodeClass === CUSTOM_NODE) {
+ return (
+
+
+
+ )
+ }
+
+ return null
})
Panel.displayName = 'Panel'
diff --git a/web/app/components/workflow/note-node/constants.ts b/web/app/components/workflow/note-node/constants.ts
new file mode 100644
index 00000000000000..efd1e01b3c202a
--- /dev/null
+++ b/web/app/components/workflow/note-node/constants.ts
@@ -0,0 +1,42 @@
+import { NoteTheme } from './types'
+
+export const CUSTOM_NOTE_NODE = 'custom-note'
+
+export const THEME_MAP: Record = {
+ [NoteTheme.blue]: {
+ outer: '#2E90FA',
+ title: '#D1E9FF',
+ bg: '#EFF8FF',
+ border: '#84CAFF',
+ },
+ [NoteTheme.cyan]: {
+ outer: '#06AED4',
+ title: '#CFF9FE',
+ bg: '#ECFDFF',
+ border: '#67E3F9',
+ },
+ [NoteTheme.green]: {
+ outer: '#16B364',
+ title: '#D3F8DF',
+ bg: '#EDFCF2',
+ border: '#73E2A3',
+ },
+ [NoteTheme.yellow]: {
+ outer: '#EAAA08',
+ title: '#FEF7C3',
+ bg: '#FEFBE8',
+ border: '#FDE272',
+ },
+ [NoteTheme.pink]: {
+ outer: '#EE46BC',
+ title: '#FCE7F6',
+ bg: '#FDF2FA',
+ border: '#FAA7E0',
+ },
+ [NoteTheme.violet]: {
+ outer: '#875BF7',
+ title: '#ECE9FE',
+ bg: '#F5F3FF',
+ border: '#C3B5FD',
+ },
+}
diff --git a/web/app/components/workflow/note-node/hooks.ts b/web/app/components/workflow/note-node/hooks.ts
new file mode 100644
index 00000000000000..7606951726cfea
--- /dev/null
+++ b/web/app/components/workflow/note-node/hooks.ts
@@ -0,0 +1,29 @@
+import { useCallback } from 'react'
+import type { EditorState } from 'lexical'
+import { useNodeDataUpdate } from '../hooks'
+import type { NoteTheme } from './types'
+
+export const useNote = (id: string) => {
+ const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
+
+ const handleThemeChange = useCallback((theme: NoteTheme) => {
+ handleNodeDataUpdateWithSyncDraft({ id, data: { theme } })
+ }, [handleNodeDataUpdateWithSyncDraft, id])
+
+ const handleEditorChange = useCallback((editorState: EditorState) => {
+ if (!editorState?.isEmpty())
+ handleNodeDataUpdateWithSyncDraft({ id, data: { text: JSON.stringify(editorState) } })
+ else
+ handleNodeDataUpdateWithSyncDraft({ id, data: { text: '' } })
+ }, [handleNodeDataUpdateWithSyncDraft, id])
+
+ const handleShowAuthorChange = useCallback((showAuthor: boolean) => {
+ handleNodeDataUpdateWithSyncDraft({ id, data: { showAuthor } })
+ }, [handleNodeDataUpdateWithSyncDraft, id])
+
+ return {
+ handleThemeChange,
+ handleEditorChange,
+ handleShowAuthorChange,
+ }
+}
diff --git a/web/app/components/workflow/note-node/index.tsx b/web/app/components/workflow/note-node/index.tsx
new file mode 100644
index 00000000000000..850c6b730a67f6
--- /dev/null
+++ b/web/app/components/workflow/note-node/index.tsx
@@ -0,0 +1,127 @@
+import {
+ memo,
+ useCallback,
+ useRef,
+} from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { useClickAway } from 'ahooks'
+import type { NodeProps } from 'reactflow'
+import NodeResizer from '../nodes/_base/components/node-resizer'
+import {
+ useNodeDataUpdate,
+ useNodesInteractions,
+} from '../hooks'
+import { useStore } from '../store'
+import {
+ NoteEditor,
+ NoteEditorContextProvider,
+ NoteEditorToolbar,
+} from './note-editor'
+import { THEME_MAP } from './constants'
+import { useNote } from './hooks'
+import type { NoteNodeType } from './types'
+
+const Icon = () => {
+ return (
+
+
+
+ )
+}
+
+const NoteNode = ({
+ id,
+ data,
+}: NodeProps) => {
+ const { t } = useTranslation()
+ const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
+ const ref = useRef(null)
+ const theme = data.theme
+ const {
+ handleThemeChange,
+ handleEditorChange,
+ handleShowAuthorChange,
+ } = useNote(id)
+ const {
+ handleNodesCopy,
+ handleNodesDuplicate,
+ handleNodeDelete,
+ } = useNodesInteractions()
+ const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
+
+ const handleDeleteNode = useCallback(() => {
+ handleNodeDelete(id)
+ }, [id, handleNodeDelete])
+
+ useClickAway(() => {
+ handleNodeDataUpdateWithSyncDraft({ id, data: { selected: false } })
+ }, ref)
+
+ return (
+
+
+ <>
+ }
+ minWidth={240}
+ maxWidth={640}
+ minHeight={88}
+ />
+
+ {
+ data.selected && (
+
+
+
+ )
+ }
+
+ {
+ data.showAuthor && (
+
+ {data.author}
+
+ )
+ }
+ >
+
+
+ )
+}
+
+export default memo(NoteNode)
diff --git a/web/app/components/workflow/note-node/note-editor/context.tsx b/web/app/components/workflow/note-node/note-editor/context.tsx
new file mode 100644
index 00000000000000..0d892b3ae680e5
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/context.tsx
@@ -0,0 +1,65 @@
+'use client'
+
+import {
+ createContext,
+ memo,
+ useRef,
+} from 'react'
+import { LexicalComposer } from '@lexical/react/LexicalComposer'
+import { LinkNode } from '@lexical/link'
+import {
+ ListItemNode,
+ ListNode,
+} from '@lexical/list'
+import { createNoteEditorStore } from './store'
+import theme from './theme'
+
+type NoteEditorStore = ReturnType
+const NoteEditorContext = createContext(null)
+
+type NoteEditorContextProviderProps = {
+ value: string
+ children: JSX.Element | string | (JSX.Element | string)[]
+}
+export const NoteEditorContextProvider = memo(({
+ value,
+ children,
+}: NoteEditorContextProviderProps) => {
+ const storeRef = useRef()
+
+ if (!storeRef.current)
+ storeRef.current = createNoteEditorStore()
+
+ let initialValue = null
+ try {
+ initialValue = JSON.parse(value)
+ }
+ catch (e) {
+
+ }
+
+ const initialConfig = {
+ namespace: 'note-editor',
+ nodes: [
+ LinkNode,
+ ListNode,
+ ListItemNode,
+ ],
+ editorState: !initialValue?.root.children.length ? null : JSON.stringify(initialValue),
+ onError: (error: Error) => {
+ throw error
+ },
+ theme,
+ }
+
+ return (
+
+
+ {children}
+
+
+ )
+})
+NoteEditorContextProvider.displayName = 'NoteEditorContextProvider'
+
+export default NoteEditorContext
diff --git a/web/app/components/workflow/note-node/note-editor/editor.tsx b/web/app/components/workflow/note-node/note-editor/editor.tsx
new file mode 100644
index 00000000000000..189cc78c42e54e
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/editor.tsx
@@ -0,0 +1,62 @@
+'use client'
+
+import {
+ memo,
+ useCallback,
+} from 'react'
+import type { EditorState } from 'lexical'
+import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
+import { ContentEditable } from '@lexical/react/LexicalContentEditable'
+import { ClickableLinkPlugin } from '@lexical/react/LexicalClickableLinkPlugin'
+import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
+import { ListPlugin } from '@lexical/react/LexicalListPlugin'
+import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
+import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
+import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
+import LinkEditorPlugin from './plugins/link-editor-plugin'
+import FormatDetectorPlugin from './plugins/format-detector-plugin'
+// import TreeView from '@/app/components/base/prompt-editor/plugins/tree-view'
+import Placeholder from '@/app/components/base/prompt-editor/plugins/placeholder'
+
+type EditorProps = {
+ placeholder?: string
+ onChange?: (editorState: EditorState) => void
+ containerElement: HTMLDivElement | null
+}
+const Editor = ({
+ placeholder = 'write you note...',
+ onChange,
+ containerElement,
+}: EditorProps) => {
+ const handleEditorChange = useCallback((editorState: EditorState) => {
+ onChange?.(editorState)
+ }, [onChange])
+
+ return (
+
+
+
+
+ }
+ placeholder={ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+
+
+
+
+
+
+
+ {/* */}
+
+ )
+}
+
+export default memo(Editor)
diff --git a/web/app/components/workflow/note-node/note-editor/index.tsx b/web/app/components/workflow/note-node/note-editor/index.tsx
new file mode 100644
index 00000000000000..f3c7364e8e0691
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/index.tsx
@@ -0,0 +1,3 @@
+export { NoteEditorContextProvider } from './context'
+export { default as NoteEditor } from './editor'
+export { default as NoteEditorToolbar } from './toolbar'
diff --git a/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts
new file mode 100644
index 00000000000000..bc7e855c3bc941
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts
@@ -0,0 +1,78 @@
+import {
+ useCallback,
+ useEffect,
+} from 'react'
+import {
+ $getSelection,
+ $isRangeSelection,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { LinkNode } from '@lexical/link'
+import { $isLinkNode } from '@lexical/link'
+import { $isListItemNode } from '@lexical/list'
+import { getSelectedNode } from '../../utils'
+import { useNoteEditorStore } from '../../store'
+
+export const useFormatDetector = () => {
+ const [editor] = useLexicalComposerContext()
+ const noteEditorStore = useNoteEditorStore()
+
+ const handleFormat = useCallback(() => {
+ editor.getEditorState().read(() => {
+ if (editor.isComposing())
+ return
+
+ const selection = $getSelection()
+
+ if ($isRangeSelection(selection)) {
+ const node = getSelectedNode(selection)
+ const {
+ setSelectedIsBold,
+ setSelectedIsItalic,
+ setSelectedIsStrikeThrough,
+ setSelectedLinkUrl,
+ setSelectedIsLink,
+ setSelectedIsBullet,
+ } = noteEditorStore.getState()
+ setSelectedIsBold(selection.hasFormat('bold'))
+ setSelectedIsItalic(selection.hasFormat('italic'))
+ setSelectedIsStrikeThrough(selection.hasFormat('strikethrough'))
+ const parent = node.getParent()
+ if ($isLinkNode(parent) || $isLinkNode(node)) {
+ const linkUrl = ($isLinkNode(parent) ? parent : node as LinkNode).getURL()
+ setSelectedLinkUrl(linkUrl)
+ setSelectedIsLink(true)
+ }
+ else {
+ setSelectedLinkUrl('')
+ setSelectedIsLink(false)
+ }
+
+ if ($isListItemNode(parent) || $isListItemNode(node))
+ setSelectedIsBullet(true)
+ else
+ setSelectedIsBullet(false)
+ }
+ })
+ }, [editor, noteEditorStore])
+
+ useEffect(() => {
+ document.addEventListener('selectionchange', handleFormat)
+ return () => {
+ document.removeEventListener('selectionchange', handleFormat)
+ }
+ }, [handleFormat])
+
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerUpdateListener(() => {
+ handleFormat()
+ }),
+ )
+ }, [editor, handleFormat])
+
+ return {
+ handleFormat,
+ }
+}
diff --git a/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx
new file mode 100644
index 00000000000000..3a2585c4b5ab6f
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx
@@ -0,0 +1,9 @@
+import { useFormatDetector } from './hooks'
+
+const FormatDetectorPlugin = () => {
+ useFormatDetector()
+
+ return null
+}
+
+export default FormatDetectorPlugin
diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx
new file mode 100644
index 00000000000000..5819bc8bde715d
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx
@@ -0,0 +1,152 @@
+import {
+ memo,
+ useEffect,
+ useState,
+} from 'react'
+import { escape } from 'lodash-es'
+import {
+ FloatingPortal,
+ flip,
+ offset,
+ shift,
+ useFloating,
+} from '@floating-ui/react'
+import { useTranslation } from 'react-i18next'
+import { useClickAway } from 'ahooks'
+import cn from 'classnames'
+import { useStore } from '../../store'
+import { useLink } from './hooks'
+import Button from '@/app/components/base/button'
+import {
+ Edit03,
+ LinkBroken01,
+ LinkExternal01,
+} from '@/app/components/base/icons/src/vender/line/general'
+
+type LinkEditorComponentProps = {
+ containerElement: HTMLDivElement | null
+}
+const LinkEditorComponent = ({
+ containerElement,
+}: LinkEditorComponentProps) => {
+ const { t } = useTranslation()
+ const {
+ handleSaveLink,
+ handleUnlink,
+ } = useLink()
+ const selectedLinkUrl = useStore(s => s.selectedLinkUrl)
+ const linkAnchorElement = useStore(s => s.linkAnchorElement)
+ const linkOperatorShow = useStore(s => s.linkOperatorShow)
+ const setLinkAnchorElement = useStore(s => s.setLinkAnchorElement)
+ const setLinkOperatorShow = useStore(s => s.setLinkOperatorShow)
+ const [url, setUrl] = useState(selectedLinkUrl)
+ const { refs, floatingStyles, elements } = useFloating({
+ placement: 'top',
+ middleware: [
+ offset(4),
+ shift(),
+ flip(),
+ ],
+ })
+
+ useClickAway(() => {
+ setLinkAnchorElement()
+ }, linkAnchorElement)
+
+ useEffect(() => {
+ setUrl(selectedLinkUrl)
+ }, [selectedLinkUrl])
+
+ useEffect(() => {
+ if (linkAnchorElement)
+ refs.setReference(linkAnchorElement)
+ }, [linkAnchorElement, refs])
+
+ return (
+ <>
+ {
+ elements.reference && (
+
+
+ {
+ !linkOperatorShow && (
+ <>
+
setUrl(e.target.value)}
+ placeholder={t('workflow.nodes.note.editor.enterUrl') || ''}
+ autoFocus
+ />
+
handleSaveLink(url)}
+ >
+ {t('common.operation.ok')}
+
+ >
+ )
+ }
+ {
+ linkOperatorShow && (
+ <>
+
+
+
+ {t('workflow.nodes.note.editor.openLink')}
+
+
+ {escape(url)}
+
+
+
+
{
+ e.stopPropagation()
+ setLinkOperatorShow(false)
+ }}
+ >
+
+ {t('common.operation.edit')}
+
+
+
+ {t('workflow.nodes.note.editor.unlink')}
+
+ >
+ )
+ }
+
+
+ )
+ }
+ >
+ )
+}
+
+export default memo(LinkEditorComponent)
diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts
new file mode 100644
index 00000000000000..8be8b551963b67
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts
@@ -0,0 +1,115 @@
+import {
+ useCallback,
+ useEffect,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ CLICK_COMMAND,
+ COMMAND_PRIORITY_LOW,
+} from 'lexical'
+import {
+ mergeRegister,
+} from '@lexical/utils'
+import {
+ TOGGLE_LINK_COMMAND,
+} from '@lexical/link'
+import { escape } from 'lodash-es'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useNoteEditorStore } from '../../store'
+import { urlRegExp } from '../../utils'
+import { useToastContext } from '@/app/components/base/toast'
+
+export const useOpenLink = () => {
+ const [editor] = useLexicalComposerContext()
+ const noteEditorStore = useNoteEditorStore()
+
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerUpdateListener(() => {
+ setTimeout(() => {
+ const {
+ selectedLinkUrl,
+ selectedIsLink,
+ setLinkAnchorElement,
+ setLinkOperatorShow,
+ } = noteEditorStore.getState()
+
+ if (selectedIsLink) {
+ setLinkAnchorElement(true)
+
+ if (selectedLinkUrl)
+ setLinkOperatorShow(true)
+ else
+ setLinkOperatorShow(false)
+ }
+ else {
+ setLinkAnchorElement()
+ setLinkOperatorShow(false)
+ }
+ })
+ }),
+ editor.registerCommand(
+ CLICK_COMMAND,
+ (payload) => {
+ setTimeout(() => {
+ const {
+ selectedLinkUrl,
+ selectedIsLink,
+ setLinkAnchorElement,
+ setLinkOperatorShow,
+ } = noteEditorStore.getState()
+
+ if (selectedIsLink) {
+ if ((payload.metaKey || payload.ctrlKey) && selectedLinkUrl) {
+ window.open(selectedLinkUrl, '_blank')
+ return true
+ }
+ setLinkAnchorElement(true)
+
+ if (selectedLinkUrl)
+ setLinkOperatorShow(true)
+ else
+ setLinkOperatorShow(false)
+ }
+ else {
+ setLinkAnchorElement()
+ setLinkOperatorShow(false)
+ }
+ })
+ return false
+ },
+ COMMAND_PRIORITY_LOW,
+ ),
+ )
+ }, [editor, noteEditorStore])
+}
+
+export const useLink = () => {
+ const { t } = useTranslation()
+ const [editor] = useLexicalComposerContext()
+ const noteEditorStore = useNoteEditorStore()
+ const { notify } = useToastContext()
+
+ const handleSaveLink = useCallback((url: string) => {
+ if (url && !urlRegExp.test(url)) {
+ notify({ type: 'error', message: t('workflow.nodes.note.editor.invalidUrl') })
+ return
+ }
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, escape(url))
+
+ const { setLinkAnchorElement } = noteEditorStore.getState()
+ setLinkAnchorElement()
+ }, [editor, noteEditorStore, notify, t])
+
+ const handleUnlink = useCallback(() => {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
+
+ const { setLinkAnchorElement } = noteEditorStore.getState()
+ setLinkAnchorElement()
+ }, [editor, noteEditorStore])
+
+ return {
+ handleSaveLink,
+ handleUnlink,
+ }
+}
diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx
new file mode 100644
index 00000000000000..a5b3df6504ae95
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx
@@ -0,0 +1,25 @@
+import {
+ memo,
+} from 'react'
+import { useStore } from '../../store'
+import { useOpenLink } from './hooks'
+import LinkEditorComponent from './component'
+
+type LinkEditorPluginProps = {
+ containerElement: HTMLDivElement | null
+}
+const LinkEditorPlugin = ({
+ containerElement,
+}: LinkEditorPluginProps) => {
+ useOpenLink()
+ const linkAnchorElement = useStore(s => s.linkAnchorElement)
+
+ if (!linkAnchorElement)
+ return null
+
+ return (
+
+ )
+}
+
+export default memo(LinkEditorPlugin)
diff --git a/web/app/components/workflow/note-node/note-editor/store.ts b/web/app/components/workflow/note-node/note-editor/store.ts
new file mode 100644
index 00000000000000..3507bb7c0cb177
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/store.ts
@@ -0,0 +1,72 @@
+import { useContext } from 'react'
+import {
+ useStore as useZustandStore,
+} from 'zustand'
+import { createStore } from 'zustand/vanilla'
+import NoteEditorContext from './context'
+
+type Shape = {
+ linkAnchorElement: HTMLElement | null
+ setLinkAnchorElement: (open?: boolean) => void
+ linkOperatorShow: boolean
+ setLinkOperatorShow: (linkOperatorShow: boolean) => void
+ selectedIsBold: boolean
+ setSelectedIsBold: (selectedIsBold: boolean) => void
+ selectedIsItalic: boolean
+ setSelectedIsItalic: (selectedIsItalic: boolean) => void
+ selectedIsStrikeThrough: boolean
+ setSelectedIsStrikeThrough: (selectedIsStrikeThrough: boolean) => void
+ selectedLinkUrl: string
+ setSelectedLinkUrl: (selectedLinkUrl: string) => void
+ selectedIsLink: boolean
+ setSelectedIsLink: (selectedIsLink: boolean) => void
+ selectedIsBullet: boolean
+ setSelectedIsBullet: (selectedIsBullet: boolean) => void
+}
+
+export const createNoteEditorStore = () => {
+ return createStore
(set => ({
+ linkAnchorElement: null,
+ setLinkAnchorElement: (open) => {
+ if (open) {
+ setTimeout(() => {
+ const nativeSelection = window.getSelection()
+
+ if (nativeSelection?.focusNode) {
+ const parent = nativeSelection.focusNode.parentElement
+ set(() => ({ linkAnchorElement: parent }))
+ }
+ })
+ }
+ else {
+ set(() => ({ linkAnchorElement: null }))
+ }
+ },
+ linkOperatorShow: false,
+ setLinkOperatorShow: linkOperatorShow => set(() => ({ linkOperatorShow })),
+ selectedIsBold: false,
+ setSelectedIsBold: selectedIsBold => set(() => ({ selectedIsBold })),
+ selectedIsItalic: false,
+ setSelectedIsItalic: selectedIsItalic => set(() => ({ selectedIsItalic })),
+ selectedIsStrikeThrough: false,
+ setSelectedIsStrikeThrough: selectedIsStrikeThrough => set(() => ({ selectedIsStrikeThrough })),
+ selectedLinkUrl: '',
+ setSelectedLinkUrl: selectedLinkUrl => set(() => ({ selectedLinkUrl })),
+ selectedIsLink: false,
+ setSelectedIsLink: selectedIsLink => set(() => ({ selectedIsLink })),
+ selectedIsBullet: false,
+ setSelectedIsBullet: selectedIsBullet => set(() => ({ selectedIsBullet })),
+ }))
+}
+
+export function useStore(selector: (state: Shape) => T): T {
+ const store = useContext(NoteEditorContext)
+ if (!store)
+ throw new Error('Missing NoteEditorContext.Provider in the tree')
+
+ return useZustandStore(store, selector)
+}
+
+export const useNoteEditorStore = () => {
+ return useContext(NoteEditorContext)!
+}
diff --git a/web/app/components/workflow/note-node/note-editor/theme/index.ts b/web/app/components/workflow/note-node/note-editor/theme/index.ts
new file mode 100644
index 00000000000000..5cb8dec37f450a
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/theme/index.ts
@@ -0,0 +1,18 @@
+import type { EditorThemeClasses } from 'lexical'
+
+import './theme.css'
+
+const theme: EditorThemeClasses = {
+ paragraph: 'note-editor-theme_paragraph',
+ list: {
+ ul: 'note-editor-theme_list-ul',
+ listitem: 'note-editor-theme_list-li',
+ },
+ link: 'note-editor-theme_link',
+ text: {
+ italic: 'note-editor-theme_text-italic',
+ strikethrough: 'note-editor-theme_text-strikethrough',
+ },
+}
+
+export default theme
diff --git a/web/app/components/workflow/note-node/note-editor/theme/theme.css b/web/app/components/workflow/note-node/note-editor/theme/theme.css
new file mode 100644
index 00000000000000..8c42757c30ba1b
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/theme/theme.css
@@ -0,0 +1,28 @@
+.note-editor-theme_paragraph {
+ font-size: 12px;
+}
+
+.note-editor-theme_list-ul {
+ font-size: 12px;
+ margin: 0;
+ padding: 0;
+ list-style: disc;
+}
+
+.note-editor-theme_list-li {
+ margin-left: 18px;
+ margin-right: 8px;
+}
+
+.note-editor-theme_link {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.note-editor-theme_text-strikethrough {
+ text-decoration: line-through;
+}
+
+.note-editor-theme_text-italic {
+ font-style: italic;
+}
\ No newline at end of file
diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx
new file mode 100644
index 00000000000000..429188a89b5637
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx
@@ -0,0 +1,105 @@
+import {
+ memo,
+ useState,
+} from 'react'
+import cn from 'classnames'
+import { NoteTheme } from '../../types'
+import { THEME_MAP } from '../../constants'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+ PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+
+export const COLOR_LIST = [
+ {
+ key: NoteTheme.blue,
+ inner: THEME_MAP[NoteTheme.blue].title,
+ outer: THEME_MAP[NoteTheme.blue].outer,
+ },
+ {
+ key: NoteTheme.cyan,
+ inner: THEME_MAP[NoteTheme.cyan].title,
+ outer: THEME_MAP[NoteTheme.cyan].outer,
+ },
+ {
+ key: NoteTheme.green,
+ inner: THEME_MAP[NoteTheme.green].title,
+ outer: THEME_MAP[NoteTheme.green].outer,
+ },
+ {
+ key: NoteTheme.yellow,
+ inner: THEME_MAP[NoteTheme.yellow].title,
+ outer: THEME_MAP[NoteTheme.yellow].outer,
+ },
+ {
+ key: NoteTheme.pink,
+ inner: THEME_MAP[NoteTheme.pink].title,
+ outer: THEME_MAP[NoteTheme.pink].outer,
+ },
+ {
+ key: NoteTheme.violet,
+ inner: THEME_MAP[NoteTheme.violet].title,
+ outer: THEME_MAP[NoteTheme.violet].outer,
+ },
+]
+
+export type ColorPickerProps = {
+ theme: NoteTheme
+ onThemeChange: (theme: NoteTheme) => void
+}
+const ColorPicker = ({
+ theme,
+ onThemeChange,
+}: ColorPickerProps) => {
+ const [open, setOpen] = useState(false)
+
+ return (
+
+ setOpen(!open)}>
+
+
+
+
+ {
+ COLOR_LIST.map(color => (
+
{
+ e.stopPropagation()
+ onThemeChange(color.key)
+ setOpen(false)
+ }}
+ >
+
+
+
+ ))
+ }
+
+
+
+ )
+}
+
+export default memo(ColorPicker)
diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/command.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/command.tsx
new file mode 100644
index 00000000000000..96ad5e8d92b86c
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/toolbar/command.tsx
@@ -0,0 +1,81 @@
+import {
+ memo,
+ useMemo,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import { useStore } from '../store'
+import { useCommand } from './hooks'
+import { Link01 } from '@/app/components/base/icons/src/vender/line/general'
+import {
+ Bold01,
+ Dotpoints01,
+ Italic01,
+ Strikethrough01,
+} from '@/app/components/base/icons/src/vender/line/editor'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
+
+type CommandProps = {
+ type: 'bold' | 'italic' | 'strikethrough' | 'link' | 'bullet'
+}
+const Command = ({
+ type,
+}: CommandProps) => {
+ const { t } = useTranslation()
+ const selectedIsBold = useStore(s => s.selectedIsBold)
+ const selectedIsItalic = useStore(s => s.selectedIsItalic)
+ const selectedIsStrikeThrough = useStore(s => s.selectedIsStrikeThrough)
+ const selectedIsLink = useStore(s => s.selectedIsLink)
+ const selectedIsBullet = useStore(s => s.selectedIsBullet)
+ const { handleCommand } = useCommand()
+
+ const icon = useMemo(() => {
+ switch (type) {
+ case 'bold':
+ return
+ case 'italic':
+ return
+ case 'strikethrough':
+ return
+ case 'link':
+ return
+ case 'bullet':
+ return
+ }
+ }, [type, selectedIsBold, selectedIsItalic, selectedIsStrikeThrough, selectedIsLink, selectedIsBullet])
+
+ const tip = useMemo(() => {
+ switch (type) {
+ case 'bold':
+ return t('workflow.nodes.note.editor.bold')
+ case 'italic':
+ return t('workflow.nodes.note.editor.italic')
+ case 'strikethrough':
+ return t('workflow.nodes.note.editor.strikethrough')
+ case 'link':
+ return t('workflow.nodes.note.editor.link')
+ case 'bullet':
+ return t('workflow.nodes.note.editor.bulletList')
+ }
+ }, [type, t])
+
+ return (
+
+ handleCommand(type)}
+ >
+ {icon}
+
+
+ )
+}
+
+export default memo(Command)
diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx
new file mode 100644
index 00000000000000..aefdb46b0aae82
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx
@@ -0,0 +1,7 @@
+const Divider = () => {
+ return (
+
+ )
+}
+
+export default Divider
diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx
new file mode 100644
index 00000000000000..c6284a9a74b72d
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx
@@ -0,0 +1,86 @@
+import { memo } from 'react'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { useFontSize } from './hooks'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+ PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import { TitleCase } from '@/app/components/base/icons/src/vender/line/editor'
+import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
+import { Check } from '@/app/components/base/icons/src/vender/line/general'
+
+const FontSizeSelector = () => {
+ const { t } = useTranslation()
+ const FONT_SIZE_LIST = [
+ {
+ key: '12px',
+ value: t('workflow.nodes.note.editor.small'),
+ },
+ {
+ key: '14px',
+ value: t('workflow.nodes.note.editor.medium'),
+ },
+ {
+ key: '16px',
+ value: t('workflow.nodes.note.editor.large'),
+ },
+ ]
+ const {
+ fontSizeSelectorShow,
+ handleOpenFontSizeSelector,
+ fontSize,
+ handleFontSize,
+ } = useFontSize()
+
+ return (
+
+ handleOpenFontSizeSelector(!fontSizeSelectorShow)}>
+
+
+ {FONT_SIZE_LIST.find(font => font.key === fontSize)?.value || t('workflow.nodes.note.editor.small')}
+
+
+
+
+
+ {
+ FONT_SIZE_LIST.map(font => (
+
{
+ e.stopPropagation()
+ handleFontSize(font.key)
+ handleOpenFontSizeSelector(false)
+ }}
+ >
+
+ {font.value}
+
+ {
+ fontSize === font.key && (
+
+ )
+ }
+
+ ))
+ }
+
+
+
+ )
+}
+
+export default memo(FontSizeSelector)
diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts b/web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts
new file mode 100644
index 00000000000000..8ed942d8d62e08
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts
@@ -0,0 +1,147 @@
+import {
+ useCallback,
+ useEffect,
+ useState,
+} from 'react'
+import {
+ $createParagraphNode,
+ $getSelection,
+ $isRangeSelection,
+ $setSelection,
+ COMMAND_PRIORITY_CRITICAL,
+ FORMAT_TEXT_COMMAND,
+ SELECTION_CHANGE_COMMAND,
+} from 'lexical'
+import {
+ $getSelectionStyleValueForProperty,
+ $patchStyleText,
+ $setBlocksType,
+} from '@lexical/selection'
+import { INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list'
+import { mergeRegister } from '@lexical/utils'
+import {
+ $isLinkNode,
+ TOGGLE_LINK_COMMAND,
+} from '@lexical/link'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useNoteEditorStore } from '../store'
+import { getSelectedNode } from '../utils'
+
+export const useCommand = () => {
+ const [editor] = useLexicalComposerContext()
+ const noteEditorStore = useNoteEditorStore()
+
+ const handleCommand = useCallback((type: string) => {
+ if (type === 'bold')
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
+
+ if (type === 'italic')
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
+
+ if (type === 'strikethrough')
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
+
+ if (type === 'link') {
+ editor.update(() => {
+ const selection = $getSelection()
+
+ if ($isRangeSelection(selection)) {
+ const node = getSelectedNode(selection)
+ const parent = node.getParent()
+ const { setLinkAnchorElement } = noteEditorStore.getState()
+
+ if ($isLinkNode(parent) || $isLinkNode(node)) {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
+ setLinkAnchorElement()
+ }
+ else {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, '')
+ setLinkAnchorElement(true)
+ }
+ }
+ })
+ }
+
+ if (type === 'bullet') {
+ const { selectedIsBullet } = noteEditorStore.getState()
+
+ if (selectedIsBullet) {
+ editor.update(() => {
+ const selection = $getSelection()
+ if ($isRangeSelection(selection))
+ $setBlocksType(selection, () => $createParagraphNode())
+ })
+ }
+ else {
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
+ }
+ }
+ }, [editor, noteEditorStore])
+
+ return {
+ handleCommand,
+ }
+}
+
+export const useFontSize = () => {
+ const [editor] = useLexicalComposerContext()
+ const [fontSize, setFontSize] = useState('12px')
+ const [fontSizeSelectorShow, setFontSizeSelectorShow] = useState(false)
+
+ const handleFontSize = useCallback((fontSize: string) => {
+ editor.update(() => {
+ const selection = $getSelection()
+
+ if ($isRangeSelection(selection))
+ $patchStyleText(selection, { 'font-size': fontSize })
+ })
+ }, [editor])
+
+ const handleOpenFontSizeSelector = useCallback((newFontSizeSelectorShow: boolean) => {
+ if (newFontSizeSelectorShow) {
+ editor.update(() => {
+ const selection = $getSelection()
+
+ if ($isRangeSelection(selection))
+ $setSelection(selection.clone())
+ })
+ }
+ setFontSizeSelectorShow(newFontSizeSelectorShow)
+ }, [editor])
+
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerUpdateListener(() => {
+ editor.getEditorState().read(() => {
+ const selection = $getSelection()
+
+ if ($isRangeSelection(selection)) {
+ const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px')
+ setFontSize(fontSize)
+ }
+ })
+ }),
+ editor.registerCommand(
+ SELECTION_CHANGE_COMMAND,
+ () => {
+ const selection = $getSelection()
+
+ if ($isRangeSelection(selection)) {
+ const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px')
+ setFontSize(fontSize)
+ }
+
+ return false
+ },
+ COMMAND_PRIORITY_CRITICAL,
+ ),
+ )
+ }, [editor])
+
+ return {
+ fontSize,
+ handleFontSize,
+ fontSizeSelectorShow,
+ handleOpenFontSizeSelector,
+ }
+}
diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/index.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/index.tsx
new file mode 100644
index 00000000000000..98ee0cdf0e5c2e
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/toolbar/index.tsx
@@ -0,0 +1,48 @@
+import { memo } from 'react'
+import Divider from './divider'
+import type { ColorPickerProps } from './color-picker'
+import ColorPicker from './color-picker'
+import FontSizeSelector from './font-size-selector'
+import Command from './command'
+import type { OperatorProps } from './operator'
+import Operator from './operator'
+
+type ToolbarProps = ColorPickerProps & OperatorProps
+const Toolbar = ({
+ theme,
+ onThemeChange,
+ onCopy,
+ onDuplicate,
+ onDelete,
+ showAuthor,
+ onShowAuthorChange,
+}: ToolbarProps) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default memo(Toolbar)
diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx
new file mode 100644
index 00000000000000..7cd27a00a19abf
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx
@@ -0,0 +1,107 @@
+import {
+ memo,
+ useState,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import ShortcutsName from '@/app/components/workflow/shortcuts-name'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+ PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
+import Switch from '@/app/components/base/switch'
+
+export type OperatorProps = {
+ onCopy: () => void
+ onDuplicate: () => void
+ onDelete: () => void
+ showAuthor: boolean
+ onShowAuthorChange: (showAuthor: boolean) => void
+}
+const Operator = ({
+ onCopy,
+ onDelete,
+ onDuplicate,
+ showAuthor,
+ onShowAuthorChange,
+}: OperatorProps) => {
+ const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
+
+ return (
+
+ setOpen(!open)}>
+
+
+
+
+
+
+
+
{
+ onCopy()
+ setOpen(false)
+ }}
+ >
+ {t('workflow.common.copy')}
+
+
+
{
+ onDuplicate()
+ setOpen(false)
+ }}
+ >
+ {t('workflow.common.duplicate')}
+
+
+
+
+
+
e.stopPropagation()}
+ >
+
{t('workflow.nodes.note.editor.showAuthor')}
+
+
+
+
+
+
{
+ onDelete()
+ setOpen(false)
+ }}
+ >
+ {t('common.operation.delete')}
+
+
+
+
+
+
+ )
+}
+
+export default memo(Operator)
diff --git a/web/app/components/workflow/note-node/note-editor/utils.ts b/web/app/components/workflow/note-node/note-editor/utils.ts
new file mode 100644
index 00000000000000..b9ce2d33b314da
--- /dev/null
+++ b/web/app/components/workflow/note-node/note-editor/utils.ts
@@ -0,0 +1,21 @@
+import { $isAtNodeEnd } from '@lexical/selection'
+import type { ElementNode, RangeSelection, TextNode } from 'lexical'
+
+export function getSelectedNode(
+ selection: RangeSelection,
+): TextNode | ElementNode {
+ const anchor = selection.anchor
+ const focus = selection.focus
+ const anchorNode = selection.anchor.getNode()
+ const focusNode = selection.focus.getNode()
+ if (anchorNode === focusNode)
+ return anchorNode
+
+ const isBackward = selection.isBackward()
+ if (isBackward)
+ return $isAtNodeEnd(focus) ? anchorNode : focusNode
+ else
+ return $isAtNodeEnd(anchor) ? anchorNode : focusNode
+}
+
+export const urlRegExp = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/
diff --git a/web/app/components/workflow/note-node/types.ts b/web/app/components/workflow/note-node/types.ts
new file mode 100644
index 00000000000000..ad68bd0f10867b
--- /dev/null
+++ b/web/app/components/workflow/note-node/types.ts
@@ -0,0 +1,17 @@
+import type { CommonNodeType } from '../types'
+
+export enum NoteTheme {
+ blue = 'blue',
+ cyan = 'cyan',
+ green = 'green',
+ yellow = 'yellow',
+ pink = 'pink',
+ violet = 'violet',
+}
+
+export type NoteNodeType = CommonNodeType & {
+ text: string
+ theme: NoteTheme
+ author: string
+ showAuthor: boolean
+}
diff --git a/web/app/components/workflow/operator/control.tsx b/web/app/components/workflow/operator/control.tsx
index 8903239e90bf7f..02d9b7cc6b7d99 100644
--- a/web/app/components/workflow/operator/control.tsx
+++ b/web/app/components/workflow/operator/control.tsx
@@ -1,4 +1,8 @@
-import { memo, useCallback } from 'react'
+import type { MouseEvent } from 'react'
+import {
+ memo,
+ useCallback,
+} from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { useKeyPress } from 'ahooks'
@@ -11,6 +15,7 @@ import { isEventTargetInputArea } from '../utils'
import { useStore } from '../store'
import AddBlock from './add-block'
import TipPopup from './tip-popup'
+import { useOperator } from './hooks'
import {
Cursor02C,
Hand02,
@@ -20,12 +25,14 @@ import {
Hand02 as Hand02Solid,
} from '@/app/components/base/icons/src/vender/solid/editor'
import { OrganizeGrid } from '@/app/components/base/icons/src/vender/line/layout'
+import { StickerSquare } from '@/app/components/base/icons/src/vender/line/files'
const Control = () => {
const { t } = useTranslation()
const controlMode = useStore(s => s.controlMode)
const setControlMode = useStore(s => s.setControlMode)
const { handleLayout } = useWorkflow()
+ const { handleAddNote } = useOperator()
const {
nodesReadOnly,
getNodesReadOnly,
@@ -75,9 +82,28 @@ const Control = () => {
handleLayout()
}
+ const addNote = (e: MouseEvent) => {
+ if (getNodesReadOnly())
+ return
+
+ e.stopPropagation()
+ handleAddNote()
+ }
+
return (
+
+
+
+
+
{
+ const workflowStore = useWorkflowStore()
+ const { userProfile } = useAppContext()
+
+ const handleAddNote = useCallback(() => {
+ const newNode = generateNewNode({
+ type: CUSTOM_NOTE_NODE,
+ data: {
+ title: '',
+ desc: '',
+ type: '' as any,
+ text: '',
+ theme: NoteTheme.blue,
+ author: userProfile?.name || '',
+ showAuthor: true,
+ width: 240,
+ height: 88,
+ _isCandidate: true,
+ } as NoteNodeType,
+ position: {
+ x: 0,
+ y: 0,
+ },
+ })
+ workflowStore.setState({
+ candidateNode: newNode,
+ })
+ }, [workflowStore, userProfile])
+
+ return {
+ handleAddNote,
+ }
+}
diff --git a/web/app/components/workflow/panel-contextmenu.tsx b/web/app/components/workflow/panel-contextmenu.tsx
index eeae51c8d1aff4..a5e63fda4ec601 100644
--- a/web/app/components/workflow/panel-contextmenu.tsx
+++ b/web/app/components/workflow/panel-contextmenu.tsx
@@ -13,6 +13,7 @@ import {
useWorkflowStartRun,
} from './hooks'
import AddBlock from './operator/add-block'
+import { useOperator } from './operator/hooks'
import { exportAppConfig } from '@/service/apps'
import { useToastContext } from '@/app/components/base/toast'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -27,6 +28,7 @@ const PanelContextmenu = () => {
const { handleNodesPaste } = useNodesInteractions()
const { handlePaneContextmenuCancel } = usePanelInteractions()
const { handleStartWorkflowRun } = useWorkflowStartRun()
+ const { handleAddNote } = useOperator()
useClickAway(() => {
handlePaneContextmenuCancel()
@@ -78,6 +80,16 @@ const PanelContextmenu = () => {
crossAxis: -4,
}}
/>
+
{
+ e.stopPropagation()
+ handleAddNote()
+ handlePaneContextmenuCancel()
+ }}
+ >
+ {t('workflow.nodes.note.addNote')}
+
{
diff --git a/web/app/components/workflow/utils.ts b/web/app/components/workflow/utils.ts
index 60e9f69ddf4515..4ad9c6591c8631 100644
--- a/web/app/components/workflow/utils.ts
+++ b/web/app/components/workflow/utils.ts
@@ -17,6 +17,7 @@ import type {
} from './types'
import { BlockEnum } from './types'
import {
+ CUSTOM_NODE,
ITERATION_NODE_Z_INDEX,
NODE_WIDTH_X_OFFSET,
START_INITIAL_POSITION,
@@ -105,7 +106,8 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
}, {} as Record
)
return nodes.map((node) => {
- node.type = 'custom'
+ if (!node.type)
+ node.type = CUSTOM_NODE
const connectedEdges = getConnectedEdges([node], edges)
node.data._connectedSourceHandleIds = connectedEdges.filter(edge => edge.source === node.id).map(edge => edge.sourceHandle || 'source')
@@ -189,7 +191,7 @@ export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
- const nodes = cloneDeep(originNodes).filter(node => !node.parentId)
+ const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
const edges = cloneDeep(originEdges).filter(edge => !edge.data?.isInIteration)
dagreGraph.setGraph({
rankdir: 'LR',
@@ -280,10 +282,10 @@ export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSo
return nodesConnectedSourceOrTargetHandleIdsMap
}
-export const generateNewNode = ({ data, position, id, zIndex, ...rest }: Omit & { id?: string }) => {
+export const generateNewNode = ({ data, position, id, zIndex, type, ...rest }: Omit & { id?: string }) => {
return {
id: id || `${Date.now()}`,
- type: 'custom',
+ type: type || CUSTOM_NODE,
data,
position,
targetPosition: Position.Left,
diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx
index a9e7d69aeae023..93cf1a59cb2f51 100644
--- a/web/context/app-context.tsx
+++ b/web/context/app-context.tsx
@@ -19,6 +19,7 @@ export type AppContextValue = {
currentWorkspace: ICurrentWorkspace
isCurrentWorkspaceManager: boolean
isCurrentWorkspaceOwner: boolean
+ isCurrentWorkspaceEditor: boolean
mutateCurrentWorkspace: VoidFunction
pageContainerRef: React.RefObject
langeniusVersionInfo: LangGeniusVersionResponse
@@ -59,6 +60,7 @@ const AppContext = createContext({
currentWorkspace: initialWorkspaceInfo,
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
+ isCurrentWorkspaceEditor: false,
mutateUserProfile: () => { },
mutateCurrentWorkspace: () => { },
pageContainerRef: createRef(),
@@ -86,6 +88,7 @@ export const AppContextProvider: FC = ({ children }) =>
const [currentWorkspace, setCurrentWorkspace] = useState(initialWorkspaceInfo)
const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
+ const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
const updateUserProfileAndVersion = useCallback(async () => {
if (userProfileResponse && !userProfileResponse.bodyUsed) {
const result = await userProfileResponse.json()
@@ -121,6 +124,7 @@ export const AppContextProvider: FC = ({ children }) =>
currentWorkspace,
isCurrentWorkspaceManager,
isCurrentWorkspaceOwner,
+ isCurrentWorkspaceEditor,
mutateCurrentWorkspace,
}}>
diff --git a/web/i18n/de-DE/common.ts b/web/i18n/de-DE/common.ts
index 5a10b255c11fb5..fa6a14a65d9ec1 100644
--- a/web/i18n/de-DE/common.ts
+++ b/web/i18n/de-DE/common.ts
@@ -169,6 +169,8 @@ const translation = {
adminTip: 'Kann Apps erstellen & Team-Einstellungen verwalten',
normal: 'Normal',
normalTip: 'Kann nur Apps verwenden, kann keine Apps erstellen',
+ editor: 'Editor',
+ editorTip: 'Kann Apps erstellen & bearbeiten',
inviteTeamMember: 'Teammitglied hinzufügen',
inviteTeamMemberTip: 'Sie können direkt nach der Anmeldung auf Ihre Teamdaten zugreifen.',
email: 'E-Mail',
@@ -185,6 +187,7 @@ const translation = {
removeFromTeamTip: 'Wird den Teamzugang entfernen',
setAdmin: 'Als Administrator einstellen',
setMember: 'Als normales Mitglied einstellen',
+ setEditor: 'Als Editor einstellen',
disinvite: 'Einladung widerrufen',
deleteMember: 'Mitglied löschen',
you: '(Du)',
diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts
index fe08e122dd953d..299edc480c22dc 100644
--- a/web/i18n/en-US/common.ts
+++ b/web/i18n/en-US/common.ts
@@ -37,6 +37,10 @@ const translation = {
duplicate: 'Duplicate',
rename: 'Rename',
},
+ errorMsg: {
+ fieldRequired: '{{field}} is required',
+ urlError: 'url should start with http:// or https://',
+ },
placeholder: {
input: 'Please enter',
select: 'Please select',
@@ -169,6 +173,10 @@ const translation = {
adminTip: 'Can build apps & manage team settings',
normal: 'Normal',
normalTip: 'Only can use apps, can not build apps',
+ builder: 'Builder',
+ builderTip: 'Can build & edit own apps',
+ editor: 'Editor',
+ editorTip: 'Can build & edit apps',
inviteTeamMember: 'Add team member',
inviteTeamMemberTip: 'They can access your team data directly after signing in.',
email: 'Email',
@@ -185,6 +193,8 @@ const translation = {
removeFromTeamTip: 'Will remove team access',
setAdmin: 'Set as administrator',
setMember: 'Set to ordinary member',
+ setBuilder: 'Set as builder',
+ setEditor: 'Set as editor',
disinvite: 'Cancel the invitation',
deleteMember: 'Delete Member',
you: '(You)',
@@ -354,6 +364,7 @@ const translation = {
dataSource: {
add: 'Add a data source',
connect: 'Connect',
+ configure: 'Configure',
notion: {
title: 'Notion',
description: 'Using Notion as a data source for the Knowledge.',
@@ -373,6 +384,14 @@ const translation = {
preview: 'PREVIEW',
},
},
+ website: {
+ title: 'Website',
+ description: 'Import content from websites using web crawler.',
+ with: 'With',
+ configuredCrawlers: 'Configured crawlers',
+ active: 'Active',
+ inactive: 'Inactive',
+ },
},
plugin: {
serpapi: {
diff --git a/web/i18n/en-US/dataset-creation.ts b/web/i18n/en-US/dataset-creation.ts
index c315b4c4a6d936..ff884f1c14c451 100644
--- a/web/i18n/en-US/dataset-creation.ts
+++ b/web/i18n/en-US/dataset-creation.ts
@@ -11,6 +11,12 @@ const translation = {
error: {
unavailable: 'This Knowledge is not available',
},
+ firecrawl: {
+ configFirecrawl: 'Configure 🔥Firecrawl',
+ apiKeyPlaceholder: 'API key from firecrawl.dev, starting with "fc-"',
+ apiKeyFormatError: 'API key should start with "fc-"',
+ getApiKeyLinkText: 'Get your API key from firecrawl.dev',
+ },
stepOne: {
filePreview: 'File Preview',
pagePreview: 'Page Preview',
@@ -50,6 +56,30 @@ const translation = {
confirmButton: 'Create',
failed: 'Creation failed',
},
+ website: {
+ fireCrawlNotConfigured: 'Firecrawl is not configured',
+ fireCrawlNotConfiguredDescription: 'Configure Firecrawl with API key to use it.',
+ configure: 'Configure',
+ run: 'Run',
+ firecrawlTitle: 'Extract web content with 🔥Firecrawl',
+ firecrawlDoc: 'Firecrawl docs',
+ firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync_from_website',
+ options: 'Options',
+ crawlSubPage: 'Crawl sub-pages',
+ limit: 'Limit',
+ maxDepth: 'Max depth',
+ excludePaths: 'Exclude paths',
+ includeOnlyPaths: 'Include only paths',
+ extractOnlyMainContent: 'Extract only main content (no headers, navs, footers, etc.)',
+ exceptionErrorTitle: 'An exception occurred while running Firecrawl job:',
+ unknownError: 'Unknown error',
+ totalPageScraped: 'Total pages scraped:',
+ selectAll: 'Select All',
+ resetAll: 'Reset All',
+ scrapTimeInfo: 'Scraped {{total}} pages in total within {{time}}s',
+ preview: 'Preview',
+ maxDepthTooltip: 'Maximum depth to crawl. Depth 1 is the base URL, depth 2 includes the base URL and its direct children, and so on.',
+ },
},
stepTwo: {
segmentation: 'Chunk settings',
@@ -86,9 +116,11 @@ const translation = {
calculating: 'Calculating...',
fileSource: 'Preprocess documents',
notionSource: 'Preprocess pages',
+ websiteSource: 'Preprocess website',
other: 'and other ',
fileUnit: ' files',
notionUnit: ' pages',
+ webpageUnit: ' pages',
previousStep: 'Previous step',
nextStep: 'Save & Process',
save: 'Save & Process',
diff --git a/web/i18n/en-US/dataset-documents.ts b/web/i18n/en-US/dataset-documents.ts
index b431965323cd27..8988b9dc184c65 100644
--- a/web/i18n/en-US/dataset-documents.ts
+++ b/web/i18n/en-US/dataset-documents.ts
@@ -2,8 +2,9 @@ const translation = {
list: {
title: 'Documents',
desc: 'All files of the Knowledge are shown here, and the entire Knowledge can be linked to Dify citations or indexed via the Chat plugin.',
- addFile: 'add file',
+ addFile: 'Add file',
addPages: 'Add Pages',
+ addUrl: 'Add URL',
table: {
header: {
fileName: 'FILE NAME',
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts
index 9ac975f8a22c2d..5d0edcf6ce95b3 100644
--- a/web/i18n/en-US/workflow.ts
+++ b/web/i18n/en-US/workflow.ts
@@ -412,6 +412,25 @@ const translation = {
iteration_other: '{{count}} Iterations',
currentIteration: 'Current Iteration',
},
+ note: {
+ addNote: 'Add Note',
+ editor: {
+ placeholder: 'Write your note...',
+ small: 'Small',
+ medium: 'Medium',
+ large: 'Large',
+ bold: 'Bold',
+ italic: 'Italic',
+ strikethrough: 'Strikethrough',
+ link: 'Link',
+ openLink: 'Open',
+ unlink: 'Unlink',
+ enterUrl: 'Enter URL...',
+ invalidUrl: 'Invalid URL',
+ bulletList: 'Bullet List',
+ showAuthor: 'Show Author',
+ },
+ },
},
tracing: {
stopBy: 'Stop by {{user}}',
diff --git a/web/i18n/fr-FR/common.ts b/web/i18n/fr-FR/common.ts
index 65d1de5b8eb182..88b317a0485e05 100644
--- a/web/i18n/fr-FR/common.ts
+++ b/web/i18n/fr-FR/common.ts
@@ -169,6 +169,8 @@ const translation = {
adminTip: 'Peut construire des applications & gérer les paramètres de l\'équipe',
normal: 'Normal',
normalTip: 'Peut seulement utiliser des applications, ne peut pas construire des applications',
+ editor: 'Éditeur',
+ editorTip: 'Peut construire des applications, mais ne peut pas gérer les paramètres de l\'équipe',
inviteTeamMember: 'Ajouter un membre de l\'équipe',
inviteTeamMemberTip: 'Ils peuvent accéder directement à vos données d\'équipe après s\'être connectés.',
email: 'Courrier électronique',
@@ -185,6 +187,7 @@ const translation = {
removeFromTeamTip: 'Supprimera l\'accès de l\'équipe',
setAdmin: 'Définir comme administrateur',
setMember: 'Définir en tant que membre ordinaire',
+ setEditor: 'Définir en tant qu\'éditeur',
disinvite: 'Annuler l\'invitation',
deleteMember: 'Supprimer Membre',
you: '(Vous)',
diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts
index 795fc26624e891..d5e2401687e28b 100644
--- a/web/i18n/ja-JP/app.ts
+++ b/web/i18n/ja-JP/app.ts
@@ -5,7 +5,7 @@ const translation = {
chatbot: 'チャットボット',
agent: 'エージェント',
workflow: 'ワークフロー',
- completion: '完了',
+ completion: 'テキスト生成',
},
duplicate: '複製',
duplicateTitle: 'アプリを複製する',
@@ -84,7 +84,7 @@ const translation = {
chatbot: 'チャットボット',
agent: 'エージェント',
workflow: 'ワークフロー',
- completion: '完了',
+ completion: 'テキスト生成',
},
}
diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts
index f01978753693f7..dca218852aba88 100644
--- a/web/i18n/ja-JP/common.ts
+++ b/web/i18n/ja-JP/common.ts
@@ -169,6 +169,8 @@ const translation = {
adminTip: 'アプリの構築およびチーム設定の管理ができます',
normal: '通常',
normalTip: 'アプリの使用のみが可能で、アプリの構築はできません',
+ editor: 'エディター',
+ editorTip: 'アプリの構築ができますが、チーム設定の管理はできません',
inviteTeamMember: 'チームメンバーを招待する',
inviteTeamMemberTip: '彼らはサインイン後、直接あなたのチームデータにアクセスできます。',
email: 'メール',
@@ -185,6 +187,7 @@ const translation = {
removeFromTeamTip: 'チームへのアクセスが削除されます',
setAdmin: '管理者に設定',
setMember: '通常のメンバーに設定',
+ setEditor: 'エディターに設定',
disinvite: '招待をキャンセル',
deleteMember: 'メンバーを削除',
you: '(あなた)',
diff --git a/web/i18n/ko-KR/common.ts b/web/i18n/ko-KR/common.ts
index bb177ff17379bc..329c86dd497236 100644
--- a/web/i18n/ko-KR/common.ts
+++ b/web/i18n/ko-KR/common.ts
@@ -165,6 +165,8 @@ const translation = {
adminTip: '앱 빌드 및 팀 설정 관리 가능',
normal: '일반',
normalTip: '앱 사용만 가능하고 앱 빌드는 불가능',
+ editor: '편집자',
+ editorTip: '앱 빌드만 가능하고 팀 설정 관리 불가능',
inviteTeamMember: '팀 멤버 초대',
inviteTeamMemberTip: '로그인 후에 바로 팀 데이터에 액세스할 수 있습니다.',
email: '이메일',
@@ -181,6 +183,7 @@ const translation = {
removeFromTeamTip: '팀 액세스가 제거됩니다',
setAdmin: '관리자 설정',
setMember: '일반 멤버 설정',
+ setEditor: '편집자 설정',
disinvite: '초대 취소',
deleteMember: '멤버 삭제',
you: '(나)',
diff --git a/web/i18n/pl-PL/common.ts b/web/i18n/pl-PL/common.ts
index e40af2a6abe014..f055469f973e6c 100644
--- a/web/i18n/pl-PL/common.ts
+++ b/web/i18n/pl-PL/common.ts
@@ -175,6 +175,8 @@ const translation = {
adminTip: 'Może tworzyć aplikacje i zarządzać ustawieniami zespołu',
normal: 'Normalny',
normalTip: 'Może tylko korzystać z aplikacji, nie może tworzyć aplikacji',
+ editor: 'Edytor',
+ editorTip: 'Może tworzyć i edytować aplikacje, ale nie zarządzać ustawieniami zespołu',
inviteTeamMember: 'Dodaj członka zespołu',
inviteTeamMemberTip:
'Mogą uzyskać bezpośredni dostęp do danych Twojego zespołu po zalogowaniu.',
@@ -193,6 +195,7 @@ const translation = {
removeFromTeamTip: 'Usunie dostęp do zespołu',
setAdmin: 'Ustaw jako administratora',
setMember: 'Ustaw jako zwykłego członka',
+ setEditor: 'Ustaw jako edytora',
disinvite: 'Anuluj zaproszenie',
deleteMember: 'Usuń członka',
you: '(Ty)',
diff --git a/web/i18n/pt-BR/common.ts b/web/i18n/pt-BR/common.ts
index 94407c1a35ed20..f551fe3c5ebf32 100644
--- a/web/i18n/pt-BR/common.ts
+++ b/web/i18n/pt-BR/common.ts
@@ -169,6 +169,8 @@ const translation = {
adminTip: 'Pode criar aplicativos e gerenciar configurações da equipe',
normal: 'Normal',
normalTip: 'Só pode usar aplicativos, não pode criar aplicativos',
+ editor: 'Editor',
+ editorTip: 'Pode editar aplicativos, mas não pode gerenciar configurações da equipe',
inviteTeamMember: 'Adicionar membro da equipe',
inviteTeamMemberTip: 'Eles podem acessar os dados da sua equipe diretamente após fazer login.',
email: 'E-mail',
@@ -185,6 +187,7 @@ const translation = {
removeFromTeamTip: 'Removerá o acesso da equipe',
setAdmin: 'Definir como administrador',
setMember: 'Definir como membro comum',
+ setEditor: 'Definir como editor',
disinvite: 'Cancelar o convite',
deleteMember: 'Excluir Membro',
you: '(Você)',
diff --git a/web/i18n/ro-RO/common.ts b/web/i18n/ro-RO/common.ts
index 40d81854493faf..63c59161d1e60c 100644
--- a/web/i18n/ro-RO/common.ts
+++ b/web/i18n/ro-RO/common.ts
@@ -168,6 +168,8 @@ const translation = {
adminTip: 'Poate construi aplicații și gestiona setările echipei',
normal: 'Normal',
normalTip: 'Poate doar utiliza aplicații, nu poate construi aplicații',
+ editor: 'Editor',
+ editorTip: 'Poate construi aplicații, dar nu poate gestiona setările echipei',
inviteTeamMember: 'Adaugă membru în echipă',
inviteTeamMemberTip: 'Pot accesa direct datele echipei dvs. după autentificare.',
email: 'Email',
@@ -184,6 +186,7 @@ const translation = {
removeFromTeamTip: 'Va elimina accesul la echipă',
setAdmin: 'Setează ca administrator',
setMember: 'Setează ca membru obișnuit',
+ setEditor: 'Setează ca editor',
disinvite: 'Anulează invitația',
deleteMember: 'Șterge membru',
you: '(Dvs.)',
diff --git a/web/i18n/uk-UA/common.ts b/web/i18n/uk-UA/common.ts
index 409c46f39537f2..4ba46b33104658 100644
--- a/web/i18n/uk-UA/common.ts
+++ b/web/i18n/uk-UA/common.ts
@@ -169,6 +169,8 @@ const translation = {
adminTip: 'Може створювати програми та керувати налаштуваннями команди',
normal: 'Звичайний',
normalTip: 'Може лише використовувати програми, не може створювати програми',
+ editor: 'Редактор',
+ editorTip: 'Може створювати програми, але не може керувати налаштуваннями команди',
inviteTeamMember: 'Додати учасника команди',
inviteTeamMemberTip: 'Вони зможуть отримати доступ до даних вашої команди безпосередньо після входу.',
email: 'Електронна пошта',
@@ -185,6 +187,7 @@ const translation = {
removeFromTeamTip: 'Буде видалено доступ до команди',
setAdmin: 'Призначити адміністратором',
setMember: 'Встановити як звичайного члена',
+ setEditor: 'Встановити як Редактор',
disinvite: 'Скасувати запрошення',
deleteMember: 'Видалити учасника',
you: '(Ви)',
diff --git a/web/i18n/vi-VN/common.ts b/web/i18n/vi-VN/common.ts
index 58c279fece764f..973f9ce6d3121e 100644
--- a/web/i18n/vi-VN/common.ts
+++ b/web/i18n/vi-VN/common.ts
@@ -168,6 +168,8 @@ const translation = {
adminTip: 'Có thể xây dựng ứng dụng và quản lý cài đặt nhóm',
normal: 'Bình thường',
normalTip: 'Chỉ có thể sử dụng ứng dụng, không thể xây dựng ứng dụng',
+ editor: 'Biên tập viên',
+ editorTip: 'Chỉ có thể xây dựng ứng dụng, không thể quản lý cài đặt nhóm',
inviteTeamMember: 'Mời thành viên nhóm',
inviteTeamMemberTip: 'Sau khi đăng nhập, họ có thể truy cập trực tiếp vào dữ liệu nhóm của bạn.',
email: 'Email',
@@ -184,6 +186,7 @@ const translation = {
removeFromTeamTip: 'Sẽ xóa quyền truy cập nhóm',
setAdmin: 'Đặt làm quản trị viên',
setMember: 'Đặt thành viên bình thường',
+ setEditor: 'Đặt làm biên tập viên',
disinvite: 'Hủy lời mời',
deleteMember: 'Xóa thành viên',
you: '(Bạn)',
diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts
index 4dda563bfa9714..03eb111db56def 100644
--- a/web/i18n/zh-Hans/common.ts
+++ b/web/i18n/zh-Hans/common.ts
@@ -37,6 +37,10 @@ const translation = {
duplicate: '复制',
rename: '重命名',
},
+ errorMsg: {
+ fieldRequired: '{{field}} 为必填项',
+ urlError: 'url 应该以 http:// 或 https:// 开头',
+ },
placeholder: {
input: '请输入',
select: '请选择',
@@ -169,6 +173,8 @@ const translation = {
adminTip: '能够建立应用程序和管理团队设置',
normal: '成员',
normalTip: '只能使用应用程序,不能建立应用程序',
+ editor: '编辑',
+ editorTip: '能够建立并编辑应用程序,不能管理团队设置',
inviteTeamMember: '添加团队成员',
inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。',
email: '邮箱',
@@ -185,6 +191,7 @@ const translation = {
removeFromTeamTip: '将取消团队访问',
setAdmin: '设为管理员',
setMember: '设为普通成员',
+ setEditor: '设为编辑',
disinvite: '取消邀请',
deleteMember: '删除成员',
you: '(你)',
@@ -353,6 +360,7 @@ const translation = {
dataSource: {
add: '添加数据源',
connect: '绑定',
+ configure: '配置',
notion: {
title: 'Notion',
description: '使用 Notion 作为知识库的数据源。',
@@ -372,6 +380,14 @@ const translation = {
preview: '预览',
},
},
+ website: {
+ title: '网站',
+ description: '使用网络爬虫从网站导入内容。',
+ with: '使用',
+ configuredCrawlers: '已配置的爬虫',
+ active: '可用',
+ inactive: '不可用',
+ },
},
plugin: {
serpapi: {
diff --git a/web/i18n/zh-Hans/dataset-creation.ts b/web/i18n/zh-Hans/dataset-creation.ts
index d36850dc3dc833..27a070535f29a3 100644
--- a/web/i18n/zh-Hans/dataset-creation.ts
+++ b/web/i18n/zh-Hans/dataset-creation.ts
@@ -11,6 +11,12 @@ const translation = {
error: {
unavailable: '该知识库不可用',
},
+ firecrawl: {
+ configFirecrawl: '配置 🔥Firecrawl',
+ apiKeyPlaceholder: '从 firecrawl.dev 获取 API Key,以 "fc-" 开头',
+ apiKeyFormatError: 'API Key 应以 "fc-" 开头',
+ getApiKeyLinkText: '从 firecrawl.dev 获取您的 API Key',
+ },
stepOne: {
filePreview: '文件预览',
pagePreview: '页面预览',
@@ -50,6 +56,30 @@ const translation = {
confirmButton: '创建',
failed: '创建失败',
},
+ website: {
+ fireCrawlNotConfigured: 'Firecrawl 未配置',
+ fireCrawlNotConfiguredDescription: '请配置 Firecrawl 的 API 密钥以使用它。',
+ configure: '配置',
+ run: '运行',
+ firecrawlTitle: '使用 🔥Firecrawl 提取网页内容',
+ firecrawlDoc: 'Firecrawl 文档',
+ firecrawlDocLink: 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/sync_from_website',
+ options: '选项',
+ crawlSubPage: '爬取子页面',
+ limit: '限制数量',
+ maxDepth: '最大深度',
+ excludePaths: '排除路径',
+ includeOnlyPaths: '仅包含路径',
+ extractOnlyMainContent: '仅提取主要内容(无标题、导航、页脚等)',
+ exceptionErrorTitle: '运行 Firecrawl 时发生异常:',
+ unknownError: '未知错误',
+ totalPageScraped: '抓取页面总数:',
+ selectAll: '全选',
+ resetAll: '重置全部',
+ scrapTimeInfo: '总共在 {{time}}秒 内抓取了 {{total}} 个页面',
+ preview: '预览',
+ maxDepthTooltip: '最大抓取深度。深度 1 表示 Base URL,深度 2 表示 Base URL及其直接子页面,依此类推。',
+ },
},
stepTwo: {
segmentation: '分段设置',
@@ -86,9 +116,11 @@ const translation = {
calculating: '计算中...',
fileSource: '预处理文档',
notionSource: '预处理页面',
+ websiteSource: '预处理页面',
other: '和其他 ',
fileUnit: ' 个文件',
notionUnit: ' 个页面',
+ webpageUnit: ' 个页面',
previousStep: '上一步',
nextStep: '保存并处理',
save: '保存并处理',
diff --git a/web/i18n/zh-Hans/dataset-documents.ts b/web/i18n/zh-Hans/dataset-documents.ts
index 9ea5e7aa0f58d6..2f68f04d1df485 100644
--- a/web/i18n/zh-Hans/dataset-documents.ts
+++ b/web/i18n/zh-Hans/dataset-documents.ts
@@ -4,6 +4,7 @@ const translation = {
desc: '知识库的所有文件都在这里显示,整个知识库都可以链接到 Dify 引用或通过 Chat 插件进行索引。',
addFile: '添加文件',
addPages: '添加页面',
+ addUrl: '添加 URL',
table: {
header: {
fileName: '文件名',
diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts
index 83b933bc89fcad..1fbaf38cc5e6fe 100644
--- a/web/i18n/zh-Hans/workflow.ts
+++ b/web/i18n/zh-Hans/workflow.ts
@@ -412,6 +412,25 @@ const translation = {
iteration_other: '{{count}}个迭代',
currentIteration: '当前迭代',
},
+ note: {
+ addNote: '添加注释',
+ editor: {
+ placeholder: '输入注释...',
+ small: '小',
+ medium: '中',
+ large: '大',
+ bold: '加粗',
+ italic: '斜体',
+ strikethrough: '删除线',
+ link: '链接',
+ openLink: '打开',
+ unlink: '取消链接',
+ enterUrl: '输入链接...',
+ invalidUrl: '无效的链接',
+ bulletList: '列表',
+ showAuthor: '显示作者',
+ },
+ },
},
tracing: {
stopBy: '由{{user}}终止',
diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts
index 9fd79df1a10ae3..043b88a79ed36d 100644
--- a/web/i18n/zh-Hant/common.ts
+++ b/web/i18n/zh-Hant/common.ts
@@ -169,6 +169,8 @@ const translation = {
adminTip: '能夠建立應用程式和管理團隊設定',
normal: '成員',
normalTip: '只能使用應用程式,不能建立應用程式',
+ editor: '編輯',
+ editorTip: '能夠建立並編輯應用程式,不能管理團隊設定',
inviteTeamMember: '新增團隊成員',
inviteTeamMemberTip: '對方在登入後可以訪問你的團隊資料。',
email: '郵箱',
@@ -185,6 +187,7 @@ const translation = {
removeFromTeamTip: '將取消團隊訪問',
setAdmin: '設為管理員',
setMember: '設為普通成員',
+ setEditor: '設為編輯',
disinvite: '取消邀請',
deleteMember: '刪除成員',
you: '(你)',
diff --git a/web/models/common.ts b/web/models/common.ts
index d5ebdc3faa3e10..730cfee05df76d 100644
--- a/web/models/common.ts
+++ b/web/models/common.ts
@@ -27,6 +27,7 @@ export type UserProfileResponse = {
interface_theme?: string
timezone?: string
last_login_at?: string
+ last_active_at?: string
last_login_ip?: string
created_at?: string
}
@@ -61,10 +62,10 @@ export type TenantInfoResponse = {
trial_end_reason: null | 'trial_exceeded' | 'using_custom'
}
-export type Member = Pick & {
+export type Member = Pick & {
avatar: string
status: 'pending' | 'active' | 'banned' | 'closed'
- role: 'owner' | 'admin' | 'normal'
+ role: 'owner' | 'admin' | 'editor' | 'normal'
}
export enum ProviderName {
@@ -125,7 +126,7 @@ export type IWorkspace = {
}
export type ICurrentWorkspace = Omit & {
- role: 'normal' | 'admin' | 'owner'
+ role: 'owner' | 'admin' | 'editor' | 'normal'
providers: Provider[]
in_trail: boolean
trial_end_reason?: string
@@ -171,6 +172,39 @@ export type DataSourceNotion = {
source_info: DataSourceNotionWorkspace
}
+export enum DataSourceCategory {
+ website = 'website',
+}
+export enum WebsiteProvider {
+ fireCrawl = 'firecrawl',
+}
+
+export type WebsiteCredentials = {
+ auth_type: 'bearer'
+ config: {
+ base_url: string
+ api_key: string
+ }
+}
+
+export type FirecrawlConfig = {
+ api_key: string
+ base_url: string
+}
+
+export type DataSourceWebsiteItem = {
+ id: string
+ category: DataSourceCategory.website
+ provider: WebsiteProvider
+ credentials: WebsiteCredentials
+ disabled: boolean
+ created_at: number
+ updated_at: number
+}
+export type DataSourceWebsite = {
+ settings: DataSourceWebsiteItem[]
+}
+
export type GithubRepo = {
stargazers_count: number
}
diff --git a/web/models/datasets.ts b/web/models/datasets.ts
index 1f02a43184d805..a28798ba6870ef 100644
--- a/web/models/datasets.ts
+++ b/web/models/datasets.ts
@@ -5,7 +5,7 @@ import type { Tag } from '@/app/components/base/tag-management/constant'
export enum DataSourceType {
FILE = 'upload_file',
NOTION = 'notion_import',
- WEB = 'web_import',
+ WEB = 'website_crawl',
}
export type DataSet = {
@@ -39,6 +39,22 @@ export type CustomFile = File & {
created_at?: number
}
+export type CrawlOptions = {
+ crawl_sub_pages: boolean
+ only_main_content: boolean
+ includes: string
+ excludes: string
+ limit: number | string
+ max_depth: number | string
+}
+
+export type CrawlResultItem = {
+ title: string
+ markdown: string
+ description: string
+ source_url: string
+}
+
export type FileItem = {
fileID: string
file: CustomFile
@@ -149,6 +165,8 @@ export type DataSourceInfo = {
extension: string
}
notion_page_icon?: string
+ job_id: string
+ url: string
}
export type InitialDocumentDetail = {
@@ -219,6 +237,11 @@ export type DataSource = {
file_info_list?: {
file_ids: string[]
}
+ website_info_list?: {
+ provider: string
+ job_id: string
+ urls: string[]
+ }
}
}
diff --git a/web/package.json b/web/package.json
index 81f9f83a3d4130..7ff952ae05e93c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -23,7 +23,7 @@
"@headlessui/react": "^1.7.13",
"@heroicons/react": "^2.0.16",
"@hookform/resolvers": "^3.3.4",
- "@lexical/react": "^0.12.2",
+ "@lexical/react": "^0.16.0",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@monaco-editor/react": "^4.6.0",
@@ -47,7 +47,7 @@
"js-cookie": "^3.0.1",
"katex": "^0.16.10",
"lamejs": "^1.2.1",
- "lexical": "^0.12.2",
+ "lexical": "^0.16.0",
"lodash-es": "^4.17.21",
"mermaid": "10.4.0",
"negotiator": "^0.6.3",
diff --git a/web/service/datasets.ts b/web/service/datasets.ts
index 302b16f6f55567..a382ee8ec85e28 100644
--- a/web/service/datasets.ts
+++ b/web/service/datasets.ts
@@ -152,6 +152,10 @@ export const syncDocument: Fetcher = ({ datasetId,
return get(`/datasets/${datasetId}/documents/${documentId}/notion/sync`)
}
+export const syncWebsite: Fetcher = ({ datasetId, documentId }) => {
+ return get(`/datasets/${datasetId}/documents/${documentId}/website-sync`)
+}
+
export const preImportNotionPages: Fetcher<{ notion_info: DataSourceNotionWorkspace[] }, { url: string; datasetId?: string }> = ({ url, datasetId }) => {
return get<{ notion_info: DataSourceNotionWorkspace[] }>(url, { params: { dataset_id: datasetId } })
}
@@ -227,6 +231,37 @@ export const fetchDatasetApiBaseUrl: Fetcher<{ api_base_url: string }, string> =
return get<{ api_base_url: string }>(url)
}
+export const fetchFirecrawlApiKey = () => {
+ return get('api-key-auth/data-source')
+}
+
+export const createFirecrawlApiKey: Fetcher> = (body) => {
+ return post('api-key-auth/data-source/binding', { body })
+}
+
+export const removeFirecrawlApiKey: Fetcher = (id: string) => {
+ return del(`api-key-auth/data-source/${id}`)
+}
+
+export const createFirecrawlTask: Fetcher> = (body) => {
+ return post('website/crawl', {
+ body: {
+ ...body,
+ provider: 'firecrawl',
+ },
+ })
+}
+
+export const checkFirecrawlTaskStatus: Fetcher = (jobId: string) => {
+ return get(`website/crawl/status/${jobId}`, {
+ params: {
+ provider: 'firecrawl',
+ },
+ }, {
+ silent: true,
+ })
+}
+
type FileTypesRes = {
allowed_extensions: string[]
}
diff --git a/web/utils/app-redirection.ts b/web/utils/app-redirection.ts
index cc122398a27d45..534b019250c4c2 100644
--- a/web/utils/app-redirection.ts
+++ b/web/utils/app-redirection.ts
@@ -1,9 +1,9 @@
export const getRedirection = (
- isCurrentWorkspaceManager: boolean,
+ isCurrentWorkspaceEditor: boolean,
app: any,
redirectionFunc: (href: string) => void,
) => {
- if (!isCurrentWorkspaceManager) {
+ if (!isCurrentWorkspaceEditor) {
redirectionFunc(`/app/${app.id}/overview`)
}
else {
diff --git a/web/yarn.lock b/web/yarn.lock
index e75fa8d0686142..d8aa078e6a3b14 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -414,159 +414,206 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
-"@lexical/clipboard@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.12.2.tgz"
- integrity sha512-RldmfZquuJJJCJ5WquCyoJ1/eZ+AnNgdksqvd+G+Yn/GyJl/+O3dnHM0QVaDSPvh/PynLFcCtz/57ySLo2kQxQ==
+"@lexical/clipboard@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.16.0.tgz#3ae0d87a56bd3518de077e45b0c1bbba2f356193"
+ integrity sha512-eYMJ6jCXpWBVC05Mu9HLMysrBbfi++xFfsm+Yo7A6kYGrqYUhpXqjJkYnw1xdZYL3bV73Oe4ByVJuq42GU+Mqw==
dependencies:
- "@lexical/html" "0.12.2"
- "@lexical/list" "0.12.2"
- "@lexical/selection" "0.12.2"
- "@lexical/utils" "0.12.2"
+ "@lexical/html" "0.16.0"
+ "@lexical/list" "0.16.0"
+ "@lexical/selection" "0.16.0"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/code@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/code/-/code-0.12.2.tgz"
- integrity sha512-w2JeJdnMUtYnC/Fx78sL3iJBt9Ug8pFSDOcI9ay/BkMQFQV8oqq1iyuLLBBJSG4FAM8b2DXrVdGklRQ+jTfTVw==
+"@lexical/code@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.16.0.tgz#225030342e3c361e5541c750033323007a947880"
+ integrity sha512-1EKCBSFV745UI2zn5v75sKcvVdmd+y2JtZhw8CItiQkRnBLv4l4d/RZYy+cKOuXJGsoBrKtxXn5sl7HebwQbPw==
dependencies:
- "@lexical/utils" "0.12.2"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
prismjs "^1.27.0"
-"@lexical/dragon@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.12.2.tgz"
- integrity sha512-Mt8NLzTOt+VgQtc2DKDbHBwKeRlvKqbLqRIMYUVk60gol+YV7NpVBsP1PAMuYYjrTQLhlckBSC32H1SUHZRavA==
+"@lexical/devtools-core@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/devtools-core/-/devtools-core-0.16.0.tgz#326c8e2995ce6e6e9e1fc4654ee2affbecdbd46d"
+ integrity sha512-Jt8p0J0UoMHf3UMh3VdyrXbLLwpEZuMqihTmbPRpwo+YQ6NGQU35QgwY2K0DpPAThpxL/Cm7uaFqGOy8Kjrhqw==
+ dependencies:
+ "@lexical/html" "0.16.0"
+ "@lexical/link" "0.16.0"
+ "@lexical/mark" "0.16.0"
+ "@lexical/table" "0.16.0"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/hashtag@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.12.2.tgz"
- integrity sha512-2vYzIu5Ldf+eYdUrNA2m80c3N3MF3vJ0fIJzpl5QyX8OdViggEWl1bh+lKtw1Ju0H0CUyDIXdDLZ2apW3WDkTA==
+"@lexical/dragon@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.16.0.tgz#de083903701af2bb5264309b565d613c3eec06a0"
+ integrity sha512-Yr29SFZzOPs+S6UrEZaXnnso1fJGVfZOXVJQZbyzlspqJpSHXVH7InOXYHWN6JSWQ8Hs/vU3ksJXwqz+0TCp2g==
dependencies:
- "@lexical/utils" "0.12.2"
+ lexical "0.16.0"
-"@lexical/history@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/history/-/history-0.12.2.tgz"
- integrity sha512-PM/EDjnUyBPMWh1UiYb7T+FLbvTk14HwUWLXvZxn72S6Kj8ExH/PfLbWZWLCFL8RfzvbP407VwfSN8S0bF5H6g==
+"@lexical/hashtag@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.16.0.tgz#ea0187060a114678753adaf0a15aad59d4f49a71"
+ integrity sha512-2EdAvxYVYqb0nv6vgxCRgE8ip7yez5p0y0oeUyxmdbcfZdA+Jl90gYH3VdevmZ5Bk3wE0/fIqiLD+Bb5smqjCQ==
dependencies:
- "@lexical/utils" "0.12.2"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/html@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/html/-/html-0.12.2.tgz"
- integrity sha512-LWUO6OKhDtDZa9X1spHAqzsp+4EF01exis4cz5H9y2sHi7EofogXnRCadZ+fa07NVwPVTZWsStkk5qdSe/NEzg==
+"@lexical/history@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.16.0.tgz#f83f2e331957208c5c8186d98f2f84681d936cec"
+ integrity sha512-xwFxgDZGviyGEqHmgt6A6gPhsyU/yzlKRk9TBUVByba3khuTknlJ1a80H5jb+OYcrpiElml7iVuGYt+oC7atCA==
dependencies:
- "@lexical/selection" "0.12.2"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/link@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/link/-/link-0.12.2.tgz"
- integrity sha512-etOIONa7uyRDmwg8GN52kDlf8thD2Zk1LOFLeocHWz1V8fe3i2unGUek5s/rNPkc6ynpPpNsHdN1VEghOLCCmw==
+"@lexical/html@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.16.0.tgz#98477ed0dee4c7d910608f4e4de3fbd5eeecdffe"
+ integrity sha512-okxn3q/1qkUpCZNEFRI39XeJj4YRjb6prm3WqZgP4d39DI1W24feeTZJjYRCW+dc3NInwFaolU3pNA2MGkjRtg==
dependencies:
- "@lexical/utils" "0.12.2"
+ "@lexical/selection" "0.16.0"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/list@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/list/-/list-0.12.2.tgz"
- integrity sha512-3CyWtYQC+IlK4cK/oiD8Uz1gSXD8UcKGOF2vVsDXkMU06O6zvHNmHZOnVJqA0JVNgZAoR9dMR1fi2xd4iuCAiw==
+"@lexical/link@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.16.0.tgz#f137ab3071206ed3c3a8b8a302ed66b084399ed1"
+ integrity sha512-ppvJSh/XGqlzbeymOiwcXJcUcrqgQqTK2QXTBAZq7JThtb0WsJxYd2CSLSN+Ycu23prnwqOqILcU0+34+gAVFw==
dependencies:
- "@lexical/utils" "0.12.2"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/mark@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/mark/-/mark-0.12.2.tgz"
- integrity sha512-ub+37PDfmThsqAWipRTrwqpgE+83ckqJ5C3mKQUBZvhZfVZW1rEUXZnKjFh2Q3eZK6iT7zVgoVJWJS9ZgEEyag==
+"@lexical/list@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.16.0.tgz#ed97733633492e89c68ad51a1d455b63ce5aa1c0"
+ integrity sha512-nBx/DMM7nCgnOzo1JyNnVaIrk/Xi5wIPNi8jixrEV6w9Om2K6dHutn/79Xzp2dQlNGSLHEDjky6N2RyFgmXh0g==
dependencies:
- "@lexical/utils" "0.12.2"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/markdown@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.12.2.tgz"
- integrity sha512-F2jTFtBp7Q+yoA11BeUOEcxhROzW+HUhUGdsn20pSLhuxsWRj3oUuryWFeNKFofpzTCVoqU6dwpaMNMI2mL/sQ==
+"@lexical/mark@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.16.0.tgz#e87d92845c8bd231ef47106c5d44e7e10d2a3934"
+ integrity sha512-WMR4nqygSgIQ6Vdr5WAzohxBGjH+m44dBNTbWTGZGVlRvPzvBT6tieCoxFqpceIq/ko67HGTCNoFj2cMKVwgIA==
dependencies:
- "@lexical/code" "0.12.2"
- "@lexical/link" "0.12.2"
- "@lexical/list" "0.12.2"
- "@lexical/rich-text" "0.12.2"
- "@lexical/text" "0.12.2"
- "@lexical/utils" "0.12.2"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/offset@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/offset/-/offset-0.12.2.tgz"
- integrity sha512-rZLZXfOBmpmM8A2UZsX3cr/CQYw5F/ou67AbaKI0WImb5sjnIgICZqzu9VFUnkKlVNUurEpplV3UG3D1YYh1OQ==
+"@lexical/markdown@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.16.0.tgz#fd2d2759d9d5554d9899c3e1fb30a868bfa162a2"
+ integrity sha512-7HQLFrBbpY68mcq4A6C1qIGmjgA+fAByditi2WRe7tD2eoIKb/B5baQAnDKis0J+m5kTaCBmdlT6csSzyOPzeQ==
+ dependencies:
+ "@lexical/code" "0.16.0"
+ "@lexical/link" "0.16.0"
+ "@lexical/list" "0.16.0"
+ "@lexical/rich-text" "0.16.0"
+ "@lexical/text" "0.16.0"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
+
+"@lexical/offset@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.16.0.tgz#bb3bc695ed403db0795f095330c68cdc5cbbec4b"
+ integrity sha512-4TqPEC2qA7sgO8Tm65nOWnhJ8dkl22oeuGv9sUB+nhaiRZnw3R45mDelg23r56CWE8itZnvueE7TKvV+F3OXtQ==
+ dependencies:
+ lexical "0.16.0"
-"@lexical/overflow@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.12.2.tgz"
- integrity sha512-UgE5j3ukO6qRFRpH4T7m/DvnodE9nCtImD7QinyGdsTa0hi5xlRnl0FUo605vH+vz7xEsUNAGwQXYPX9Sc/vig==
+"@lexical/overflow@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.16.0.tgz#31b791f7f7005ea4b160f3ae8083a2b3de05cfdc"
+ integrity sha512-a7gtIRxleEuMN9dj2yO4CdezBBfIr9Mq+m7G5z62+xy7VL7cfMfF+xWjy3EmDYDXS4vOQgAXAUgO4oKz2AKGhQ==
+ dependencies:
+ lexical "0.16.0"
-"@lexical/plain-text@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.12.2.tgz"
- integrity sha512-Lcg6+ngRnX70//kz34azYhID3bvW66HSHCfu5UPhCXT+vQ/Jkd/InhRKajBwWXpaJxMM1huoi3sjzVDb3luNtw==
+"@lexical/plain-text@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.16.0.tgz#b903bfb59fb6629ded24194e1bef451df3383393"
+ integrity sha512-BK7/GSOZUHRJTbNPkpb9a/xN9z+FBCdunTsZhnOY8pQ7IKws3kuMO2Tk1zXfTd882ZNAxFdDKNdLYDSeufrKpw==
+ dependencies:
+ "@lexical/clipboard" "0.16.0"
+ "@lexical/selection" "0.16.0"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/react@^0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/react/-/react-0.12.2.tgz"
- integrity sha512-ZBUvf5xmhiYWBw8pPrhYmLAEwFWrbF/cd15y76TUKD9l/2zDwwPs6nJQxBzfz3ei65r2/nnavLDV8W3QfvxfUA==
- dependencies:
- "@lexical/clipboard" "0.12.2"
- "@lexical/code" "0.12.2"
- "@lexical/dragon" "0.12.2"
- "@lexical/hashtag" "0.12.2"
- "@lexical/history" "0.12.2"
- "@lexical/link" "0.12.2"
- "@lexical/list" "0.12.2"
- "@lexical/mark" "0.12.2"
- "@lexical/markdown" "0.12.2"
- "@lexical/overflow" "0.12.2"
- "@lexical/plain-text" "0.12.2"
- "@lexical/rich-text" "0.12.2"
- "@lexical/selection" "0.12.2"
- "@lexical/table" "0.12.2"
- "@lexical/text" "0.12.2"
- "@lexical/utils" "0.12.2"
- "@lexical/yjs" "0.12.2"
+"@lexical/react@^0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.16.0.tgz#0bd3ae63ceb5ad8b77e8c0e8ba7df1a0369462f0"
+ integrity sha512-WKFQbI0/m1YkLjL5t90YLJwjGcl5QRe6mkfm3ljQuL7Ioj3F92ZN/J2gHFVJ9iC8/lJs6Zzw6oFjiP8hQxJf9Q==
+ dependencies:
+ "@lexical/clipboard" "0.16.0"
+ "@lexical/code" "0.16.0"
+ "@lexical/devtools-core" "0.16.0"
+ "@lexical/dragon" "0.16.0"
+ "@lexical/hashtag" "0.16.0"
+ "@lexical/history" "0.16.0"
+ "@lexical/link" "0.16.0"
+ "@lexical/list" "0.16.0"
+ "@lexical/mark" "0.16.0"
+ "@lexical/markdown" "0.16.0"
+ "@lexical/overflow" "0.16.0"
+ "@lexical/plain-text" "0.16.0"
+ "@lexical/rich-text" "0.16.0"
+ "@lexical/selection" "0.16.0"
+ "@lexical/table" "0.16.0"
+ "@lexical/text" "0.16.0"
+ "@lexical/utils" "0.16.0"
+ "@lexical/yjs" "0.16.0"
+ lexical "0.16.0"
react-error-boundary "^3.1.4"
-"@lexical/rich-text@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.12.2.tgz"
- integrity sha512-igsEuv7CwBOAj5c8jeE41cnx6zkhI/Bkbu4W7shT6S6lNA/3cnyZpAMlgixwyK5RoqjGRCT+IJK5l6yBxQfNkw==
+"@lexical/rich-text@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.16.0.tgz#5b9ea6ceb1ea034fa7adf1770bd7fa6af1571d1d"
+ integrity sha512-AGTD6yJZ+kj2TNah1r7/6vyufs6fZANeSvv9x5eG+WjV4uyUJYkd1qR8C5gFZHdkyr+bhAcsAXvS039VzAxRrQ==
+ dependencies:
+ "@lexical/clipboard" "0.16.0"
+ "@lexical/selection" "0.16.0"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/selection@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/selection/-/selection-0.12.2.tgz"
- integrity sha512-h+g3oOnihHKIyLTyG6uLCEVR/DmUEVdCcZO1iAoGsuW7nwWiWNPWj6oZ3Cw5J1Mk5u62DHnkkVDQsVSZbAwmtg==
+"@lexical/selection@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.16.0.tgz#8e09edb1e555e79c646a0105beab58ac21fc7158"
+ integrity sha512-trT9gQVJ2j6AwAe7tHJ30SRuxCpV6yR9LFtggxphHsXSvJYnoHC0CXh1TF2jHl8Gd5OsdWseexGLBE4Y0V3gwQ==
+ dependencies:
+ lexical "0.16.0"
-"@lexical/table@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/table/-/table-0.12.2.tgz"
- integrity sha512-tiAmTq6RKHDVER9v589Ajm9/RL+WTF1WschrH6HHVCtil6cfJfTJeJ+MF45+XEzB9fkqy2LfrScAfWxqLjVePA==
+"@lexical/table@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.16.0.tgz#68592afbb0f9c0d9bf42bebaae626b8129fc470d"
+ integrity sha512-A66K779kxdr0yH2RwT2itsMnkzyFLFNPXyiWGLobCH8ON4QPuBouZvjbRHBe8Pe64yJ0c1bRDxSbTqUi9Wt3Gg==
dependencies:
- "@lexical/utils" "0.12.2"
+ "@lexical/utils" "0.16.0"
+ lexical "0.16.0"
-"@lexical/text@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/text/-/text-0.12.2.tgz"
- integrity sha512-HyuIGuQvVi5djJKKBf+jYEBjK+0Eo9cKHf6WS7dlFozuCZvcCQEJkFy2yceWOwIVk+f2kptVQ5uO7aiZHExH2A==
+"@lexical/text@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.16.0.tgz#fc4789591f8aaa4a33bc1814280bc8725fd036a9"
+ integrity sha512-9ilaOhuNIIGHKC8g8j3K/mEvJ09af9B6RKbm3GNoRcf/WNHD4dEFWNTEvgo/3zCzAS8EUBI6UINmfQQWlMjdIQ==
+ dependencies:
+ lexical "0.16.0"
-"@lexical/utils@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/utils/-/utils-0.12.2.tgz"
- integrity sha512-xW4y4l2Yd37+qLwkBvBGyzsKCA9wnh1ljphBJeR2vreT193i2gaIwuku2ZKlER14VHw4192qNJF7vUoAEmwurQ==
+"@lexical/utils@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.16.0.tgz#6ad5785c53347aed5b39c980240c09b21c4a7469"
+ integrity sha512-GWmFEmd7o3GHqJBaEwzuZQbfTNI3Gg8ReGuHMHABgrkhZ8j2NggoRBlxsQLG0f7BewfTMVwbye22yBPq78775w==
dependencies:
- "@lexical/list" "0.12.2"
- "@lexical/selection" "0.12.2"
- "@lexical/table" "0.12.2"
+ "@lexical/list" "0.16.0"
+ "@lexical/selection" "0.16.0"
+ "@lexical/table" "0.16.0"
+ lexical "0.16.0"
-"@lexical/yjs@0.12.2":
- version "0.12.2"
- resolved "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.12.2.tgz"
- integrity sha512-OPJhkJD1Mp9W80mfLzASTB3OFWFMzJteUYA+eSyDgiX9zNi1VGxAqmIITTkDvnCMa+qvw4EfhGeGezpjx6Og4A==
+"@lexical/yjs@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.16.0.tgz#e27bec25c12e90f7768b980da08f2d2d9919d25b"
+ integrity sha512-YIJr87DfAXTwoVHDjR7cci//hr4r/a61Nn95eo2JNwbTqQo65Gp8rwJivqVxNfvKZmRdwHTKgvdEDoBmI/tGog==
dependencies:
- "@lexical/offset" "0.12.2"
+ "@lexical/offset" "0.16.0"
+ lexical "0.16.0"
"@mdx-js/loader@^2.3.0":
version "2.3.0"
@@ -4287,10 +4334,10 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
-lexical@^0.12.2:
- version "0.12.2"
- resolved "https://registry.npmjs.org/lexical/-/lexical-0.12.2.tgz"
- integrity sha512-Kxavd+ETjxtVwG/hvPd6WZfXD44sLOKe9Vlkwxy7lBQ1qZArS+rZfs+u5iXwXe6tX9f2PIM0u3RHsrCEDDE0fw==
+lexical@0.16.0, lexical@^0.16.0:
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.16.0.tgz#0515d4003cbfba5a5e0e3e50f32f65076a6b89e2"
+ integrity sha512-Skn45Qhriazq4fpAtwnAB11U//GKc4vjzx54xsV3TkDLDvWpbL4Z9TNRwRoN3g7w8AkWnqjeOSODKkrjgfRSrg==
lilconfig@2.1.0, lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0"