Merge remote-tracking branch 'origin/main' into refactor-text-generate-item

This commit is contained in:
yyh 2025-12-11 18:00:09 +08:00
commit ab3a933e11
No known key found for this signature in database
62 changed files with 1469 additions and 222 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
22.11.0

View File

@ -2,7 +2,7 @@ import logging
from typing import Any, Literal
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import InternalServerError, NotFound
import services
@ -52,10 +52,24 @@ class ChatMessagePayload(BaseModel):
inputs: dict[str, Any]
query: str
files: list[dict[str, Any]] | None = None
conversation_id: UUID | None = None
parent_message_id: UUID | None = None
conversation_id: str | None = None
parent_message_id: str | None = None
retriever_from: str = Field(default="explore_app")
@field_validator("conversation_id", "parent_message_id", mode="before")
@classmethod
def normalize_uuid(cls, value: str | UUID | None) -> str | None:
"""
Accept blank IDs and validate UUID format when provided.
"""
if not value:
return None
try:
return helper.uuid_value(value)
except ValueError as exc:
raise ValueError("must be a valid UUID") from exc
register_schema_models(console_ns, CompletionMessagePayload, ChatMessagePayload)

View File

@ -3,7 +3,7 @@ from uuid import UUID
from flask import request
from flask_restx import marshal_with
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
@ -30,9 +30,16 @@ class ConversationListQuery(BaseModel):
class ConversationRenamePayload(BaseModel):
name: str
name: str | None = None
auto_generate: bool = False
@model_validator(mode="after")
def validate_name_requirement(self):
if not self.auto_generate:
if self.name is None or not self.name.strip():
raise ValueError("name is required when auto_generate is false")
return self
register_schema_models(console_ns, ConversationListQuery, ConversationRenamePayload)

View File

@ -4,7 +4,7 @@ from uuid import UUID
from flask import request
from flask_restx import Resource
from flask_restx._http import HTTPStatus
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, NotFound
@ -37,9 +37,16 @@ class ConversationListQuery(BaseModel):
class ConversationRenamePayload(BaseModel):
name: str = Field(description="New conversation name")
name: str | None = Field(default=None, description="New conversation name (required if auto_generate is false)")
auto_generate: bool = Field(default=False, description="Auto-generate conversation name")
@model_validator(mode="after")
def validate_name_requirement(self):
if not self.auto_generate:
if self.name is None or not self.name.strip():
raise ValueError("name is required when auto_generate is false")
return self
class ConversationVariablesQuery(BaseModel):
last_id: UUID | None = Field(default=None, description="Last variable ID for pagination")

View File

@ -118,7 +118,7 @@ class ConversationService:
app_model: App,
conversation_id: str,
user: Union[Account, EndUser] | None,
name: str,
name: str | None,
auto_generate: bool,
):
conversation = cls.get_conversation(app_model, conversation_id, user)

View File

@ -0,0 +1,20 @@
import pytest
from pydantic import ValidationError
from controllers.console.explore.conversation import ConversationRenamePayload as ConsolePayload
from controllers.service_api.app.conversation import ConversationRenamePayload as ServicePayload
@pytest.mark.parametrize("payload_cls", [ConsolePayload, ServicePayload])
def test_payload_allows_auto_generate_without_name(payload_cls):
payload = payload_cls.model_validate({"auto_generate": True})
assert payload.auto_generate is True
assert payload.name is None
@pytest.mark.parametrize("payload_cls", [ConsolePayload, ServicePayload])
@pytest.mark.parametrize("value", [None, "", " "])
def test_payload_requires_name_when_not_auto_generate(payload_cls, value):
with pytest.raises(ValidationError):
payload_cls.model_validate({"name": value, "auto_generate": False})

View File

