chore: Advance the timing of the dataset payment prompt (#29497)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Joel
2025-12-11 15:49:42 +08:00
committed by GitHub
parent f20a2d1586
commit 91e5db3e83
32 changed files with 531 additions and 192 deletions

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6C19 5.44771 18.5523 5 18 5H6C5.44771 5 5 5.44771 5 6V18C5 18.5523 5.44771 19 6 19H18C18.5523 19 19 18.5523 19 18V6ZM9.73926 13.1533C10.0706 12.7115 10.6978 12.6218 11.1396 12.9531C11.5815 13.2845 11.6712 13.9117 11.3398 14.3535L9.46777 16.8486C9.14935 17.2732 8.55487 17.3754 8.11328 17.0811L6.98828 16.3311C6.52878 16.0247 6.40465 15.4039 6.71094 14.9443C7.01729 14.4848 7.63813 14.3606 8.09766 14.667L8.43457 14.8916L9.73926 13.1533ZM16 14C16.5523 14 17 14.4477 17 15C17 15.5523 16.5523 16 16 16H14C13.4477 16 13 15.5523 13 15C13 14.4477 13.4477 14 14 14H16ZM9.73926 7.15234C10.0706 6.71052 10.6978 6.62079 11.1396 6.95215C11.5815 7.28352 11.6712 7.91071 11.3398 8.35254L9.46777 10.8477C9.14936 11.2722 8.55487 11.3744 8.11328 11.0801L6.98828 10.3301C6.52884 10.0238 6.40476 9.40286 6.71094 8.94336C7.0173 8.48384 7.63814 8.35965 8.09766 8.66602L8.43457 8.89062L9.73926 7.15234ZM16.0576 8C16.6099 8 17.0576 8.44772 17.0576 9C17.0576 9.55228 16.6099 10 16.0576 10H14.0576C13.5055 9.99985 13.0576 9.55219 13.0576 9C13.0576 8.44781 13.5055 8.00015 14.0576 8H16.0576ZM21 18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V6C3 4.34315 4.34315 3 6 3H18C19.6569 3 21 4.34315 21 6V18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,26 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M19 6C19 5.44771 18.5523 5 18 5H6C5.44771 5 5 5.44771 5 6V18C5 18.5523 5.44771 19 6 19H18C18.5523 19 19 18.5523 19 18V6ZM9.73926 13.1533C10.0706 12.7115 10.6978 12.6218 11.1396 12.9531C11.5815 13.2845 11.6712 13.9117 11.3398 14.3535L9.46777 16.8486C9.14935 17.2732 8.55487 17.3754 8.11328 17.0811L6.98828 16.3311C6.52878 16.0247 6.40465 15.4039 6.71094 14.9443C7.01729 14.4848 7.63813 14.3606 8.09766 14.667L8.43457 14.8916L9.73926 13.1533ZM16 14C16.5523 14 17 14.4477 17 15C17 15.5523 16.5523 16 16 16H14C13.4477 16 13 15.5523 13 15C13 14.4477 13.4477 14 14 14H16ZM9.73926 7.15234C10.0706 6.71052 10.6978 6.62079 11.1396 6.95215C11.5815 7.28352 11.6712 7.91071 11.3398 8.35254L9.46777 10.8477C9.14936 11.2722 8.55487 11.3744 8.11328 11.0801L6.98828 10.3301C6.52884 10.0238 6.40476 9.40286 6.71094 8.94336C7.0173 8.48384 7.63814 8.35965 8.09766 8.66602L8.43457 8.89062L9.73926 7.15234ZM16.0576 8C16.6099 8 17.0576 8.44772 17.0576 9C17.0576 9.55228 16.6099 10 16.0576 10H14.0576C13.5055 9.99985 13.0576 9.55219 13.0576 9C13.0576 8.44781 13.5055 8.00015 14.0576 8H16.0576ZM21 18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V6C3 4.34315 4.34315 3 6 3H18C19.6569 3 21 4.34315 21 6V18Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "SquareChecklist"
}

View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './SquareChecklist.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'SquareChecklist'
export default Icon

View File

@@ -6,3 +6,4 @@ export { default as Mcp } from './Mcp'
export { default as NoToolPlaceholder } from './NoToolPlaceholder'
export { default as Openai } from './Openai'
export { default as ReplayLine } from './ReplayLine'
export { default as SquareChecklist } from './SquareChecklist'

View File

