dify
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
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'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const Icon: FC<{
|
||||
src: string
|
||||
name: string
|
||||
className: string
|
||||
}> = ({ src, name, className }) => {
|
||||
return (
|
||||
<NotionIcon
|
||||
src={src}
|
||||
name={name}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
}
|
||||
type Props = {
|
||||
workspaces: TDataSourceNotion[]
|
||||
}
|
||||
|
||||
const DataSourceNotion: FC<Props> = ({
|
||||
workspaces,
|
||||
}) => {
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const [canConnectNotion, setCanConnectNotion] = useState(false)
|
||||
const { data } = useSWR(canConnectNotion ? '/oauth/data-source/notion' : null, fetchNotionConnection)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const connected = !!workspaces.length
|
||||
|
||||
const handleConnectNotion = () => {
|
||||
if (!isCurrentWorkspaceManager)
|
||||
return
|
||||
|
||||
setCanConnectNotion(true)
|
||||
}
|
||||
|
||||
const handleAuthAgain = () => {
|
||||
if (data?.data)
|
||||
window.location.href = data.data
|
||||
else
|
||||
setCanConnectNotion(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (data && 'data' in data) {
|
||||
if (data.data && typeof data.data === 'string' && data.data.startsWith('http')) {
|
||||
window.location.href = data.data
|
||||
}
|
||||
else if (data.data === 'internal') {
|
||||
Toast.notify({
|
||||
type: 'info',
|
||||
message: t('common.dataSource.notion.integratedAlert'),
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [data, t])
|
||||
|
||||
return (
|
||||
<Panel
|
||||
type={DataSourceType.notion}
|
||||
isConfigured={connected}
|
||||
onConfigure={handleConnectNotion}
|
||||
readOnly={!isCurrentWorkspaceManager}
|
||||
isSupportList
|
||||
configuredList={workspaces.map(workspace => ({
|
||||
id: workspace.id,
|
||||
logo: ({ className }: { className: string }) => (
|
||||
<Icon
|
||||
src={workspace.source_info.workspace_icon!}
|
||||
name={workspace.source_info.workspace_name}
|
||||
className={className}
|
||||
/>),
|
||||
name: workspace.source_info.workspace_name,
|
||||
isActive: workspace.is_bound,
|
||||
notionConfig: {
|
||||
total: workspace.source_info.total || 0,
|
||||
},
|
||||
}))}
|
||||
onRemove={noop} // handled in operation/index.tsx
|
||||
notionActions={{
|
||||
onChangeAuthorizedPage: handleAuthAgain,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(DataSourceNotion)
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Fragment } from 'react'
|
||||
import { useSWRConfig } from 'swr'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiLoopLeftLine,
|
||||
RiMoreFill,
|
||||
RiStickyNoteAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||
import { syncDataSourceNotion, updateDataSourceNotionAction } from '@/service/common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type OperateProps = {
|
||||
payload: {
|
||||
id: string
|
||||
total: number
|
||||
}
|
||||
onAuthAgain: () => void
|
||||
}
|
||||
export default function Operate({
|
||||
payload,
|
||||
onAuthAgain,
|
||||
}: OperateProps) {
|
||||
const { t } = useTranslation()
|
||||
const { mutate } = useSWRConfig()
|
||||
|
||||
const updateIntegrates = () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
})
|
||||
mutate({ url: 'data-source/integrates' })
|
||||
}
|
||||
const handleSync = async () => {
|
||||
await syncDataSourceNotion({ url: `/oauth/data-source/notion/${payload.id}/sync` })
|
||||
updateIntegrates()
|
||||
}
|
||||
const handleRemove = async () => {
|
||||
await updateDataSourceNotionAction({ url: `/data-source/integrates/${payload.id}/disable` })
|
||||
updateIntegrates()
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
<MenuButton className={cn('flex h-8 w-8 items-center justify-center rounded-lg hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
|
||||
<RiMoreFill className='h-4 w-4 text-text-secondary' />
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems className="absolute right-0 top-9 w-60 max-w-80 origin-top-right rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
|
||||
<div className="px-1 py-1">
|
||||
<MenuItem>
|
||||
<div
|
||||
className='flex cursor-pointer rounded-lg px-3 py-2 hover:bg-state-base-hover'
|
||||
onClick={onAuthAgain}
|
||||
>
|
||||
<RiStickyNoteAddLine className='mr-2 mt-[2px] h-4 w-4 text-text-tertiary' />
|
||||
<div>
|
||||
<div className='system-sm-semibold text-text-secondary'>{t('common.dataSource.notion.changeAuthorizedPages')}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{payload.total} {t('common.dataSource.notion.pagesAuthorized')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div className='flex cursor-pointer rounded-lg px-3 py-2 hover:bg-state-base-hover' onClick={handleSync}>
|
||||
<RiLoopLeftLine className='mr-2 mt-[2px] h-4 w-4 text-text-tertiary' />
|
||||
<div className='system-sm-semibold text-text-secondary'>{t('common.dataSource.notion.sync')}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
<MenuItem>
|
||||
<div className='border-t border-divider-subtle p-1'>
|
||||
<div className='flex cursor-pointer rounded-lg px-3 py-2 hover:bg-state-base-hover' onClick={handleRemove}>
|
||||
<RiDeleteBinLine className='mr-2 mt-[2px] h-4 w-4 text-text-tertiary' />
|
||||
<div className='system-sm-semibold text-text-secondary'>{t('common.dataSource.notion.remove')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
'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/base/field'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { createDataSourceApiKeyBinding } 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<Props> = ({
|
||||
onCancel,
|
||||
onSaved,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [config, setConfig] = useState<FirecrawlConfig>({
|
||||
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',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 createDataSourceApiKeyBinding(postData)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
onSaved()
|
||||
}, [config.api_key, config.base_url, onSaved, t, isSaving])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
|
||||
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl'>
|
||||
<div className='px-8 pt-8'>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<div className='system-xl-semibold text-text-primary'>{t(`${I18N_PREFIX}.configFirecrawl`)}</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={config.api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.apiKeyPlaceholder`)!}
|
||||
/>
|
||||
<Field
|
||||
label='Base URL'
|
||||
labelClassName='!text-sm'
|
||||
value={config.base_url}
|
||||
onChange={handleConfigChange('base_url')}
|
||||
placeholder={DEFAULT_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<div className='my-8 flex h-8 items-center justify-between'>
|
||||
<a className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-text-accent' target='_blank' href='https://www.firecrawl.dev/account'>
|
||||
<span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span>
|
||||
<LinkExternal02 className='h-3 w-3' />
|
||||
</a>
|
||||
<div className='flex'>
|
||||
<Button
|
||||
size='large'
|
||||
className='mr-2'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='large'
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigFirecrawlModal)
|
||||
@@ -0,0 +1,140 @@
|
||||
'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 { DataSourceProvider } from '@/models/common'
|
||||
import Field from '@/app/components/datasets/create/website/base/field'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
type Props = {
|
||||
onCancel: () => void
|
||||
onSaved: () => void
|
||||
}
|
||||
|
||||
const I18N_PREFIX = 'datasetCreation.jinaReader'
|
||||
|
||||
const ConfigJinaReaderModal: FC<Props> = ({
|
||||
onCancel,
|
||||
onSaved,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (isSaving)
|
||||
return
|
||||
let errorMsg = ''
|
||||
if (!errorMsg) {
|
||||
if (!apiKey) {
|
||||
errorMsg = t('common.errorMsg.fieldRequired', {
|
||||
field: 'API Key',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMsg,
|
||||
})
|
||||
return
|
||||
}
|
||||
const postData = {
|
||||
category: 'website',
|
||||
provider: DataSourceProvider.jinaReader,
|
||||
credentials: {
|
||||
auth_type: 'bearer',
|
||||
config: {
|
||||
api_key: apiKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
try {
|
||||
setIsSaving(true)
|
||||
await createDataSourceApiKeyBinding(postData)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
onSaved()
|
||||
}, [apiKey, onSaved, t, isSaving])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
|
||||
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl'>
|
||||
<div className='px-8 pt-8'>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<div className='system-xl-semibold text-text-primary'>{t(`${I18N_PREFIX}.configJinaReader`)}</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={apiKey}
|
||||
onChange={(value: string | number) => setApiKey(value as string)}
|
||||
placeholder={t(`${I18N_PREFIX}.apiKeyPlaceholder`)!}
|
||||
/>
|
||||
</div>
|
||||
<div className='my-8 flex h-8 items-center justify-between'>
|
||||
<a className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-text-accent' target='_blank' href='https://jina.ai/reader/'>
|
||||
<span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span>
|
||||
<LinkExternal02 className='h-3 w-3' />
|
||||
</a>
|
||||
<div className='flex'>
|
||||
<Button
|
||||
size='large'
|
||||
className='mr-2'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='large'
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigJinaReaderModal)
|
||||
@@ -0,0 +1,161 @@
|
||||
'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 { WatercrawlConfig } from '@/models/common'
|
||||
import Field from '@/app/components/datasets/create/website/base/field'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
type Props = {
|
||||
onCancel: () => void
|
||||
onSaved: () => void
|
||||
}
|
||||
|
||||
const I18N_PREFIX = 'datasetCreation.watercrawl'
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://app.watercrawl.dev'
|
||||
|
||||
const ConfigWatercrawlModal: FC<Props> = ({
|
||||
onCancel,
|
||||
onSaved,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [config, setConfig] = useState<WatercrawlConfig>({
|
||||
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',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMsg,
|
||||
})
|
||||
return
|
||||
}
|
||||
const postData = {
|
||||
category: 'website',
|
||||
provider: 'watercrawl',
|
||||
credentials: {
|
||||
auth_type: 'x-api-key',
|
||||
config: {
|
||||
api_key: config.api_key,
|
||||
base_url: config.base_url || DEFAULT_BASE_URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
try {
|
||||
setIsSaving(true)
|
||||
await createDataSourceApiKeyBinding(postData)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.success'),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
onSaved()
|
||||
}, [config.api_key, config.base_url, onSaved, t, isSaving])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
|
||||
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl'>
|
||||
<div className='px-8 pt-8'>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<div className='system-xl-semibold text-text-primary'>{t(`${I18N_PREFIX}.configWatercrawl`)}</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<Field
|
||||
label='API Key'
|
||||
labelClassName='!text-sm'
|
||||
isRequired
|
||||
value={config.api_key}
|
||||
onChange={handleConfigChange('api_key')}
|
||||
placeholder={t(`${I18N_PREFIX}.apiKeyPlaceholder`)!}
|
||||
/>
|
||||
<Field
|
||||
label='Base URL'
|
||||
labelClassName='!text-sm'
|
||||
value={config.base_url}
|
||||
onChange={handleConfigChange('base_url')}
|
||||
placeholder={DEFAULT_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<div className='my-8 flex h-8 items-center justify-between'>
|
||||
<a className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-text-accent' target='_blank' href='https://app.watercrawl.dev/'>
|
||||
<span>{t(`${I18N_PREFIX}.getApiKeyLinkText`)}</span>
|
||||
<LinkExternal02 className='h-3 w-3' />
|
||||
</a>
|
||||
<div className='flex'>
|
||||
<Button
|
||||
size='large'
|
||||
className='mr-2'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='large'
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigWatercrawlModal)
|
||||
@@ -0,0 +1,131 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Panel from '../panel'
|
||||
import { DataSourceType } from '../panel/types'
|
||||
import ConfigFirecrawlModal from './config-firecrawl-modal'
|
||||
import ConfigWatercrawlModal from './config-watercrawl-modal'
|
||||
import ConfigJinaReaderModal from './config-jina-reader-modal'
|
||||
import cn from '@/utils/classnames'
|
||||
import s from '@/app/components/datasets/create/website/index.module.css'
|
||||
import { fetchDataSources, removeDataSourceApiKeyBinding } from '@/service/datasets'
|
||||
|
||||
import type { DataSourceItem } from '@/models/common'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
type Props = {
|
||||
provider: DataSourceProvider
|
||||
}
|
||||
|
||||
const DataSourceWebsite: FC<Props> = ({ provider }) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const [sources, setSources] = useState<DataSourceItem[]>([])
|
||||
const checkSetApiKey = useCallback(async () => {
|
||||
const res = await fetchDataSources() as any
|
||||
const list = res.sources
|
||||
setSources(list)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
checkSetApiKey()
|
||||
}, [])
|
||||
|
||||
const [configTarget, setConfigTarget] = useState<DataSourceProvider | null>(null)
|
||||
const showConfig = useCallback((provider: DataSourceProvider) => {
|
||||
setConfigTarget(provider)
|
||||
}, [setConfigTarget])
|
||||
|
||||
const hideConfig = useCallback(() => {
|
||||
setConfigTarget(null)
|
||||
}, [setConfigTarget])
|
||||
|
||||
const handleAdded = useCallback(() => {
|
||||
checkSetApiKey()
|
||||
hideConfig()
|
||||
}, [checkSetApiKey, hideConfig])
|
||||
|
||||
const getIdByProvider = (provider: DataSourceProvider): string | undefined => {
|
||||
const source = sources.find(item => item.provider === provider)
|
||||
return source?.id
|
||||
}
|
||||
|
||||
const getProviderName = (provider: DataSourceProvider): string => {
|
||||
if (provider === DataSourceProvider.fireCrawl)
|
||||
return 'Firecrawl'
|
||||
|
||||
if (provider === DataSourceProvider.waterCrawl)
|
||||
return 'WaterCrawl'
|
||||
|
||||
return 'Jina Reader'
|
||||
}
|
||||
|
||||
const handleRemove = useCallback((provider: DataSourceProvider) => {
|
||||
return async () => {
|
||||
const dataSourceId = getIdByProvider(provider)
|
||||
if (dataSourceId) {
|
||||
await removeDataSourceApiKeyBinding(dataSourceId)
|
||||
setSources(sources.filter(item => item.provider !== provider))
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.remove'),
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [sources, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
type={DataSourceType.website}
|
||||
provider={provider}
|
||||
isConfigured={sources.find(item => item.provider === provider) !== undefined}
|
||||
onConfigure={() => showConfig(provider)}
|
||||
readOnly={!isCurrentWorkspaceManager}
|
||||
configuredList={sources.filter(item => item.provider === provider).map(item => ({
|
||||
id: item.id,
|
||||
logo: ({ className }: { className: string }) => {
|
||||
if (item.provider === DataSourceProvider.fireCrawl) {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, 'ml-3 flex h-5 w-5 items-center justify-center rounded border border-divider-subtle !bg-background-default text-xs font-medium text-text-tertiary')}>🔥</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.provider === DataSourceProvider.waterCrawl) {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, 'ml-3 flex h-5 w-5 items-center justify-center rounded border border-divider-subtle !bg-background-default text-xs font-medium text-text-tertiary')}>
|
||||
<span className={s.watercrawlLogo}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(className, 'ml-3 flex h-5 w-5 items-center justify-center rounded border border-divider-subtle !bg-background-default text-xs font-medium text-text-tertiary')}>
|
||||
<span className={s.jinaLogo}/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
name: getProviderName(item.provider),
|
||||
isActive: true,
|
||||
}))}
|
||||
onRemove={handleRemove(provider)}
|
||||
/>
|
||||
{configTarget === DataSourceProvider.fireCrawl && (
|
||||
<ConfigFirecrawlModal onSaved={handleAdded} onCancel={hideConfig}/>
|
||||
)}
|
||||
{configTarget === DataSourceProvider.waterCrawl && (
|
||||
<ConfigWatercrawlModal onSaved={handleAdded} onCancel={hideConfig}/>
|
||||
)}
|
||||
{configTarget === DataSourceProvider.jinaReader && (
|
||||
<ConfigJinaReaderModal onSaved={handleAdded} onCancel={hideConfig}/>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(DataSourceWebsite)
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import Indicator from '../../../indicator'
|
||||
import Operate from '../data-source-notion/operate'
|
||||
import { DataSourceType } from './types'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
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
|
||||
}
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const ConfigItem: FC<Props> = ({
|
||||
type,
|
||||
payload,
|
||||
onRemove,
|
||||
notionActions,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isNotion = type === DataSourceType.notion
|
||||
const isWebsite = type === DataSourceType.website
|
||||
const onChangeAuthorizedPage = notionActions?.onChangeAuthorizedPage || noop
|
||||
|
||||
return (
|
||||
<div className={cn(s['workspace-item'], 'mb-1 flex items-center rounded-lg bg-components-panel-on-panel-item-bg py-1 pr-1')} key={payload.id}>
|
||||
<payload.logo className='ml-3 mr-1.5' />
|
||||
<div className='system-sm-medium grow truncate py-[7px] text-text-secondary' title={payload.name}>{payload.name}</div>
|
||||
{
|
||||
payload.isActive
|
||||
? <Indicator className='mr-[6px] shrink-0' color='green' />
|
||||
: <Indicator className='mr-[6px] shrink-0' color='yellow' />
|
||||
}
|
||||
<div className={`system-xs-semibold-uppercase mr-3 shrink-0 ${payload.isActive ? 'text-util-colors-green-green-600' : 'text-util-colors-warning-warning-600'}`}>
|
||||
{
|
||||
payload.isActive
|
||||
? t(isNotion ? 'common.dataSource.notion.connected' : 'common.dataSource.website.active')
|
||||
: t(isNotion ? 'common.dataSource.notion.disconnected' : 'common.dataSource.website.inactive')
|
||||
}
|
||||
</div>
|
||||
<div className='mr-2 h-3 w-[1px] bg-divider-regular' />
|
||||
{isNotion && (
|
||||
<Operate payload={{
|
||||
id: payload.id,
|
||||
total: payload.notionConfig?.total || 0,
|
||||
}} onAuthAgain={onChangeAuthorizedPage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
isWebsite && !readOnly && (
|
||||
<div className='cursor-pointer rounded-md p-2 text-text-tertiary hover:bg-state-base-hover' onClick={onRemove} >
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigItem)
|
||||
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import type { ConfigItemType } from './config-item'
|
||||
import ConfigItem from './config-item'
|
||||
|
||||
import s from './style.module.css'
|
||||
import { DataSourceType } from './types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
type: DataSourceType
|
||||
provider?: DataSourceProvider
|
||||
isConfigured: boolean
|
||||
onConfigure: () => void
|
||||
readOnly: boolean
|
||||
isSupportList?: boolean
|
||||
configuredList: ConfigItemType[]
|
||||
onRemove: () => void
|
||||
notionActions?: {
|
||||
onChangeAuthorizedPage: () => void
|
||||
}
|
||||
}
|
||||
|
||||
const Panel: FC<Props> = ({
|
||||
type,
|
||||
provider,
|
||||
isConfigured,
|
||||
onConfigure,
|
||||
readOnly,
|
||||
configuredList,
|
||||
isSupportList,
|
||||
onRemove,
|
||||
notionActions,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isNotion = type === DataSourceType.notion
|
||||
const isWebsite = type === DataSourceType.website
|
||||
|
||||
const getProviderName = (): string => {
|
||||
if (provider === DataSourceProvider.fireCrawl) return '🔥 Firecrawl'
|
||||
if (provider === DataSourceProvider.waterCrawl) return 'WaterCrawl'
|
||||
return 'Jina Reader'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-2 rounded-xl bg-background-section-burn'>
|
||||
<div className='flex items-center px-3 py-[9px]'>
|
||||
<div className={cn(s[`${type}-icon`], 'mr-3 h-8 w-8 rounded-lg border border-divider-subtle !bg-background-default')} />
|
||||
<div className='grow'>
|
||||
<div className='flex h-5 items-center'>
|
||||
<div className='text-sm font-medium text-text-primary'>{t(`common.dataSource.${type}.title`)}</div>
|
||||
{isWebsite && (
|
||||
<div className='ml-1 rounded-md bg-components-badge-white-to-dark px-1.5 text-xs font-medium leading-[18px] text-text-secondary'>
|
||||
<span className='text-text-tertiary'>{t('common.dataSource.website.with')}</span> {getProviderName()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
!isConfigured && (
|
||||
<div className='system-xs-medium text-text-tertiary'>
|
||||
{t(`common.dataSource.${type}.description`)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{isNotion && (
|
||||
<>
|
||||
{
|
||||
isConfigured
|
||||
? (
|
||||
<Button
|
||||
disabled={readOnly}
|
||||
className='ml-3'
|
||||
onClick={onConfigure}
|
||||
>
|
||||
{t('common.dataSource.configure')}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{isSupportList && <div
|
||||
className={
|
||||
`system-sm-medium flex min-h-7 items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 py-1 text-components-button-secondary-accent-text
|
||||
${!readOnly ? 'cursor-pointer' : 'cursor-default opacity-50 grayscale'}`
|
||||
}
|
||||
onClick={onConfigure}
|
||||
>
|
||||
<RiAddLine className='mr-[5px] h-4 w-4 text-components-button-secondary-accent-text' />
|
||||
{t('common.dataSource.connect')}
|
||||
</div>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isWebsite && !isConfigured && (
|
||||
<div
|
||||
className={
|
||||
`ml-3 flex h-7 items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg
|
||||
px-3 text-xs font-medium text-components-button-secondary-accent-text
|
||||
${!readOnly ? 'cursor-pointer' : 'cursor-default opacity-50 grayscale'}`
|
||||
}
|
||||
onClick={!readOnly ? onConfigure : undefined}
|
||||
>
|
||||
{t('common.dataSource.configure')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
{
|
||||
isConfigured && (
|
||||
<>
|
||||
<div className='flex h-[18px] items-center px-3'>
|
||||
<div className='system-xs-medium text-text-tertiary'>
|
||||
{isNotion ? t('common.dataSource.notion.connectedWorkspace') : t('common.dataSource.website.configuredCrawlers')}
|
||||
</div>
|
||||
<div className='ml-3 grow border-t border-t-divider-subtle' />
|
||||
</div>
|
||||
<div className='px-3 pb-3 pt-2'>
|
||||
{
|
||||
configuredList.map(item => (
|
||||
<ConfigItem
|
||||
key={item.id}
|
||||
type={type}
|
||||
payload={item}
|
||||
onRemove={onRemove}
|
||||
notionActions={notionActions}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Panel)
|
||||
@@ -0,0 +1,17 @@
|
||||
.notion-icon {
|
||||
background: #ffffff url(../../../assets/notion.svg) center center no-repeat;
|
||||
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);
|
||||
}
|
||||
|
||||
.workspace-item:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum DataSourceType {
|
||||
notion = 'notion',
|
||||
website = 'website',
|
||||
}
|
||||
Reference in New Issue
Block a user