@ -0,0 +1,49 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import ConfirmAddVar from './index'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
jest.mock('../../base/var-highlight', () => ({
__esModule: true,
default: ({ name }: { name: string }) => <span data-testid="var-highlight">{name}</span>,
}))
describe('ConfirmAddVar', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should render variable names', () => {
render(<ConfirmAddVar varNameArr={['foo', 'bar']} onConfirm={jest.fn()} onCancel={jest.fn()} onHide={jest.fn()} />)
const highlights = screen.getAllByTestId('var-highlight')
expect(highlights).toHaveLength(2)
expect(highlights[0]).toHaveTextContent('foo')
expect(highlights[1]).toHaveTextContent('bar')
})
it('should trigger cancel actions', () => {
const onConfirm = jest.fn()
const onCancel = jest.fn()
render(<ConfirmAddVar varNameArr={['foo']} onConfirm={onConfirm} onCancel={onCancel} onHide={jest.fn()} />)
fireEvent.click(screen.getByText('common.operation.cancel'))
expect(onCancel).toHaveBeenCalledTimes(1)
})
it('should trigger confirm actions', () => {
const onConfirm = jest.fn()
const onCancel = jest.fn()
render(<ConfirmAddVar varNameArr={['foo']} onConfirm={onConfirm} onCancel={onCancel} onHide={jest.fn()} />)
fireEvent.click(screen.getByText('common.operation.add'))
expect(onConfirm).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,56 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import EditModal from './edit-modal'
import type { ConversationHistoriesRole } from '@/models/debug'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
jest.mock('@/app/components/base/modal', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
describe('Conversation history edit modal', () => {
const data: ConversationHistoriesRole = {
user_prefix: 'user',
assistant_prefix: 'assistant',
}
beforeEach(() => {
jest.clearAllMocks()
})
it('should render provided prefixes', () => {
render(<EditModal isShow saveLoading={false} data={data} onClose={jest.fn()} onSave={jest.fn()} />)
expect(screen.getByDisplayValue('user')).toBeInTheDocument()
expect(screen.getByDisplayValue('assistant')).toBeInTheDocument()
})
it('should update prefixes and save changes', () => {
const onSave = jest.fn()
render(<EditModal isShow saveLoading={false} data={data} onClose={jest.fn()} onSave={onSave} />)
fireEvent.change(screen.getByDisplayValue('user'), { target: { value: 'member' } })
fireEvent.change(screen.getByDisplayValue('assistant'), { target: { value: 'helper' } })
fireEvent.click(screen.getByText('common.operation.save'))
expect(onSave).toHaveBeenCalledWith({
user_prefix: 'member',
assistant_prefix: 'helper',
})
})
it('should call close handler', () => {
const onClose = jest.fn()
render(<EditModal isShow saveLoading={false} data={data} onClose={onClose} onSave={jest.fn()} />)
fireEvent.click(screen.getByText('common.operation.cancel'))
expect(onClose).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,48 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import HistoryPanel from './history-panel'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
const mockDocLink = jest.fn(() => 'doc-link')
jest.mock('@/context/i18n', () => ({
useDocLink: () => mockDocLink,
}))
jest.mock('@/app/components/app/configuration/base/operation-btn', () => ({
__esModule: true,
default: ({ onClick }: { onClick: () => void }) => (
<button type="button" data-testid="edit-button" onClick={onClick}>
edit
</button>
),
}))
jest.mock('@/app/components/app/configuration/base/feature-panel', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
describe('HistoryPanel', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should render warning content and link when showWarning is true', () => {
render(<HistoryPanel showWarning onShowEditModal={jest.fn()} />)
expect(screen.getByText('appDebug.feature.conversationHistory.tip')).toBeInTheDocument()
const link = screen.getByText('appDebug.feature.conversationHistory.learnMore')
expect(link).toHaveAttribute('href', 'doc-link')
})
it('should hide warning when showWarning is false', () => {
render(<HistoryPanel showWarning={false} onShowEditModal={jest.fn()} />)
expect(screen.queryByText('appDebug.feature.conversationHistory.tip')).toBeNull()
})
})

View File

@ -0,0 +1,351 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import Prompt, { type IPromptProps } from './index'
import ConfigContext from '@/context/debug-configuration'
import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config'
import { type PromptItem, PromptRole, type PromptVariable } from '@/models/debug'
import { AppModeEnum, ModelModeType } from '@/types/app'
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
type DebugConfiguration = {
isAdvancedMode: boolean
currentAdvancedPrompt: PromptItem | PromptItem[]
setCurrentAdvancedPrompt: (prompt: PromptItem | PromptItem[], isUserChanged?: boolean) => void
modelModeType: ModelModeType
dataSets: Array<{
id: string
name?: string
}>
hasSetBlockStatus: {
context: boolean
history: boolean
query: boolean
}
}
const defaultPromptVariables: PromptVariable[] = [
{ key: 'var', name: 'Variable', type: 'string', required: true },
]
let mockSimplePromptInputProps: IPromptProps | null = null
jest.mock('./simple-prompt-input', () => ({
__esModule: true,
default: (props: IPromptProps) => {
mockSimplePromptInputProps = props
return (
<div
data-testid="simple-prompt-input"
data-mode={props.mode}
data-template={props.promptTemplate}
data-readonly={props.readonly ?? false}
data-no-title={props.noTitle ?? false}
data-gradient-border={props.gradientBorder ?? false}
data-editor-height={props.editorHeight ?? ''}
data-no-resize={props.noResize ?? false}
onClick={() => props.onChange?.('mocked prompt', props.promptVariables)}
>
SimplePromptInput Mock
</div>
)
},
}))
type AdvancedMessageInputProps = {
isChatMode: boolean
type: PromptRole
value: string
onTypeChange: (value: PromptRole) => void
canDelete: boolean
onDelete: () => void
onChange: (value: string) => void
promptVariables: PromptVariable[]
isContextMissing: boolean
onHideContextMissingTip: () => void
noResize?: boolean
}
jest.mock('./advanced-prompt-input', () => ({
__esModule: true,
default: (props: AdvancedMessageInputProps) => {
return (
<div
data-testid="advanced-message-input"
data-type={props.type}
data-value={props.value}
data-chat-mode={props.isChatMode}
data-can-delete={props.canDelete}
data-context-missing={props.isContextMissing}
>
<button type="button" onClick={() => props.onChange('updated text')}>
change
</button>
<button type="button" onClick={() => props.onTypeChange(PromptRole.assistant)}>
type
</button>
<button type="button" onClick={props.onDelete}>
delete
</button>
<button type="button" onClick={props.onHideContextMissingTip}>
hide-context
</button>
</div>
)
},
}))
const getContextValue = (overrides: Partial<DebugConfiguration> = {}): DebugConfiguration => {
return {
setCurrentAdvancedPrompt: jest.fn(),
isAdvancedMode: false,
currentAdvancedPrompt: [],
modelModeType: ModelModeType.chat,
dataSets: [],
hasSetBlockStatus: {
context: false,
history: false,
query: false,
},
...overrides,
}
}
const renderComponent = (
props: Partial<IPromptProps> = {},
contextOverrides: Partial<DebugConfiguration> = {},
) => {
const mergedProps: IPromptProps = {
mode: AppModeEnum.CHAT,
promptTemplate: 'initial template',
promptVariables: defaultPromptVariables,
onChange: jest.fn(),
...props,
}
const contextValue = getContextValue(contextOverrides)
return {
contextValue,
...render(
<ConfigContext.Provider value={contextValue as any}>
<Prompt {...mergedProps} />
</ConfigContext.Provider>,
),
}
}
describe('Prompt config component', () => {
beforeEach(() => {
jest.clearAllMocks()
mockSimplePromptInputProps = null
})
// Rendering simple mode
it('should render simple prompt when advanced mode is disabled', () => {
const onChange = jest.fn()
renderComponent({ onChange }, { isAdvancedMode: false })
const simplePrompt = screen.getByTestId('simple-prompt-input')
expect(simplePrompt).toBeInTheDocument()
expect(simplePrompt).toHaveAttribute('data-mode', AppModeEnum.CHAT)
expect(mockSimplePromptInputProps?.promptTemplate).toBe('initial template')
fireEvent.click(simplePrompt)
expect(onChange).toHaveBeenCalledWith('mocked prompt', defaultPromptVariables)
expect(screen.queryByTestId('advanced-message-input')).toBeNull()
})
// Rendering advanced chat messages
it('should render advanced chat prompts and show context missing tip when dataset context is not set', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
]
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
dataSets: [{ id: 'ds' } as unknown as DebugConfiguration['dataSets'][number]],
hasSetBlockStatus: { context: false, history: true, query: true },
},
)
const renderedMessages = screen.getAllByTestId('advanced-message-input')
expect(renderedMessages).toHaveLength(2)
expect(renderedMessages[0]).toHaveAttribute('data-context-missing', 'true')
fireEvent.click(screen.getAllByText('hide-context')[0])
expect(screen.getAllByTestId('advanced-message-input')[0]).toHaveAttribute('data-context-missing', 'false')
})
// Chat message mutations
it('should update chat prompt value and call setter with user change flag', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getAllByText('change')[0])
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith(
[
{ role: PromptRole.user, text: 'updated text' },
{ role: PromptRole.assistant, text: 'second' },
],
true,
)
})
it('should update chat prompt role when type changes', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.user, text: 'second' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getAllByText('type')[1])
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith(
[
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
],
)
})
it('should delete chat prompt item', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: 'second' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getAllByText('delete')[0])
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([{ role: PromptRole.assistant, text: 'second' }])
})
// Add message behavior
it('should append a mirrored role message when clicking add in chat mode', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.user, text: 'first' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('appDebug.promptMode.operation.addMessage'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([
{ role: PromptRole.user, text: 'first' },
{ role: PromptRole.assistant, text: '' },
])
})
it('should append a user role when the last chat prompt is from assistant', () => {
const currentAdvancedPrompt: PromptItem[] = [
{ role: PromptRole.assistant, text: 'reply' },
]
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt,
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('appDebug.promptMode.operation.addMessage'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([
{ role: PromptRole.assistant, text: 'reply' },
{ role: PromptRole.user, text: '' },
])
})
it('should insert a system message when adding to an empty chat prompt list', () => {
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt: [],
modelModeType: ModelModeType.chat,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('appDebug.promptMode.operation.addMessage'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([{ role: PromptRole.system, text: '' }])
})
it('should not show add button when reaching max prompt length', () => {
const prompts: PromptItem[] = Array.from({ length: MAX_PROMPT_MESSAGE_LENGTH }, (_, index) => ({
role: PromptRole.user,
text: `item-${index}`,
}))
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt: prompts,
modelModeType: ModelModeType.chat,
},
)
expect(screen.queryByText('appDebug.promptMode.operation.addMessage')).toBeNull()
})
// Completion mode
it('should update completion prompt value and flag as user change', () => {
const setCurrentAdvancedPrompt = jest.fn()
renderComponent(
{},
{
isAdvancedMode: true,
currentAdvancedPrompt: { role: PromptRole.user, text: 'single' },
modelModeType: ModelModeType.completion,
setCurrentAdvancedPrompt,
},
)
fireEvent.click(screen.getByText('change'))
expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith({ role: PromptRole.user, text: 'updated text' }, true)
})
})