@@ -21,7 +21,6 @@ type NotionPageSelectorProps = {
datasetId?: string
credentialList: DataSourceCredential[]
onSelectCredential?: (credentialId: string) => void
supportBatchUpload?: boolean
}
const NotionPageSelector = ({
@@ -33,7 +32,6 @@ const NotionPageSelector = ({
datasetId = '',
credentialList,
onSelectCredential,
supportBatchUpload = false,
}: NotionPageSelectorProps) => {
const [searchValue, setSearchValue] = useState('')
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
@@ -177,7 +175,6 @@ const NotionPageSelector = ({
canPreview={canPreview}
previewPageId={previewPageId}
onPreview={handlePreviewPage}
isMultipleChoice={supportBatchUpload}
/>
)}
</div>

View File

@@ -7,7 +7,6 @@ import Checkbox from '../../checkbox'
import NotionIcon from '../../notion-icon'
import cn from '@/utils/classnames'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import Radio from '@/app/components/base/radio/ui'
type PageSelectorProps = {
value: Set<string>
@@ -82,7 +81,6 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
searchValue: string
previewPageId: string
pagesMap: DataSourceNotionPageMap
isMultipleChoice?: boolean
}>) => {
const { t } = useTranslation()
const {
@@ -97,7 +95,6 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
searchValue,
previewPageId,
pagesMap,
isMultipleChoice,
} = data
const current = dataList[index]
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
@@ -138,24 +135,14 @@ const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
previewPageId === current.page_id && 'bg-state-base-hover')}
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
>
{isMultipleChoice ? (
<Checkbox
className='mr-2 shrink-0'
checked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>) : (
<Radio
className='mr-2 shrink-0'
isChecked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>
)}
<Checkbox
className='mr-2 shrink-0'
checked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>
{!searchValue && renderArrow()}
<NotionIcon
className='mr-1 shrink-0'
@@ -204,7 +191,6 @@ const PageSelector = ({
canPreview = true,
previewPageId,
onPreview,
isMultipleChoice = true,
}: PageSelectorProps) => {
const { t } = useTranslation()
const [dataList, setDataList] = useState<NotionPageItem[]>([])
@@ -278,7 +264,7 @@ const PageSelector = ({
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
if (copyValue.has(pageId)) {
if (!searchValue && isMultipleChoice) {
if (!searchValue) {
for (const item of currentWithChildrenAndDescendants.descendants)
copyValue.delete(item)
}
@@ -286,18 +272,12 @@ const PageSelector = ({
copyValue.delete(pageId)
}
else {
if (!searchValue && isMultipleChoice) {
if (!searchValue) {
for (const item of currentWithChildrenAndDescendants.descendants)
copyValue.add(item)
}
// Single choice mode, clear previous selection
if (!isMultipleChoice && copyValue.size > 0) {
copyValue.clear()
copyValue.add(pageId)
}
else {
copyValue.add(pageId)
}
copyValue.add(pageId)
}
onSelect(new Set(copyValue))
@@ -341,7 +321,6 @@ const PageSelector = ({
searchValue,
previewPageId: currentPreviewPageId,
pagesMap,
isMultipleChoice,
}}
>
{Item}

View File

@@ -12,6 +12,7 @@ const PremiumBadgeVariants = cva(
size: {
s: 'premium-badge-s',
m: 'premium-badge-m',
custom: '',
},
color: {
blue: 'premium-badge-blue',
@@ -33,7 +34,7 @@ const PremiumBadgeVariants = cva(
)
type PremiumBadgeProps = {
size?: 's' | 'm'
size?: 's' | 'm' | 'custom'
color?: 'blue' | 'indigo' | 'gray' | 'orange'
allowHover?: boolean
styleCss?: CSSProperties

View File

@@ -0,0 +1,118 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import PlanUpgradeModal from './index'
const mockSetShowPricingModal = jest.fn()
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
jest.mock('@/app/components/base/modal', () => {
const MockModal = ({ isShow, children }: { isShow: boolean; children: React.ReactNode }) => (
isShow ? <div data-testid="plan-upgrade-modal">{children}</div> : null
)
return {
__esModule: true,
default: MockModal,
}
})
jest.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,
}),
}))
const baseProps = {
title: 'Upgrade Required',
description: 'You need to upgrade your plan.',
show: true,
onClose: jest.fn(),
}
const renderComponent = (props: Partial<React.ComponentProps<typeof PlanUpgradeModal>> = {}) => {
const mergedProps = { ...baseProps, ...props }
return render(<PlanUpgradeModal {...mergedProps} />)
}
describe('PlanUpgradeModal', () => {
beforeEach(() => {
jest.clearAllMocks()
})
// Rendering and props-driven content
it('should render modal with provided content when visible', () => {
// Arrange
const extraInfoText = 'Additional upgrade details'
renderComponent({
extraInfo: <div>{extraInfoText}</div>,
})
// Assert
expect(screen.getByText(baseProps.title)).toBeInTheDocument()
expect(screen.getByText(baseProps.description)).toBeInTheDocument()
expect(screen.getByText(extraInfoText)).toBeInTheDocument()
expect(screen.getByText('billing.triggerLimitModal.dismiss')).toBeInTheDocument()
expect(screen.getByText('billing.triggerLimitModal.upgrade')).toBeInTheDocument()
})
// Guard against rendering when modal is hidden
it('should not render content when show is false', () => {
// Act
renderComponent({ show: false })
// Assert
expect(screen.queryByText(baseProps.title)).not.toBeInTheDocument()
expect(screen.queryByText(baseProps.description)).not.toBeInTheDocument()
})
// User closes the modal from dismiss button
it('should call onClose when dismiss button is clicked', async () => {
// Arrange
const user = userEvent.setup()
const onClose = jest.fn()
renderComponent({ onClose })
// Act
await user.click(screen.getByText('billing.triggerLimitModal.dismiss'))
// Assert
expect(onClose).toHaveBeenCalledTimes(1)
})
// Upgrade path uses provided callback over pricing modal
it('should call onUpgrade and onClose when upgrade button is clicked with onUpgrade provided', async () => {
// Arrange
const user = userEvent.setup()
const onClose = jest.fn()
const onUpgrade = jest.fn()
renderComponent({ onClose, onUpgrade })
// Act
await user.click(screen.getByText('billing.triggerLimitModal.upgrade'))
// Assert
expect(onClose).toHaveBeenCalledTimes(1)
expect(onUpgrade).toHaveBeenCalledTimes(1)
expect(mockSetShowPricingModal).not.toHaveBeenCalled()
})
// Fallback upgrade path opens pricing modal when no onUpgrade is supplied
it('should open pricing modal when upgrade button is clicked without onUpgrade', async () => {
// Arrange
const user = userEvent.setup()
const onClose = jest.fn()
renderComponent({ onClose, onUpgrade: undefined })
// Act
await user.click(screen.getByText('billing.triggerLimitModal.upgrade'))
// Assert
expect(onClose).toHaveBeenCalledTimes(1)
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,87 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import styles from './style.module.css'
import { SquareChecklist } from '../../base/icons/src/vender/other'
import { useModalContext } from '@/context/modal-context'
type Props = {
Icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>
title: string
description: string
extraInfo?: React.ReactNode
show: boolean
onClose: () => void
onUpgrade?: () => void
}
const PlanUpgradeModal: FC<Props> = ({
Icon = SquareChecklist,
title,
description,
extraInfo,
show,
onClose,
onUpgrade,
}) => {
const { t } = useTranslation()
const { setShowPricingModal } = useModalContext()
const handleUpgrade = useCallback(() => {
onClose()
onUpgrade ? onUpgrade() : setShowPricingModal()
}, [onClose, onUpgrade, setShowPricingModal])
return (
<Modal
isShow={show}
onClose={onClose}
closable={false}
clickOutsideNotClose
className={`${styles.surface} w-[580px] rounded-2xl !p-0`}
>
<div className='relative'>
<div
aria-hidden
className={`${styles.heroOverlay} pointer-events-none absolute inset-0`}
/>
<div className='px-8 pt-8'>
<div className={`${styles.icon} flex size-12 items-center justify-center rounded-xl shadow-lg backdrop-blur-[5px]`}>
<Icon className='size-6 text-text-primary-on-surface' />
</div>
<div className='mt-6 space-y-2'>
<div className={`${styles.highlight} title-3xl-semi-bold`}>
{title}
</div>
<div className='system-md-regular text-text-tertiary'>
{description}
</div>
</div>
{extraInfo}
</div>
</div>
<div className='mb-8 mt-10 flex justify-end space-x-2 px-8'>
<Button
onClick={onClose}
>
{t('billing.triggerLimitModal.dismiss')}
</Button>
<UpgradeBtn
size='custom'
isShort
onClick={handleUpgrade}
className='!h-8 !rounded-lg px-2'
labelKey='billing.triggerLimitModal.upgrade'
loc='trigger-events-limit-modal'
/>
</div>
</Modal>
)
}
export default React.memo(PlanUpgradeModal)

View File

@@ -19,7 +19,6 @@
background:
linear-gradient(180deg, var(--color-components-avatar-bg-mask-stop-0, rgba(255, 255, 255, 0.12)) 0%, var(--color-components-avatar-bg-mask-stop-100, rgba(255, 255, 255, 0.08)) 100%),
var(--color-util-colors-blue-brand-blue-brand-500, #296dff);
box-shadow: 0 10px 20px color-mix(in srgb, var(--color-util-colors-blue-brand-blue-brand-500, #296dff) 35%, transparent);
}
.highlight {

View File

@@ -2,27 +2,22 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
import UsageInfo from '@/app/components/billing/usage-info'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import type { Plan } from '@/app/components/billing/type'
import styles from './index.module.css'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
type Props = {
show: boolean
onDismiss: () => void
onClose: () => void
onUpgrade: () => void
usage: number
total: number
resetInDays?: number
planType: Plan
}
const TriggerEventsLimitModal: FC<Props> = ({
show,
onDismiss,
onClose,
onUpgrade,
usage,
total,
@@ -31,59 +26,25 @@ const TriggerEventsLimitModal: FC<Props> = ({
const { t } = useTranslation()
return (
<Modal
isShow={show}
onClose={onDismiss}
closable={false}
clickOutsideNotClose
className={`${styles.surface} flex h-[360px] w-[580px] flex-col overflow-hidden rounded-2xl !p-0 shadow-xl`}
>
<div className='relative flex w-full flex-1 items-stretch justify-center'>
<div
aria-hidden
className={`${styles.heroOverlay} pointer-events-none absolute inset-0`}
<PlanUpgradeModal
show={show}
onClose={onClose}
onUpgrade={onUpgrade}
Icon={TriggerAll as React.ComponentType<React.SVGProps<SVGSVGElement>>}
title={t('billing.triggerLimitModal.title')}
description={t('billing.triggerLimitModal.description')}
extraInfo={(
<UsageInfo
className='mt-4 w-full rounded-[12px] bg-components-panel-on-panel-item-bg'
Icon={TriggerAll}
name={t('billing.triggerLimitModal.usageTitle')}
usage={usage}
total={total}
resetInDays={resetInDays}
hideIcon
/>
<div className='relative z-10 flex w-full flex-col items-start gap-4 px-8 pt-8'>
<div className={`${styles.icon} flex h-12 w-12 items-center justify-center rounded-[12px]`}>
<TriggerAll className='h-5 w-5 text-text-primary-on-surface' />
</div>
<div className='flex flex-col items-start gap-2'>
<div className={`${styles.highlight} title-lg-semi-bold`}>
{t('billing.triggerLimitModal.title')}
</div>
<div className='body-md-regular text-text-secondary'>
{t('billing.triggerLimitModal.description')}
</div>
</div>
<UsageInfo
className='mb-5 w-full rounded-[12px] bg-components-panel-on-panel-item-bg'
Icon={TriggerAll}
name={t('billing.triggerLimitModal.usageTitle')}
usage={usage}
total={total}
resetInDays={resetInDays}
hideIcon
/>
</div>
</div>
<div className='flex h-[76px] w-full items-center justify-end gap-2 px-8 pb-8 pt-5'>
<Button
className='h-8 w-[77px] min-w-[72px] !rounded-lg !border-[0.5px] px-3 py-2'
onClick={onDismiss}
>
{t('billing.triggerLimitModal.dismiss')}
</Button>
<UpgradeBtn
isShort
onClick={onUpgrade}
className='flex w-[93px] items-center justify-center !rounded-lg !px-2'
style={{ height: 32 }}
labelKey='billing.triggerLimitModal.upgrade'
loc='trigger-events-limit-modal'
/>
</div>
</Modal>
)}
/>
)
}

View File

@@ -11,7 +11,7 @@ type Props = {
className?: string
style?: CSSProperties
isFull?: boolean
size?: 'md' | 'lg'
size?: 's' | 'm' | 'custom'
isPlain?: boolean
isShort?: boolean
onClick?: () => void
@@ -21,6 +21,7 @@ type Props = {
const UpgradeBtn: FC<Props> = ({
className,
size = 'm',
style,
isPlain = false,
isShort = false,
@@ -62,7 +63,7 @@ const UpgradeBtn: FC<Props> = ({
return (
<PremiumBadge
size='m'
size={size}
color='blue'
allowHover={true}
onClick={onClick}

View File

@@ -22,6 +22,10 @@ import classNames from '@/utils/classnames'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import NotionConnector from '@/app/components/base/notion-connector'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import { useBoolean } from 'ahooks'
import { Plan } from '@/app/components/billing/type'
import UpgradeCard from './upgrade-card'
type IStepOneProps = {
datasetId?: string
@@ -52,7 +56,7 @@ const StepOne = ({
dataSourceTypeDisable,
changeType,
onSetting,
onStepChange,
onStepChange: doOnStepChange,
files,
updateFileList,
updateFile,
@@ -110,7 +114,33 @@ const StepOne = ({
const hasNotin = notionPages.length > 0
const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace
const isShowVectorSpaceFull = (allFileLoaded || hasNotin) && isVectorSpaceFull && enableBilling
const supportBatchUpload = !enableBilling || plan.type !== 'sandbox'
const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox
const notSupportBatchUpload = !supportBatchUpload
const [isShowPlanUpgradeModal, {
setTrue: showPlanUpgradeModal,
setFalse: hidePlanUpgradeModal,
}] = useBoolean(false)
const onStepChange = useCallback(() => {
if (notSupportBatchUpload) {
let isMultiple = false
if (dataSourceType === DataSourceType.FILE && files.length > 1)
isMultiple = true
if (dataSourceType === DataSourceType.NOTION && notionPages.length > 1)
isMultiple = true
if (dataSourceType === DataSourceType.WEB && websitePages.length > 1)
isMultiple = true
if (isMultiple) {
showPlanUpgradeModal()
return
}
}
doOnStepChange()
}, [dataSourceType, doOnStepChange, files.length, notSupportBatchUpload, notionPages.length, showPlanUpgradeModal, websitePages.length])
const nextDisabled = useMemo(() => {
if (!files.length)
return true
@@ -244,6 +274,14 @@ const StepOne = ({
</span>
</Button>
</div>
{
enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
<div className='mt-5'>
<div className='mb-4 h-px bg-divider-subtle'></div>
<UpgradeCard />
</div>
)
}
</>
)}
{dataSourceType === DataSourceType.NOTION && (
@@ -259,7 +297,6 @@ const StepOne = ({
credentialList={notionCredentialList}
onSelectCredential={updateNotionCredentialId}
datasetId={datasetId}
supportBatchUpload={supportBatchUpload}
/>
</div>
{isShowVectorSpaceFull && (
@@ -291,7 +328,6 @@ const StepOne = ({
crawlOptions={crawlOptions}
onCrawlOptionsChange={onCrawlOptionsChange}
authedDataSourceList={authedDataSourceList}
supportBatchUpload={supportBatchUpload}
/>
</div>
{isShowVectorSpaceFull && (
@@ -332,6 +368,14 @@ const StepOne = ({
/>
)}
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
{isShowPlanUpgradeModal && (
<PlanUpgradeModal
show
onClose={hidePlanUpgradeModal}
title={t('billing.upgrade.uploadMultiplePages.title')!}
description={t('billing.upgrade.uploadMultiplePages.description')!}
/>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,33 @@
'use client'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useModalContext } from '@/context/modal-context'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
const UpgradeCard: FC = () => {
const { t } = useTranslation()
const { setShowPricingModal } = useModalContext()
const handleUpgrade = useCallback(() => {
setShowPricingModal()
}, [setShowPricingModal])
return (
<div className='flex items-center justify-between rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg py-3 pl-4 pr-3.5 shadow-xs backdrop-blur-[5px] '>
<div>
<div className='title-md-semi-bold bg-[linear-gradient(92deg,_var(--components-input-border-active-prompt-1,_#0BA5EC)_0%,_var(--components-input-border-active-prompt-2,_#155AEF)_99.21%)] bg-clip-text text-transparent'>{t('billing.upgrade.uploadMultipleFiles.title')}</div>
<div className='system-xs-regular text-text-tertiary'>{t('billing.upgrade.uploadMultipleFiles.description')}</div>
</div>
<UpgradeBtn
size='custom'
isShort
className='ml-3 !h-8 !rounded-lg px-2'
labelKey='billing.triggerLimitModal.upgrade'
loc='upload-multiple-files'
onClick={handleUpgrade}
/>
</div>
)
}
export default React.memo(UpgradeCard)

View File

@@ -6,7 +6,6 @@ import cn from '@/utils/classnames'
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import Checkbox from '@/app/components/base/checkbox'
import Button from '@/app/components/base/button'
import Radio from '@/app/components/base/radio/ui'
type Props = {
payload: CrawlResultItemType
@@ -14,7 +13,6 @@ type Props = {
isPreview: boolean
onCheckChange: (checked: boolean) => void
onPreview: () => void
isMultipleChoice: boolean
}
const CrawledResultItem: FC<Props> = ({
@@ -23,7 +21,6 @@ const CrawledResultItem: FC<Props> = ({
isChecked,
onCheckChange,
onPreview,
isMultipleChoice,
}) => {
const { t } = useTranslation()
@@ -34,21 +31,7 @@ const CrawledResultItem: FC<Props> = ({
<div className={cn(isPreview ? 'bg-state-base-active' : 'group hover:bg-state-base-hover', 'cursor-pointer rounded-lg p-2')}>
<div className='relative flex'>
<div className='flex h-5 items-center'>
{
isMultipleChoice ? (
<Checkbox
className='mr-2 shrink-0'
checked={isChecked}
onCheck={handleCheckChange}
/>
) : (
<Radio
className='mr-2 shrink-0'
isChecked={isChecked}
onCheck={handleCheckChange}
/>
)
}
<Checkbox className='mr-2 shrink-0' checked={isChecked} onCheck={handleCheckChange} />
</div>
<div className='flex min-w-0 grow flex-col'>
<div

View File

@@ -16,7 +16,6 @@ type Props = {
onSelectedChange: (selected: CrawlResultItem[]) => void
onPreview: (payload: CrawlResultItem) => void
usedTime: number
isMultipleChoice: boolean
}
const CrawledResult: FC<Props> = ({
@@ -26,7 +25,6 @@ const CrawledResult: FC<Props> = ({
onSelectedChange,
onPreview,
usedTime,
isMultipleChoice,
}) => {
const { t } = useTranslation()
@@ -42,17 +40,13 @@ const CrawledResult: FC<Props> = ({
const handleItemCheckChange = useCallback((item: CrawlResultItem) => {
return (checked: boolean) => {
if (checked) {
if (isMultipleChoice)
onSelectedChange([...checkedList, item])
else
onSelectedChange([item])
}
else {
if (checked)
onSelectedChange([...checkedList, item])
else
onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url))
}
}
}, [checkedList, isMultipleChoice, onSelectedChange])
}, [checkedList, onSelectedChange])
const [previewIndex, setPreviewIndex] = React.useState<number>(-1)
const handlePreview = useCallback((index: number) => {
@@ -65,13 +59,11 @@ const CrawledResult: FC<Props> = ({
return (
<div className={cn(className, 'border-t-[0.5px] border-divider-regular shadow-xs shadow-shadow-shadow-3')}>
<div className='flex h-[34px] items-center justify-between px-4'>
{isMultipleChoice && (
<CheckboxWithLabel
isChecked={isCheckAll}
onChange={handleCheckedAll} label={isCheckAll ? t(`${I18N_PREFIX}.resetAll`) : t(`${I18N_PREFIX}.selectAll`)}
labelClassName='system-[13px] leading-[16px] font-medium text-text-secondary'
/>
)}
<CheckboxWithLabel
isChecked={isCheckAll}
onChange={handleCheckedAll} label={isCheckAll ? t(`${I18N_PREFIX}.resetAll`) : t(`${I18N_PREFIX}.selectAll`)}
labelClassName='system-[13px] leading-[16px] font-medium text-text-secondary'
/>
<div className='text-xs text-text-tertiary'>
{t(`${I18N_PREFIX}.scrapTimeInfo`, {
total: list.length,
@@ -88,7 +80,6 @@ const CrawledResult: FC<Props> = ({
payload={item}
isChecked={checkedList.some(checkedItem => checkedItem.source_url === item.source_url)}
onCheckChange={handleItemCheckChange(item)}
isMultipleChoice={isMultipleChoice}
/>
))}
</div>

View File

@@ -26,7 +26,6 @@ type Props = {
onJobIdChange: (jobId: string) => void
crawlOptions: CrawlOptions
onCrawlOptionsChange: (payload: CrawlOptions) => void
supportBatchUpload: boolean
}
enum Step {
@@ -42,7 +41,6 @@ const FireCrawl: FC<Props> = ({
onJobIdChange,
crawlOptions,
onCrawlOptionsChange,
supportBatchUpload,
}) => {
const { t } = useTranslation()
const [step, setStep] = useState<Step>(Step.init)
@@ -168,12 +166,8 @@ const FireCrawl: FC<Props> = ({
setCrawlErrorMessage(errorMessage || t(`${I18N_PREFIX}.unknownError`))
}
else {
data.data = data.data.map((item: any) => ({
...item,
content: item.markdown,
}))
setCrawlResult(data)
onCheckedCrawlResultChange(supportBatchUpload ? (data.data || []) : (data.data?.slice(0, 1) || [])) // default select the crawl result
onCheckedCrawlResultChange(data.data || []) // default select the crawl result
setCrawlErrorMessage('')
}
}
@@ -184,7 +178,7 @@ const FireCrawl: FC<Props> = ({
finally {
setStep(Step.finished)
}
}, [checkValid, crawlOptions, onJobIdChange, waitForCrawlFinished, t, onCheckedCrawlResultChange, supportBatchUpload])
}, [checkValid, crawlOptions, onJobIdChange, t, waitForCrawlFinished, onCheckedCrawlResultChange])
return (
<div>
@@ -223,7 +217,6 @@ const FireCrawl: FC<Props> = ({
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
isMultipleChoice={supportBatchUpload}
/>
}
</div>

View File

@@ -24,7 +24,6 @@ type Props = {
crawlOptions: CrawlOptions
onCrawlOptionsChange: (payload: CrawlOptions) => void
authedDataSourceList: DataSourceAuth[]
supportBatchUpload?: boolean
}
const Website: FC<Props> = ({
@@ -36,7 +35,6 @@ const Website: FC<Props> = ({
crawlOptions,
onCrawlOptionsChange,
authedDataSourceList,
supportBatchUpload = false,
}) => {
const { t } = useTranslation()
const { setShowAccountSettingModal } = useModalContext()
@@ -118,7 +116,6 @@ const Website: FC<Props> = ({
onJobIdChange={onJobIdChange}
crawlOptions={crawlOptions}
onCrawlOptionsChange={onCrawlOptionsChange}
supportBatchUpload={supportBatchUpload}
/>
)}
{source && selectedProvider === DataSourceProvider.waterCrawl && (
@@ -129,7 +126,6 @@ const Website: FC<Props> = ({
onJobIdChange={onJobIdChange}
crawlOptions={crawlOptions}
onCrawlOptionsChange={onCrawlOptionsChange}
supportBatchUpload={supportBatchUpload}
/>
)}
{source && selectedProvider === DataSourceProvider.jinaReader && (
@@ -140,7 +136,6 @@ const Website: FC<Props> = ({
onJobIdChange={onJobIdChange}
crawlOptions={crawlOptions}
onCrawlOptionsChange={onCrawlOptionsChange}
supportBatchUpload={supportBatchUpload}
/>
)}
{!source && (

View File

@@ -26,7 +26,6 @@ type Props = {
onJobIdChange: (jobId: string) => void
crawlOptions: CrawlOptions
onCrawlOptionsChange: (payload: CrawlOptions) => void
supportBatchUpload: boolean
}
enum Step {
@@ -42,7 +41,6 @@ const JinaReader: FC<Props> = ({
onJobIdChange,
crawlOptions,
onCrawlOptionsChange,
supportBatchUpload,
}) => {
const { t } = useTranslation()
const [step, setStep] = useState<Step>(Step.init)
@@ -178,7 +176,7 @@ const JinaReader: FC<Props> = ({
}
else {
setCrawlResult(data)
onCheckedCrawlResultChange(supportBatchUpload ? (data.data || []) : (data.data?.slice(0, 1) || [])) // default select the crawl result
onCheckedCrawlResultChange(data.data || []) // default select the crawl result
setCrawlErrorMessage('')
}
}
@@ -190,7 +188,7 @@ const JinaReader: FC<Props> = ({
finally {
setStep(Step.finished)
}
}, [checkValid, crawlOptions, onCheckedCrawlResultChange, onJobIdChange, supportBatchUpload, t, waitForCrawlFinished])
}, [checkValid, crawlOptions, onCheckedCrawlResultChange, onJobIdChange, t, waitForCrawlFinished])
return (
<div>
@@ -229,7 +227,6 @@ const JinaReader: FC<Props> = ({
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
isMultipleChoice={supportBatchUpload}
/>
}
</div>

View File

@@ -26,7 +26,6 @@ type Props = {
onJobIdChange: (jobId: string) => void
crawlOptions: CrawlOptions
onCrawlOptionsChange: (payload: CrawlOptions) => void
supportBatchUpload: boolean
}
enum Step {
@@ -42,7 +41,6 @@ const WaterCrawl: FC<Props> = ({
onJobIdChange,
crawlOptions,
onCrawlOptionsChange,
supportBatchUpload,
}) => {
const { t } = useTranslation()
const [step, setStep] = useState<Step>(Step.init)
@@ -165,7 +163,7 @@ const WaterCrawl: FC<Props> = ({
}
else {
setCrawlResult(data)
onCheckedCrawlResultChange(supportBatchUpload ? (data.data || []) : (data.data?.slice(0, 1) || [])) // default select the crawl result
onCheckedCrawlResultChange(data.data || []) // default select the crawl result
setCrawlErrorMessage('')
}
}
@@ -176,7 +174,7 @@ const WaterCrawl: FC<Props> = ({
finally {
setStep(Step.finished)
}
}, [checkValid, crawlOptions, onCheckedCrawlResultChange, onJobIdChange, supportBatchUpload, t, waitForCrawlFinished])
}, [checkValid, crawlOptions, onCheckedCrawlResultChange, onJobIdChange, t, waitForCrawlFinished])
return (
<div>
@@ -215,7 +213,6 @@ const WaterCrawl: FC<Props> = ({
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
isMultipleChoice={supportBatchUpload}
/>
}
</div>

View File

@@ -28,7 +28,7 @@ export type LocalFileProps = {
const LocalFile = ({
allowedExtensions,
supportBatchUpload = false,
supportBatchUpload = true,
}: LocalFileProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)

View File

@@ -30,7 +30,7 @@ const OnlineDocuments = ({
nodeId,
nodeData,
isInPipeline = false,
supportBatchUpload = false,
supportBatchUpload = true,
onCredentialChange,
}: OnlineDocumentsProps) => {
const docLink = useDocLink()

View File

@@ -29,7 +29,7 @@ const OnlineDrive = ({
nodeId,
nodeData,
isInPipeline = false,
supportBatchUpload = false,
supportBatchUpload = true,
onCredentialChange,
}: OnlineDriveProps) => {
const docLink = useDocLink()

View File

@@ -42,7 +42,7 @@ const WebsiteCrawl = ({
nodeId,
nodeData,
isInPipeline = false,
supportBatchUpload = false,
supportBatchUpload = true,
onCredentialChange,
}: WebsiteCrawlProps) => {
const { t } = useTranslation()

View File

@@ -36,6 +36,10 @@ import { useAddDocumentsSteps, useLocalFile, useOnlineDocument, useOnlineDrive,
import DataSourceProvider from './data-source/store/provider'
import { useDataSourceStore } from './data-source/store'
import { useFileUploadConfig } from '@/service/use-common'
import UpgradeCard from '../../create/step-one/upgrade-card'
import Divider from '@/app/components/base/divider'
import { useBoolean } from 'ahooks'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
const CreateFormPipeline = () => {
const { t } = useTranslation()
@@ -57,7 +61,7 @@ const CreateFormPipeline = () => {
const {
steps,
currentStep,
handleNextStep,
handleNextStep: doHandleNextStep,
handleBackStep,
} = useAddDocumentsSteps()
const {
@@ -104,6 +108,33 @@ const CreateFormPipeline = () => {
}, [allFileLoaded, datasource, datasourceType, enableBilling, isVectorSpaceFull, onlineDocuments.length, onlineDriveFileList.length, websitePages.length])
const supportBatchUpload = !enableBilling || plan.type !== 'sandbox'
const [isShowPlanUpgradeModal, {
setTrue: showPlanUpgradeModal,
setFalse: hidePlanUpgradeModal,
}] = useBoolean(false)
const handleNextStep = useCallback(() => {
if (!supportBatchUpload) {
let isMultiple = false
if (datasourceType === DatasourceType.localFile && localFileList.length > 1)
isMultiple = true
if (datasourceType === DatasourceType.onlineDocument && onlineDocuments.length > 1)
isMultiple = true
if (datasourceType === DatasourceType.websiteCrawl && websitePages.length > 1)
isMultiple = true
if (datasourceType === DatasourceType.onlineDrive && selectedFileIds.length > 1)
isMultiple = true
if (isMultiple) {
showPlanUpgradeModal()
return
}
}
doHandleNextStep()
}, [datasourceType, doHandleNextStep, localFileList.length, onlineDocuments.length, selectedFileIds.length, showPlanUpgradeModal, supportBatchUpload, websitePages.length])
const nextBtnDisabled = useMemo(() => {
if (!datasource) return true
if (datasourceType === DatasourceType.localFile)
@@ -125,16 +156,16 @@ const CreateFormPipeline = () => {
const showSelect = useMemo(() => {
if (datasourceType === DatasourceType.onlineDocument) {
const pagesCount = currentWorkspace?.pages.length ?? 0
return supportBatchUpload && pagesCount > 0
return pagesCount > 0
}
if (datasourceType === DatasourceType.onlineDrive) {
const isBucketList = onlineDriveFileList.some(file => file.type === 'bucket')
return supportBatchUpload && !isBucketList && onlineDriveFileList.filter((item) => {
return !isBucketList && onlineDriveFileList.filter((item) => {
return item.type !== 'bucket'
}).length > 0
}
return false
}, [currentWorkspace?.pages.length, datasourceType, supportBatchUpload, onlineDriveFileList])
}, [currentWorkspace?.pages.length, datasourceType, onlineDriveFileList])
const totalOptions = useMemo(() => {
if (datasourceType === DatasourceType.onlineDocument)
@@ -390,11 +421,12 @@ const CreateFormPipeline = () => {
}, [PagesMapAndSelectedPagesId, currentWorkspace?.pages, dataSourceStore, datasourceType])
const clearDataSourceData = useCallback((dataSource: Datasource) => {
if (dataSource.nodeData.provider_type === DatasourceType.onlineDocument)
const providerType = dataSource.nodeData.provider_type
if (providerType === DatasourceType.onlineDocument)
clearOnlineDocumentData()
else if (dataSource.nodeData.provider_type === DatasourceType.websiteCrawl)
else if (providerType === DatasourceType.websiteCrawl)
clearWebsiteCrawlData()
else if (dataSource.nodeData.provider_type === DatasourceType.onlineDrive)
else if (providerType === DatasourceType.onlineDrive)
clearOnlineDriveData()
}, [clearOnlineDocumentData, clearOnlineDriveData, clearWebsiteCrawlData])
@@ -452,7 +484,6 @@ const CreateFormPipeline = () => {
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
onCredentialChange={handleCredentialChange}
supportBatchUpload={supportBatchUpload}
/>
)}
{datasourceType === DatasourceType.websiteCrawl && (
@@ -460,7 +491,6 @@ const CreateFormPipeline = () => {
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
onCredentialChange={handleCredentialChange}
supportBatchUpload={supportBatchUpload}
/>
)}
{datasourceType === DatasourceType.onlineDrive && (
@@ -468,7 +498,6 @@ const CreateFormPipeline = () => {
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
onCredentialChange={handleCredentialChange}
supportBatchUpload={supportBatchUpload}
/>
)}
{isShowVectorSpaceFull && (
@@ -483,6 +512,14 @@ const CreateFormPipeline = () => {
handleNextStep={handleNextStep}
tip={tip}
/>
{
!supportBatchUpload && datasourceType === DatasourceType.localFile && localFileList.length > 0 && (
<>
<Divider type='horizontal' className='my-4 h-px bg-divider-subtle' />
<UpgradeCard />
</>
)
}
</div>
)
}
@@ -561,6 +598,14 @@ const CreateFormPipeline = () => {
</div>
)
}
{isShowPlanUpgradeModal && (
<PlanUpgradeModal
show
onClose={hidePlanUpgradeModal}
title={t('billing.upgrade.uploadMultiplePages.title')!}
description={t('billing.upgrade.uploadMultiplePages.description')!}
/>
)}
</div>
)
}

View File

@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useMemo } from 'react'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
@@ -11,6 +11,10 @@ import {
import cn from '@/utils/classnames'
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
import Popover from '@/app/components/base/popover'
import { useBoolean } from 'ahooks'
import { useProviderContext } from '@/context/provider-context'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import { Plan } from '@/app/components/billing/type'
export type ISegmentAddProps = {
importStatus: ProcessStatus | string | undefined
@@ -35,6 +39,23 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
embedding,
}) => {
const { t } = useTranslation()
const [isShowPlanUpgradeModal, {
setTrue: showPlanUpgradeModal,
setFalse: hidePlanUpgradeModal,
}] = useBoolean(false)
const { plan, enableBilling } = useProviderContext()
const { type } = plan
const canAdd = enableBilling ? type !== Plan.sandbox : true
const withNeedUpgradeCheck = useCallback((fn: () => void) => {
return () => {
if (!canAdd) {
showPlanUpgradeModal()
return
}
fn()
}
}, [canAdd, showPlanUpgradeModal])
const textColor = useMemo(() => {
return embedding
? 'text-components-button-secondary-accent-text-disabled'
@@ -90,7 +111,7 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
type='button'
className={`inline-flex items-center rounded-l-lg border-r-[1px] border-r-divider-subtle px-2.5 py-2
hover:bg-state-base-hover disabled:cursor-not-allowed disabled:hover:bg-transparent`}
onClick={showNewSegmentModal}
onClick={withNeedUpgradeCheck(showNewSegmentModal)}
disabled={embedding}
>
<RiAddLine className={cn('h-4 w-4', textColor)} />
@@ -108,7 +129,7 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
<button
type='button'
className='system-md-regular flex w-full items-center rounded-lg px-2 py-1.5 text-text-secondary'
onClick={showBatchModal}
onClick={withNeedUpgradeCheck(showBatchModal)}
>
{t('datasetDocuments.list.action.batchAdd')}
</button>
@@ -116,7 +137,7 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
}
btnElement={
<div className='flex items-center justify-center' >
<RiArrowDownSLine className={cn('h-4 w-4', textColor)}/>
<RiArrowDownSLine className={cn('h-4 w-4', textColor)} />
</div>
}
btnClassName={open => cn(
@@ -129,7 +150,16 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
className='h-fit min-w-[128px]'
disabled={embedding}
/>
{isShowPlanUpgradeModal && (
<PlanUpgradeModal
show
onClose={hidePlanUpgradeModal}
title={t('billing.upgrade.addChunks.title')!}
description={t('billing.upgrade.addChunks.description')!}
/>
)}
</div>
)
}
export default React.memo(SegmentAdd)

View File

@@ -9,7 +9,6 @@ export type TriggerEventsLimitModalPayload = {
usage: number
total: number
resetInDays?: number
planType: Plan
storageKey?: string
persistDismiss?: boolean
}
@@ -98,7 +97,6 @@ export const useTriggerEventsLimitModal = ({
payload: {
usage: usage.triggerEvents,
total: total.triggerEvents,
planType: type,
resetInDays: triggerResetInDays,
storageKey,
persistDismiss,

View File

@@ -31,7 +31,7 @@ const triggerEventsLimitModalMock = jest.fn((props: any) => {
latestTriggerEventsModalProps = props
return (
<div data-testid="trigger-limit-modal">
<button type="button" onClick={props.onDismiss}>dismiss</button>
<button type="button" onClick={props.onClose}>dismiss</button>
<button type="button" onClick={props.onUpgrade}>upgrade</button>
</div>
)
@@ -115,11 +115,10 @@ describe('ModalContextProvider trigger events limit modal', () => {
usage: 3000,
total: 3000,
resetInDays: 5,
planType: Plan.professional,
})
act(() => {
latestTriggerEventsModalProps.onDismiss()
latestTriggerEventsModalProps.onClose()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
@@ -149,7 +148,7 @@ describe('ModalContextProvider trigger events limit modal', () => {
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
act(() => {
latestTriggerEventsModalProps.onDismiss()
latestTriggerEventsModalProps.onClose()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
@@ -177,7 +176,7 @@ describe('ModalContextProvider trigger events limit modal', () => {
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
act(() => {
latestTriggerEventsModalProps.onDismiss()
latestTriggerEventsModalProps.onClose()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())

View File

@@ -485,9 +485,8 @@ export const ModalContextProvider = ({
show
usage={showTriggerEventsLimitModal.payload.usage}
total={showTriggerEventsLimitModal.payload.total}
planType={showTriggerEventsLimitModal.payload.planType}
resetInDays={showTriggerEventsLimitModal.payload.resetInDays}
onDismiss={() => {
onClose={() => {
persistTriggerEventsLimitModalDismiss()
setShowTriggerEventsLimitModal(null)
}}

View File

@@ -221,6 +221,20 @@ const translation = {
fullTipLine2: 'annotate more conversations.',
quotaTitle: 'Annotation Reply Quota',
},
upgrade: {
uploadMultiplePages: {
title: 'Upgrade to upload multiple documents at once',
description: 'Youve reached the upload limit — only one document can be selected and uploaded at a time on your current plan.',
},
uploadMultipleFiles: {
title: 'Upgrade to unlock batch document upload',
description: 'Batch-upload more documents at once to save time and improve efficiency.',
},
addChunks: {
title: 'Upgrade to continue adding chunks',
description: 'Youve reached the limit of adding chunks for this plan.',
},
},
}
export default translation

View File

@@ -202,6 +202,20 @@ const translation = {
quotaTitle: '注釈返信クォータ',
},
teamMembers: 'チームメンバー',
upgrade: {
uploadMultiplePages: {
title: '複数ドキュメントを一度にアップロードするにはアップグレード',
description: '現在のプランではアップロード上限に達しています。1回の操作で選択・アップロードできるドキュメントは1つのみです。',
},
uploadMultipleFiles: {
title: '一括ドキュメントアップロード機能を解放するにはアップグレードが必要です',
description: '複数のドキュメントを一度にバッチアップロードすることで、時間を節約し、作業効率を向上できます。',
},
addChunks: {
title: 'アップグレードして、チャンクを引き続き追加できるようにしてください。',
description: 'このプランでは、チャンク追加の上限に達しています。',
},
},
}
export default translation

View File

@@ -202,6 +202,20 @@ const translation = {
quotaTitle: '标注的配额',
},
teamMembers: '团队成员',
upgrade: {
uploadMultiplePages: {
title: '升级以一次性上传多个文档',
description: '您已达到当前套餐的上传限制 —— 该套餐每次只能选择并上传 1 个文档。',
},
uploadMultipleFiles: {
title: '升级以解锁批量文档上传功能',
description: '一次性批量上传更多文档,以节省时间并提升效率。',
},
addChunks: {
title: '升级以继续添加分段',
description: '您已达到此计划的添加分段上限。',
},
},
}
export default translation