View File

@ -0,0 +1,37 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import MessageTypeSelector from './message-type-selector'
import { PromptRole } from '@/models/debug'
describe('MessageTypeSelector', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should render current value and keep options hidden by default', () => {
render(<MessageTypeSelector value={PromptRole.user} onChange={jest.fn()} />)
expect(screen.getByText(PromptRole.user)).toBeInTheDocument()
expect(screen.queryByText(PromptRole.system)).toBeNull()
})
it('should toggle option list when clicking the selector', () => {
render(<MessageTypeSelector value={PromptRole.system} onChange={jest.fn()} />)
fireEvent.click(screen.getByText(PromptRole.system))
expect(screen.getByText(PromptRole.user)).toBeInTheDocument()
expect(screen.getByText(PromptRole.assistant)).toBeInTheDocument()
})
it('should call onChange with selected type and close the list', () => {
const onChange = jest.fn()
render(<MessageTypeSelector value={PromptRole.assistant} onChange={onChange} />)
fireEvent.click(screen.getByText(PromptRole.assistant))
fireEvent.click(screen.getByText(PromptRole.user))
expect(onChange).toHaveBeenCalledWith(PromptRole.user)
expect(screen.queryByText(PromptRole.system)).toBeNull()
})
})

View File

@ -0,0 +1,66 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
describe('PromptEditorHeightResizeWrap', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.useFakeTimers()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should render children, footer, and hide resize handler when requested', () => {
const { container } = render(
<PromptEditorHeightResizeWrap
className="wrapper"
height={150}
minHeight={100}
onHeightChange={jest.fn()}
footer={<div>footer</div>}
hideResize
>
<div>content</div>
</PromptEditorHeightResizeWrap>,
)
expect(screen.getByText('content')).toBeInTheDocument()
expect(screen.getByText('footer')).toBeInTheDocument()
expect(container.querySelector('.cursor-row-resize')).toBeNull()
})
it('should resize height with mouse events and clamp to minHeight', () => {
const onHeightChange = jest.fn()
const { container } = render(
<PromptEditorHeightResizeWrap
height={150}
minHeight={100}
onHeightChange={onHeightChange}
>
<div>content</div>
</PromptEditorHeightResizeWrap>,
)
const handle = container.querySelector('.cursor-row-resize')
expect(handle).not.toBeNull()
fireEvent.mouseDown(handle as Element, { clientY: 100 })
expect(document.body.style.userSelect).toBe('none')
fireEvent.mouseMove(document, { clientY: 130 })
jest.runAllTimers()
expect(onHeightChange).toHaveBeenLastCalledWith(180)
onHeightChange.mockClear()
fireEvent.mouseMove(document, { clientY: -100 })
jest.runAllTimers()
expect(onHeightChange).toHaveBeenLastCalledWith(100)
fireEvent.mouseUp(document)
expect(document.body.style.userSelect).toBe('')
})
})

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

@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Abrechnung und Abonnements',
viewBillingDescription: 'Zahlungsmethoden, Rechnungen und Abonnementänderungen verwalten',
viewBillingAction: 'Verwalten',
upgrade: {
uploadMultiplePages: {
title: 'Upgrade, um mehrere Dokumente gleichzeitig hochzuladen',
description: 'Sie haben das Upload-Limit erreicht in Ihrem aktuellen Tarif kann jeweils nur ein Dokument ausgewählt und hochgeladen werden.',
},
uploadMultipleFiles: {
title: 'Upgrade, um den Massen-Upload von Dokumenten freizuschalten',
description: 'Lade mehrere Dokumente gleichzeitig hoch, um Zeit zu sparen und die Effizienz zu steigern.',
},
addChunks: {
title: 'Upgraden, um weiterhin Abschnitte hinzuzufügen',
description: 'Sie haben das Limit für das Hinzufügen von Abschnitten in diesem Tarif erreicht.',
},
},
}
export default translation

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

@ -161,7 +161,7 @@ const translation = {
includesTitle: 'Todo de Community, además:',
name: 'Premium',
for: 'Para organizaciones y equipos de tamaño mediano',
features: ['Confiabilidad Autogestionada por Diversos Proveedores de Nube', 'Espacio de trabajo único', 'Personalización de Logotipo y Marca de la Aplicación Web', 'Soporte prioritario por correo electrónico y chat'],
features: ['Confiabilidad autogestionada por varios proveedores de la nube', 'Espacio de trabajo único', 'Personalización de Logotipo y Marca de la Aplicación Web', 'Soporte prioritario por correo electrónico y chat'],
},
},
vectorSpace: {
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Facturación y Suscripciones',
viewBillingDescription: 'Gestiona métodos de pago, facturas y cambios de suscripción',
viewBillingAction: 'Gestionar',
upgrade: {
uploadMultiplePages: {
title: 'Actualiza para subir varios documentos a la vez',
description: 'Has alcanzado el límite de carga: solo se puede seleccionar y subir un documento a la vez en tu plan actual.',
},
uploadMultipleFiles: {
title: 'Actualiza para desbloquear la carga de documentos en lote',
description: 'Carga en lote más documentos a la vez para ahorrar tiempo y mejorar la eficiencia.',
},
addChunks: {
title: 'Actualiza para seguir agregando fragmentos',
description: 'Has alcanzado el límite de agregar fragmentos para este plan.',
},
},
}
export default translation

View File

@ -161,7 +161,7 @@ const translation = {
name: 'پیشرفته',
priceTip: 'بر اساس بازار ابری',
comingSoon: 'پشتیبانی مایکروسافت آژور و گوگل کلود به زودی در دسترس خواهد بود',
features: ['قابلیت اطمینان خودمدیریتی توسط ارائه‌دهندگان مختلف ابری', 'فضای کاری تنها', 'سفارشی‌سازی لوگو و برند وب‌اپ', 'پشتیبانی اولویت‌دار ایمیل و چت'],
features: ['قابلیت اطمینان خودمدیریتی توسط ارائه‌دهندگان مختلف ابری', 'فضای کاری تنها', 'سفارشی‌سازی لوگو و برندینگ وب‌اپ', 'پشتیبانی اولویت‌دار ایمیل و چت'],
},
},
vectorSpace: {
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'صورتحساب و اشتراک‌ها',
viewBillingDescription: 'مدیریت روش‌های پرداخت، صورت‌حساب‌ها و تغییرات اشتراک',
viewBillingAction: 'مدیریت',
upgrade: {
uploadMultiplePages: {
title: 'ارتقا برای آپلود همزمان چندین سند',
description: 'شما به حد آپلود رسیده‌اید — در طرح فعلی خود تنها می‌توانید یک سند را در هر بار انتخاب و آپلود کنید.',
},
uploadMultipleFiles: {
title: 'ارتقا دهید تا امکان بارگذاری دسته‌ای اسناد فعال شود',
description: 'بارگذاری دسته‌ای چندین سند به‌طور همزمان برای صرفه‌جویی در زمان و افزایش کارایی.',
},
addChunks: {
title: 'برای ادامه افزودن بخش‌ها ارتقا دهید',
description: 'شما به حد اضافه کردن بخش‌ها برای این طرح رسیده‌اید.',
},
},
}
export default translation

View File

@ -137,7 +137,7 @@ const translation = {
name: 'Entreprise',
description: 'Obtenez toutes les capacités et le support pour les systèmes à grande échelle et critiques pour la mission.',
includesTitle: 'Tout ce qui est inclus dans le plan Équipe, plus :',
features: ['Solutions de déploiement évolutives de niveau entreprise', 'Autorisation de licence commerciale', 'Fonctionnalités exclusives pour les entreprises', 'Espaces de travail multiples et gestion d\'entreprise', 'SSO', 'Accords de niveau de service négociés par les partenaires de Dify', 'Sécurité et contrôles avancés', 'Mises à jour et maintenance par Dify Officiellement', 'Assistance technique professionnelle'],
features: ['Solutions de déploiement évolutives de niveau entreprise', 'Autorisation de licence commerciale', 'Fonctionnalités exclusives pour les entreprises', 'Espaces de travail multiples et gestion d\'entreprise', 'SSO', 'Accords sur les SLA négociés par les partenaires Dify', 'Sécurité et contrôles avancés', 'Mises à jour et maintenance par Dify Officiellement', 'Assistance technique professionnelle'],
for: 'Pour les équipes de grande taille',
btnText: 'Contacter les ventes',
priceTip: 'Facturation Annuel Seulement',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Facturation et abonnements',
viewBillingDescription: 'Gérer les méthodes de paiement, les factures et les modifications d\'abonnement',
viewBillingAction: 'Gérer',
upgrade: {
uploadMultiplePages: {
title: 'Passez à la version supérieure pour télécharger plusieurs documents à la fois',
description: 'Vous avez atteint la limite de téléchargement — un seul document peut être sélectionné et téléchargé à la fois avec votre abonnement actuel.',
},
uploadMultipleFiles: {
title: 'Passez à la version supérieure pour débloquer le téléchargement de documents en lot',
description: 'Téléchargez plusieurs documents à la fois pour gagner du temps et améliorer l\'efficacité.',
},
addChunks: {
title: 'Mettez à niveau pour continuer à ajouter des morceaux',
description: 'Vous avez atteint la limite d\'ajout de morceaux pour ce plan.',
},
},
}
export default translation

View File

@ -213,6 +213,20 @@ const translation = {
viewBillingTitle: 'बिलिंग और सब्सक्रिप्शन',
viewBillingDescription: 'भुगतान के तरीकों, चालानों और सदस्यता में बदलावों का प्रबंधन करें',
viewBillingAction: 'प्रबंध करना',
upgrade: {
uploadMultiplePages: {
title: 'एक बार में कई दस्तावेज़ अपलोड करने के लिए अपग्रेड करें',
description: 'आपने अपलोड की सीमा तक पहुँच लिया है — आपके वर्तमान प्लान पर एक समय में केवल एक ही दस्तावेज़ चुना और अपलोड किया जा सकता है।',
},
uploadMultipleFiles: {
title: 'बैच दस्तावेज़ अपलोड अनलॉक करने के लिए अपग्रेड करें',
description: 'समय बचाने और कार्यक्षमता बढ़ाने के लिए एक बार में अधिक दस्तावेज़ बैच-अपलोड करें।',
},
addChunks: {
title: 'अधिक चंक्स जोड़ने के लिए अपग्रेड करें',
description: 'आप इस योजना के लिए टुकड़े जोड़ने की सीमा तक पहुँच चुके हैं।',
},
},
}
export default translation

View File

@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Penagihan dan Langganan',
viewBillingDescription: 'Kelola metode pembayaran, faktur, dan perubahan langganan',
viewBillingAction: 'Kelola',
upgrade: {
uploadMultiplePages: {
title: 'Tingkatkan untuk mengunggah beberapa dokumen sekaligus',
description: 'Anda telah mencapai batas unggah — hanya satu dokumen yang dapat dipilih dan diunggah sekaligus dengan paket Anda saat ini.',
},
uploadMultipleFiles: {
title: 'Tingkatkan untuk membuka unggahan dokumen batch',
description: 'Unggah lebih banyak dokumen sekaligus untuk menghemat waktu dan meningkatkan efisiensi.',
},
addChunks: {
title: 'Tingkatkan untuk terus menambahkan potongan',
description: 'Anda telah mencapai batas penambahan potongan untuk paket ini.',
},
},
}
export default translation

View File

@ -164,7 +164,7 @@ const translation = {
for: 'Per utenti individuali, piccole squadre o progetti non commerciali',
},
premium: {
features: ['Affidabilità autogestita dai vari provider cloud', 'Spazio di lavoro singolo', 'Personalizzazione del Logo e del Marchio dell\'App Web', 'Assistenza Prioritaria via Email e Chat'],
features: ['Affidabilità Autogestita dai Vari Provider Cloud', 'Spazio di lavoro singolo', 'Personalizzazione del Logo e del Marchio dell\'App Web', 'Assistenza Prioritaria via Email e Chat'],
name: 'Premium',
priceTip: 'Basato su Cloud Marketplace',
includesTitle: 'Tutto dalla Community, oltre a:',
@ -213,6 +213,20 @@ const translation = {
viewBillingTitle: 'Fatturazione e Abbonamenti',
viewBillingDescription: 'Gestisci metodi di pagamento, fatture e modifiche all\'abbonamento',
viewBillingAction: 'Gestire',
upgrade: {
uploadMultiplePages: {
title: 'Aggiorna per caricare più documenti contemporaneamente',
description: 'Hai raggiunto il limite di caricamento: sul tuo piano attuale può essere selezionato e caricato un solo documento alla volta.',
},
uploadMultipleFiles: {
title: 'Aggiorna per sbloccare il caricamento di documenti in batch',
description: 'Carica più documenti contemporaneamente per risparmiare tempo e migliorare l\'efficienza.',
},
addChunks: {
title: 'Aggiorna per continuare ad aggiungere blocchi',
description: 'Hai raggiunto il limite di aggiunta di blocchi per questo piano.',
},
},
}
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

@ -152,7 +152,7 @@ const translation = {
btnText: '판매 문의하기',
for: '대규모 팀을 위해',
priceTip: '연간 청구 전용',
features: ['기업용 확장형 배포 솔루션', '상업용 라이선스 승인', '독점 기업 기능', '여러 작업 공간 및 기업 관리', '싱글 사인온', 'Dify 파트너가 협상한 SLA', '고급 보안 및 제어', 'Dify 공식 업데이트 및 유지보수', '전문 기술 지원'],
features: ['기업용 확장형 배포 솔루션', '상업용 라이선스 승인', '독점 기업 기능', '여러 작업 공간 및 기업 관리', '싱글 사인온', 'Dify 파트너가 협상한 SLA', '고급 보안 및 제어', 'Dify 공식 업데이트 및 유지 관리', '전문 기술 지원'],
},
community: {
btnText: '커뮤니티 시작하기',
@ -215,6 +215,20 @@ const translation = {
viewBillingTitle: '청구 및 구독',
viewBillingDescription: '결제 수단, 청구서 및 구독 변경 관리',
viewBillingAction: '관리하다',
upgrade: {
uploadMultiplePages: {
title: '한 번에 여러 문서를 업로드하려면 업그레이드하세요',
description: '업로드 한도에 도달했습니다 — 현재 요금제에서는 한 번에 한 개의 문서만 선택하고 업로드할 수 있습니다.',
},
uploadMultipleFiles: {
title: '업그레이드하여 대량 문서 업로드 기능 잠금 해제',
description: '한 번에 더 많은 문서를 일괄 업로드하여 시간 절약과 효율성을 높이세요.',
},
addChunks: {
title: '계속해서 조각을 추가하려면 업그레이드하세요',
description: '이 요금제에서는 더 이상 청크를 추가할 수 있는 한도에 도달했습니다.',
},
},
}
export default translation

View File

@ -147,7 +147,7 @@ const translation = {
description:
'Uzyskaj pełne możliwości i wsparcie dla systemów o kluczowym znaczeniu dla misji.',
includesTitle: 'Wszystko w planie Zespołowym, plus:',
features: ['Skalowalne rozwiązania wdrożeniowe klasy korporacyjnej', 'Autoryzacja licencji komercyjnej', 'Ekskluzywne funkcje dla przedsiębiorstw', 'Wiele przestrzeni roboczych i zarządzanie przedsiębiorstwem', 'SSO', 'Negocjowane umowy SLA przez partnerów Dify', 'Zaawansowane zabezpieczenia i kontrola', 'Aktualizacje i konserwacja przez Dify oficjalnie', 'Profesjonalne wsparcie techniczne'],
features: ['Rozwiązania wdrożeniowe klasy korporacyjnej, skalowalne', 'Autoryzacja licencji komercyjnej', 'Ekskluzywne funkcje dla przedsiębiorstw', 'Wiele przestrzeni roboczych i zarządzanie przedsiębiorstwem', 'SSO', 'Negocjowane umowy SLA przez partnerów Dify', 'Zaawansowane zabezpieczenia i kontrola', 'Aktualizacje i konserwacja przez Dify oficjalnie', 'Profesjonalne wsparcie techniczne'],
priceTip: 'Tylko roczne fakturowanie',
btnText: 'Skontaktuj się z działem sprzedaży',
for: 'Dla dużych zespołów',
@ -163,7 +163,7 @@ const translation = {
for: 'Dla użytkowników indywidualnych, małych zespołów lub projektów niekomercyjnych',
},
premium: {
features: ['Niezawodność zarządzana samodzielnie przez różnych dostawców chmury', 'Pojedyncza przestrzeń robocza', 'Dostosowywanie logo i identyfikacji wizualnej aplikacji webowej', 'Priorytetowe wsparcie e-mail i czat'],
features: ['Niezawodność zarządzana samodzielnie przez różnych dostawców chmury', 'Pojedyncza przestrzeń robocza', 'Dostosowywanie logo i marki aplikacji webowej', 'Priorytetowe wsparcie e-mail i czat'],
description: 'Dla średnich organizacji i zespołów',
for: 'Dla średnich organizacji i zespołów',
name: 'Premium',
@ -212,6 +212,20 @@ const translation = {
viewBillingTitle: 'Rozliczenia i subskrypcje',
viewBillingDescription: 'Zarządzaj metodami płatności, fakturami i zmianami subskrypcji',
viewBillingAction: 'Zarządzać',
upgrade: {
uploadMultiplePages: {
title: 'Przejdź na wyższą wersję, aby przesyłać wiele dokumentów jednocześnie',
description: 'Osiągnąłeś limit przesyłania — w ramach obecnego planu można wybrać i przesłać tylko jeden dokument naraz.',
},
uploadMultipleFiles: {
title: 'Uaktualnij, aby odblokować przesyłanie dokumentów wsadowych',
description: 'Przesyłaj wiele dokumentów jednocześnie, aby zaoszczędzić czas i zwiększyć wydajność.',
},
addChunks: {
title: 'Uaktualnij, aby kontynuować dodawanie fragmentów',
description: 'Osiągnąłeś limit dodawania fragmentów w tym planie.',
},
},
}
export default translation

View File

@ -153,7 +153,7 @@ const translation = {
for: 'Para Usuários Individuais, Pequenas Equipes ou Projetos Não Comerciais',
},
premium: {
features: ['Confiabilidade Autogerenciada por Diversos Provedores de Nuvem', 'Espaço de Trabalho Único', 'Personalização de Logo e Marca do WebApp', 'Suporte Prioritário por Email e Chat'],
features: ['Confiabilidade Autogerenciada por Diversos Provedores de Nuvem', 'Espaço de Trabalho Único', 'Personalização de Logo e Branding do WebApp', 'Suporte Prioritário por E-mail e Chat'],
includesTitle: 'Tudo da Comunidade, além de:',
for: 'Para organizações e equipes de médio porte',
price: 'Escalável',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Faturamento e Assinaturas',
viewBillingDescription: 'Gerencie métodos de pagamento, faturas e alterações de assinatura',
viewBillingAction: 'Gerenciar',
upgrade: {
uploadMultiplePages: {
title: 'Atualize para enviar vários documentos de uma vez',
description: 'Você atingiu o limite de upload — apenas um documento pode ser selecionado e enviado por vez no seu plano atual.',
},
uploadMultipleFiles: {
title: 'Atualize para desbloquear o envio de documentos em lote',
description: 'Faça upload de mais documentos de uma vez para economizar tempo e aumentar a eficiência.',
},
addChunks: {
title: 'Faça upgrade para continuar adicionando blocos',
description: 'Você atingiu o limite de adição de blocos para este plano.',
},
},
}
export default translation

View File

@ -137,14 +137,14 @@ const translation = {
name: 'Întreprindere',
description: 'Obțineți capacități și asistență complete pentru sisteme critice la scară largă.',
includesTitle: 'Tot ce este în planul Echipă, plus:',
features: ['Soluții de implementare scalabile la nivel de întreprindere', 'Autorizație de licență comercială', 'Funcții Exclusive pentru Afaceri', 'Multiple spații de lucru și gestionarea întreprinderii', 'Autentificare unică', 'SLA-uri negociate de partenerii Dify', 'Securitate și Control Avansate', 'Actualizări și întreținere de către Dify Oficial', 'Asistență Tehnică Profesională'],
features: ['Soluții de implementare scalabile la nivel de întreprindere', 'Autorizație de licență comercială', 'Funcții Exclusive pentru Afaceri', 'Mai multe spații de lucru și managementul întreprinderii', 'Autentificare unică', 'SLA-uri negociate de partenerii Dify', 'Securitate și Control Avansate', 'Actualizări și întreținere de către Dify Oficial', 'Asistență Tehnică Profesională'],
for: 'Pentru echipe de mari dimensiuni',
price: 'Personalizat',
priceTip: 'Facturare anuală doar',
btnText: 'Contactați Vânzări',
},
community: {
features: ['Toate Funcțiile Principale Lansate în Repositorul Public', 'Spațiu de lucru unic', 'Respectă Licența Open Source Dify'],
features: ['Toate Funcționalitățile de Bază Lansate în Repositorul Public', 'Spațiu de lucru unic', 'Respectă Licența Open Source Dify'],
description: 'Pentru utilizatori individuali, echipe mici sau proiecte necomerciale',
btnText: 'Începe cu Comunitatea',
price: 'Gratuit',
@ -153,7 +153,7 @@ const translation = {
includesTitle: 'Funcții gratuite:',
},
premium: {
features: ['Fiabilitate autogestionată de diferiți furnizori de cloud', 'Spațiu de lucru unic', 'Personalizare Logo și Branding pentru WebApp', 'Asistență prioritară prin email și chat'],
features: ['Fiabilitate autogestionată de diferiți furnizori de cloud', 'Spațiu de lucru unic', 'Personalizare logo și branding pentru aplicația web', 'Asistență prioritară prin e-mail și chat'],
btnText: 'Obține Premium în',
description: 'Pentru organizații și echipe de dimensiuni medii',
includesTitle: 'Totul din Comunitate, plus:',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Facturare și abonamente',
viewBillingDescription: 'Gestionează metodele de plată, facturile și modificările abonamentului',
viewBillingAction: 'Gestiona',
upgrade: {
uploadMultiplePages: {
title: 'Actualizează pentru a încărca mai multe documente odată',
description: 'Ați atins limita de încărcare — poate fi selectat și încărcat doar un singur document odată în planul dvs. actual.',
},
uploadMultipleFiles: {
title: 'Fă upgrade pentru a debloca încărcarea documentelor în masă',
description: 'Încărcați mai multe documente simultan pentru a economisi timp și a îmbunătăți eficiența.',
},
addChunks: {
title: 'Actualizează pentru a continua să adaugi segmente',
description: 'Ai atins limita de adăugare a segmentelor pentru acest plan.',
},
},
}
export default translation

View File

@ -137,14 +137,14 @@ const translation = {
name: 'Корпоративный',
description: 'Получите полный набор возможностей и поддержку для крупномасштабных критически важных систем.',
includesTitle: 'Все в командном плане, плюс:',
features: ['Масштабируемые решения для развертывания корпоративного уровня', 'Разрешение на коммерческую лицензию', 'Эксклюзивные корпоративные функции', 'Несколько рабочих пространств и корпоративное управление', 'Единый вход (SSO)', 'Договоренные SLA с партнёрами Dify', 'Расширенные функции безопасности и управления', 'Обновления и обслуживание от Dify официально', 'Профессиональная техническая поддержка'],
features: ['Масштабируемые решения для развертывания корпоративного уровня', 'Разрешение на коммерческую лицензию', 'Эксклюзивные корпоративные функции', 'Несколько рабочих пространств и корпоративное управление', 'Единый вход (SSO)', 'Согласованные SLA с партнёрами Dify', 'Расширенные функции безопасности и управления', 'Обновления и обслуживание от Dify официально', 'Профессиональная техническая поддержка'],
price: 'Пользовательский',
priceTip: 'Только годовая подписка',
for: 'Для команд большого размера',
btnText: 'Связаться с отделом продаж',
},
community: {
features: ['Все основные функции выпущены в публичный репозиторий', 'Одиночное рабочее пространство', 'Соответствует лицензии с открытым исходным кодом Dify'],
features: ['Все основные функции выпущены в публичный репозиторий', 'Единое рабочее пространство', 'Соответствует лицензии с открытым исходным кодом Dify'],
name: 'Сообщество',
btnText: 'Начните с сообщества',
price: 'Свободно',
@ -153,7 +153,7 @@ const translation = {
for: 'Для отдельных пользователей, малых команд или некоммерческих проектов',
},
premium: {
features: ['Самоуправляемая надежность у различных облачных провайдеров', 'Одиночное рабочее пространство', 'Настройка логотипа и брендинга веб-приложения', 'Приоритетная поддержка по электронной почте и в чате'],
features: ['Самоуправляемая надежность у различных облачных провайдеров', 'Единое рабочее пространство', 'Настройка логотипа и брендинга веб-приложения', 'Приоритетная поддержка по электронной почте и в чате'],
description: 'Для средних организаций и команд',
includesTitle: 'Всё из Сообщества, плюс:',
priceTip: 'На основе облачного маркетплейса',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Платежи и подписки',
viewBillingDescription: 'Управляйте способами оплаты, счетами и изменениями подписки',
viewBillingAction: 'Управлять',
upgrade: {
uploadMultiplePages: {
title: 'Обновите версию, чтобы загружать несколько документов одновременно',
description: 'Вы достигли лимита загрузки — на вашем текущем тарифном плане можно выбрать и загрузить только один документ за раз.',
},
uploadMultipleFiles: {
title: 'Обновите версию, чтобы включить массовую загрузку документов',
description: 'Загружайте больше документов одновременно, чтобы сэкономить время и повысить эффективность.',
},
addChunks: {
title: 'Обновите версию, чтобы продолжить добавление блоков',
description: 'Вы достигли предела добавления чанков по этому тарифному плану.',
},
},
}
export default translation

View File

@ -137,14 +137,14 @@ const translation = {
name: 'Podjetje',
description: 'Pridobite vse zmogljivosti in podporo za velike sisteme kritične za misijo.',
includesTitle: 'Vse v načrtu Ekipa, plus:',
features: ['Razširljive rešitve za uvajanje na ravni podjetja', 'Pooblastilo za komercialno licenco', 'Ekskluzivne funkcije za podjetja', 'Več delovnih prostorov in upravljanje podjetja', 'SSO', 'Pogajani SLA-ji s strani partnerjev Dify', 'Napredna varnost in nadzor', 'Posodobitve in vzdrževanje uradno s strani Dify', 'Strokovna tehnična podpora'],
features: ['Razširljive rešitve za uvajanje na ravni podjetja', 'Pooblastilo za komercialno licenco', 'Ekskluzivne funkcije za podjetja', 'Več delovnih prostorov in upravljanje podjetja', 'SSO', 'Pogajani SLA-ji s strani partnerjev Dify', 'Napredna varnost in nadzor', 'Posodobitve in vzdrževanje s strani Dify uradno', 'Strokovna tehnična podpora'],
priceTip: 'Letno zaračunavanje samo',
price: 'Po meri',
btnText: 'Kontaktirajte prodajo',
for: 'Za velike ekipe',
},
community: {
features: ['Vse osnovne funkcije so izdane v javnem repozitoriju', 'Enotno delovno okolje', 'V skladu z Dify licenco odprte kode'],
features: ['Vse osnovne funkcije so izdane v javni repozitorij', 'Enotno delovno okolje', 'V skladu z Dify licenco odprte kode'],
includesTitle: 'Brezplačne funkcije:',
price: 'Brezplačno',
name: 'Skupnost',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Fakturiranje in naročnine',
viewBillingDescription: 'Upravljajte načine plačila, račune in spremembe naročnin',
viewBillingAction: 'Upravljaj',
upgrade: {
uploadMultiplePages: {
title: 'Nadgradite za nalaganje več dokumentov hkrati',
description: 'Dosegli ste omejitev nalaganja — na vašem trenutnem načrtu je mogoče izbrati in naložiti le en dokument naenkrat.',
},
uploadMultipleFiles: {
title: 'Nadgradite za odklep nalaganja dokumentov v skupkih',
description: 'Naložite več dokumentov hkrati, da prihranite čas in izboljšate učinkovitost.',
},
addChunks: {
title: 'Nadgradite, da nadaljujete z dodajanjem delov',
description: 'Dosegli ste omejitev dodajanja delov za ta načrt.',
},
},
}
export default translation

View File

@ -137,14 +137,14 @@ const translation = {
name: 'กิจการ',
description: 'รับความสามารถและการสนับสนุนเต็มรูปแบบสําหรับระบบที่สําคัญต่อภารกิจขนาดใหญ่',
includesTitle: 'ทุกอย่างในแผนทีม รวมถึง:',
features: ['โซลูชันการปรับใช้ที่ปรับขนาดได้สำหรับองค์กร', 'การอนุญาตใบอนุญาตเชิงพาณิชย์', 'ฟีเจอร์สำหรับองค์กรแบบพิเศษ', 'หลายพื้นที่ทำงานและการจัดการองค์กร', 'ระบบลงชื่อเพียงครั้งเดียว', 'ข้อตกลงระดับการให้บริการที่เจรจาโดยพันธมิตร Dify', 'ระบบความปลอดภัยและการควบคุมขั้นสูง', 'การอัปเดตและการบำรุงรักษาโดย Dify อย่างเป็นทางการ', 'การสนับสนุนทางเทคนิคระดับมืออาชีพ'],
features: ['โซลูชันการปรับใช้ที่ปรับขนาดได้สำหรับองค์กร', 'การอนุญาตใบอนุญาตเชิงพาณิชย์', 'ฟีเจอร์เฉพาะสำหรับองค์กร', 'หลายพื้นที่ทำงานและการจัดการองค์กร', 'ประกันสังคม', 'ข้อตกลง SLA ที่เจรจากับพันธมิตร Dify', 'ระบบความปลอดภัยและการควบคุมขั้นสูง', 'การอัปเดตและการบำรุงรักษาโดย Dify อย่างเป็นทางการ', 'การสนับสนุนทางเทคนิคระดับมืออาชีพ'],
btnText: 'ติดต่อฝ่ายขาย',
price: 'ที่กำหนดเอง',
for: 'สำหรับทีมขนาดใหญ่',
priceTip: 'การเรียกเก็บเงินประจำปีเท่านั้น',
},
community: {
features: ['คุณลักษณะหลักทั้งหมดถูกปล่อยภายใต้ที่เก็บสาธารณะ', 'พื้นที่ทำงานเดียว', 'เป็นไปตามใบอนุญาตแบบเปิดของ Dify'],
features: ['คุณลักษณะหลักทั้งหมดถูกปล่อยภายใต้ที่เก็บสาธารณะ', 'พื้นที่ทำงานเดียว', 'เป็นไปตามใบอนุญาตแบบโอเพ่นซอร์สของ Dify'],
name: 'ชุมชน',
price: 'ฟรี',
includesTitle: 'คุณสมบัติเสรี:',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'การเรียกเก็บเงินและการสมัครสมาชิก',
viewBillingDescription: 'จัดการวิธีการชำระเงิน ใบแจ้งหนี้ และการเปลี่ยนแปลงการสมัครสมาชิก',
viewBillingAction: 'จัดการ',
upgrade: {
uploadMultiplePages: {
title: 'อัปเกรดเพื่ออัปโหลดเอกสารหลายฉบับพร้อมกัน',
description: 'คุณได้ถึงขีดจำกัดการอัปโหลดแล้ว — สามารถเลือกและอัปโหลดเอกสารได้เพียงไฟล์เดียวต่อครั้งในแผนปัจจุบันของคุณ',
},
uploadMultipleFiles: {
title: 'อัปเกรดเพื่อปลดล็อกการอัปโหลดเอกสารเป็นชุด',
description: 'อัปโหลดเอกสารหลายชิ้นพร้อมกันในครั้งเดียวเพื่อประหยัดเวลาและเพิ่มประสิทธิภาพ',
},
addChunks: {
title: 'อัปเกรดเพื่อเพิ่มชิ้นส่วนต่อ',
description: 'คุณได้ถึงขีดจำกัดในการเพิ่มชิ้นส่วนสำหรับแผนนี้แล้ว',
},
},
}
export default translation

View File

@ -144,7 +144,7 @@ const translation = {
price: 'Özel',
},
community: {
features: ['Tüm Temel Özellikler Açık Kaynak Depoda Yayınlandı', 'Tek Çalışma Alanı', 'Dify Açık Kaynak Lisansına uygundur'],
features: ['Tüm Temel Özellikler Açık Depoda Yayınlandı', 'Tek Çalışma Alanı', 'Dify Açık Kaynak Lisansına uygundur'],
price: 'Ücretsiz',
includesTitle: 'Ücretsiz Özellikler:',
name: 'Topluluk',
@ -153,7 +153,7 @@ const translation = {
description: 'Bireysel Kullanıcılar, Küçük Ekipler veya Ticari Olmayan Projeler İçin',
},
premium: {
features: ['Çeşitli Bulut Sağlayıcıları Tarafından Kendi Kendine Yönetilen Güvenilirlik', 'Tek Çalışma Alanı', 'Web Uygulaması Logo ve Marka Özelleştirme', 'Öncelikli E-posta ve Sohbet Desteği'],
features: ['Çeşitli Bulut Sağlayıcıları Tarafından Kendi Kendine Yönetilen Güvenilirlik', 'Tek Çalışma Alanı', 'Web Uygulama Logo ve Marka Özelleştirme', 'Öncelikli E-posta ve Sohbet Desteği'],
name: 'Premium',
includesTitle: 'Topluluktan her şey, artı:',
for: 'Orta Büyüklükteki Organizasyonlar ve Ekipler için',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Faturalama ve Abonelikler',
viewBillingDescription: 'Ödeme yöntemlerini, faturaları ve abonelik değişikliklerini yönetin',
viewBillingAction: 'Yönet',
upgrade: {
uploadMultiplePages: {
title: 'Aynı anda birden fazla belge yüklemek için yükseltin',
description: 'Yükleme sınırına ulaştınız — mevcut planınızda aynı anda yalnızca bir belge seçip yükleyebilirsiniz.',
},
uploadMultipleFiles: {
title: 'Toplu belge yüklemeyi açmak için yükseltin',
description: 'Zaman kazanmak ve verimliliği artırmak için bir kerede daha fazla belgeyi toplu olarak yükleyin.',
},
addChunks: {
title: 'Parçalar eklemeye devam etmek için yükseltin',
description: 'Bu plan için parça ekleme sınırına ulaştınız.',
},
},
}
export default translation

View File

@ -137,7 +137,7 @@ const translation = {
name: 'Ентерпрайз',
description: 'Отримайте повні можливості та підтримку для масштабних критично важливих систем.',
includesTitle: 'Все, що входить до плану Team, плюс:',
features: ['Масштабовані рішення для розгортання корпоративного рівня', 'Авторизація комерційної ліцензії', 'Ексклюзивні корпоративні функції', 'Кілька робочих просторів та управління підприємством', 'ЄДИНА СИСТЕМА АВТОРИЗАЦІЇ', 'Узгоджені SLA партнерами Dify', 'Розширена безпека та контроль', 'Оновлення та обслуговування від Dify офіційно', 'Професійна технічна підтримка'],
features: ['Масштабовані рішення для розгортання корпоративного рівня', 'Авторизація комерційної ліцензії', 'Ексклюзивні корпоративні функції', 'Кілька робочих просторів та управління підприємством', 'ЄД', 'Узгоджені SLA партнерами Dify', 'Розширена безпека та контроль', 'Оновлення та обслуговування від Dify офіційно', 'Професійна технічна підтримка'],
btnText: 'Зв\'язатися з відділом продажу',
priceTip: 'Тільки річна оплата',
for: 'Для великих команд',
@ -153,7 +153,7 @@ const translation = {
name: 'Спільнота',
},
premium: {
features: ['Самокерована надійність від різних хмарних постачальників', 'Один робочий простір', 'Налаштування логотипу та бренду WebApp', 'Пріоритетна підтримка електронної пошти та чату'],
features: ['Самокерована надійність від різних хмарних постачальників', 'Один робочий простір', 'Налаштування логотипу та бренду веб-додатку', 'Пріоритетна підтримка електронної пошти та чату'],
description: 'Для середніх підприємств та команд',
btnText: 'Отримайте Преміум у',
price: 'Масштабований',
@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Білінг та підписки',
viewBillingDescription: 'Керуйте способами оплати, рахунками та змінами підписки',
viewBillingAction: 'Керувати',
upgrade: {
uploadMultiplePages: {
title: 'Оновіть, щоб завантажувати кілька документів одночасно',
description: 'Ви досягли ліміту завантаження — на вашому поточному плані можна вибрати та завантажити лише один документ одночасно.',
},
uploadMultipleFiles: {
title: 'Оновіть, щоб розблокувати пакетне завантаження документів',
description: 'Завантажуйте кілька документів одночасно, щоб заощадити час і підвищити ефективність.',
},
addChunks: {
title: 'Оновіть, щоб продовжити додавати частини',
description: 'Ви досягли межі додавання фрагментів для цього плану.',
},
},
}
export default translation

View File

@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: 'Thanh toán và Đăng ký',
viewBillingDescription: 'Quản lý phương thức thanh toán, hóa đơn và thay đổi đăng ký',
viewBillingAction: 'Quản lý',
upgrade: {
uploadMultiplePages: {
title: 'Nâng cấp để tải lên nhiều tài liệu cùng lúc',
description: 'Bạn đã đạt đến giới hạn tải lên — chỉ có thể chọn và tải lên một tài liệu trong một lần với gói hiện tại của bạn.',
},
uploadMultipleFiles: {
title: 'Nâng cấp để mở khóa tải lên nhiều tài liệu',
description: 'Tải lên nhiều tài liệu cùng lúc để tiết kiệm thời gian và nâng cao hiệu quả.',
},
addChunks: {
title: 'Nâng cấp để tiếp tục thêm các phần',
description: 'Bạn đã đạt đến giới hạn thêm phần cho gói này.',
},
},
}
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

View File

@ -202,6 +202,20 @@ const translation = {
viewBillingTitle: '帳單與訂閱',
viewBillingDescription: '管理付款方式、發票和訂閱變更',
viewBillingAction: '管理',
upgrade: {
uploadMultiplePages: {
title: '升級以一次上傳多個文件',
description: '您已達到上傳限制 — 在您目前的方案下,每次只能選擇並上傳一個文件。',
},
uploadMultipleFiles: {
title: '升級以解鎖批量上傳文件功能',
description: '一次批量上傳更多文件,以節省時間並提高效率。',
},
addChunks: {
title: '升級以繼續添加區塊',
description: '您已達到此方案可新增區塊的上限。',
},
},
}
export default translation