mirror of
https://github.com/langgenius/dify.git
synced 2026-01-22 04:55:36 +08:00
Compare commits
8 Commits
refactor/m
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 211c57f7b6 | |||
| 524ce14a68 | |||
| 1813b65acb | |||
| 6452c5a7ac | |||
| 1d778d532a | |||
| aa68966b55 | |||
| 117b6c65e4 | |||
| 061feebd87 |
@ -1,5 +1,6 @@
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Generator, Iterable
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
@ -36,6 +37,8 @@ from extensions.ext_database import db
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import Message, MessageFile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolEngine:
|
||||
"""
|
||||
@ -123,25 +126,31 @@ class ToolEngine:
|
||||
# transform tool invoke message to get LLM friendly message
|
||||
return plain_text, message_files, meta
|
||||
except ToolProviderCredentialValidationError as e:
|
||||
logger.error(e, exc_info=True)
|
||||
error_response = "Please check your tool provider credentials"
|
||||
agent_tool_callback.on_tool_error(e)
|
||||
except (ToolNotFoundError, ToolNotSupportedError, ToolProviderNotFoundError) as e:
|
||||
error_response = f"there is not a tool named {tool.entity.identity.name}"
|
||||
logger.error(e, exc_info=True)
|
||||
agent_tool_callback.on_tool_error(e)
|
||||
except ToolParameterValidationError as e:
|
||||
error_response = f"tool parameters validation error: {e}, please check your tool parameters"
|
||||
agent_tool_callback.on_tool_error(e)
|
||||
logger.error(e, exc_info=True)
|
||||
except ToolInvokeError as e:
|
||||
error_response = f"tool invoke error: {e}"
|
||||
agent_tool_callback.on_tool_error(e)
|
||||
logger.error(e, exc_info=True)
|
||||
except ToolEngineInvokeError as e:
|
||||
meta = e.meta
|
||||
error_response = f"tool invoke error: {meta.error}"
|
||||
agent_tool_callback.on_tool_error(e)
|
||||
logger.error(e, exc_info=True)
|
||||
return error_response, [], meta
|
||||
except Exception as e:
|
||||
error_response = f"unknown error: {e}"
|
||||
agent_tool_callback.on_tool_error(e)
|
||||
logger.error(e, exc_info=True)
|
||||
|
||||
return error_response, [], ToolInvokeMeta.error_instance(error_response)
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ from core.tools.entities.tool_entities import (
|
||||
)
|
||||
from core.tools.errors import ToolInvokeError
|
||||
from factories.file_factory import build_from_mapping
|
||||
from libs.login import current_user
|
||||
from models import Account, Tenant
|
||||
from models.model import App, EndUser
|
||||
from models.workflow import Workflow
|
||||
@ -28,21 +27,6 @@ from models.workflow import Workflow
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _try_resolve_user_from_request() -> Account | EndUser | None:
|
||||
"""
|
||||
Try to resolve user from Flask request context.
|
||||
|
||||
Returns None if not in a request context or if user is not available.
|
||||
"""
|
||||
# Note: `current_user` is a LocalProxy. Never compare it with None directly.
|
||||
# Use _get_current_object() to dereference the proxy
|
||||
user = getattr(current_user, "_get_current_object", lambda: current_user)()
|
||||
# Check if we got a valid user object
|
||||
if user is not None and hasattr(user, "id"):
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
class WorkflowTool(Tool):
|
||||
"""
|
||||
Workflow tool.
|
||||
@ -223,12 +207,6 @@ class WorkflowTool(Tool):
|
||||
Returns:
|
||||
Account | EndUser | None: The resolved user object, or None if resolution fails.
|
||||
"""
|
||||
# Try to resolve user from request context first
|
||||
user = _try_resolve_user_from_request()
|
||||
if user is not None:
|
||||
return user
|
||||
|
||||
# Fall back to database resolution
|
||||
return self._resolve_user_from_database(user_id=user_id)
|
||||
|
||||
def _resolve_user_from_database(self, user_id: str) -> Account | EndUser | None:
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
"""make message annotation question not nullable
|
||||
|
||||
Revision ID: 9e6fa5cbcd80
|
||||
Revises: 03f8dcbc611e
|
||||
Create Date: 2025-11-06 16:03:54.549378
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9e6fa5cbcd80'
|
||||
down_revision = '288345cd01d1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
message_annotations = sa.table(
|
||||
"message_annotations",
|
||||
sa.column("id", sa.String),
|
||||
sa.column("message_id", sa.String),
|
||||
sa.column("question", sa.Text),
|
||||
)
|
||||
messages = sa.table(
|
||||
"messages",
|
||||
sa.column("id", sa.String),
|
||||
sa.column("query", sa.Text),
|
||||
)
|
||||
update_question_from_message = (
|
||||
sa.update(message_annotations)
|
||||
.where(
|
||||
sa.and_(
|
||||
message_annotations.c.question.is_(None),
|
||||
message_annotations.c.message_id.isnot(None),
|
||||
)
|
||||
)
|
||||
.values(
|
||||
question=sa.select(sa.func.coalesce(messages.c.query, ""))
|
||||
.where(messages.c.id == message_annotations.c.message_id)
|
||||
.scalar_subquery()
|
||||
)
|
||||
)
|
||||
bind.execute(update_question_from_message)
|
||||
|
||||
fill_remaining_questions = (
|
||||
sa.update(message_annotations)
|
||||
.where(message_annotations.c.question.is_(None))
|
||||
.values(question="")
|
||||
)
|
||||
bind.execute(fill_remaining_questions)
|
||||
with op.batch_alter_table('message_annotations', schema=None) as batch_op:
|
||||
batch_op.alter_column('question', existing_type=sa.TEXT(), nullable=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('message_annotations', schema=None) as batch_op:
|
||||
batch_op.alter_column('question', existing_type=sa.TEXT(), nullable=True)
|
||||
@ -1423,7 +1423,7 @@ class MessageAnnotation(Base):
|
||||
app_id: Mapped[str] = mapped_column(StringUUID)
|
||||
conversation_id: Mapped[str | None] = mapped_column(StringUUID, sa.ForeignKey("conversations.id"))
|
||||
message_id: Mapped[str | None] = mapped_column(StringUUID)
|
||||
question: Mapped[str | None] = mapped_column(LongText, nullable=True)
|
||||
question: Mapped[str] = mapped_column(LongText, nullable=False)
|
||||
content: Mapped[str] = mapped_column(LongText, nullable=False)
|
||||
hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"))
|
||||
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
|
||||
@ -209,8 +209,12 @@ class AppAnnotationService:
|
||||
if not app:
|
||||
raise NotFound("App not found")
|
||||
|
||||
question = args.get("question")
|
||||
if question is None:
|
||||
raise ValueError("'question' is required")
|
||||
|
||||
annotation = MessageAnnotation(
|
||||
app_id=app.id, content=args["answer"], question=args["question"], account_id=current_user.id
|
||||
app_id=app.id, content=args["answer"], question=question, account_id=current_user.id
|
||||
)
|
||||
db.session.add(annotation)
|
||||
db.session.commit()
|
||||
@ -219,7 +223,7 @@ class AppAnnotationService:
|
||||
if annotation_setting:
|
||||
add_annotation_to_index_task.delay(
|
||||
annotation.id,
|
||||
args["question"],
|
||||
question,
|
||||
current_tenant_id,
|
||||
app_id,
|
||||
annotation_setting.collection_binding_id,
|
||||
@ -244,8 +248,12 @@ class AppAnnotationService:
|
||||
if not annotation:
|
||||
raise NotFound("Annotation not found")
|
||||
|
||||
question = args.get("question")
|
||||
if question is None:
|
||||
raise ValueError("'question' is required")
|
||||
|
||||
annotation.content = args["answer"]
|
||||
annotation.question = args["question"]
|
||||
annotation.question = question
|
||||
|
||||
db.session.commit()
|
||||
# if annotation reply is enabled , add annotation to index
|
||||
|
||||
@ -220,6 +220,23 @@ class TestAnnotationService:
|
||||
# Note: In this test, no annotation setting exists, so task should not be called
|
||||
mock_external_service_dependencies["add_task"].delay.assert_not_called()
|
||||
|
||||
def test_insert_app_annotation_directly_requires_question(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Question must be provided when inserting annotations directly.
|
||||
"""
|
||||
fake = Faker()
|
||||
app, _ = self._create_test_app_and_account(db_session_with_containers, mock_external_service_dependencies)
|
||||
|
||||
annotation_args = {
|
||||
"question": None,
|
||||
"answer": fake.text(max_nb_chars=200),
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
AppAnnotationService.insert_app_annotation_directly(annotation_args, app.id)
|
||||
|
||||
def test_insert_app_annotation_directly_app_not_found(
|
||||
self, db_session_with_containers, mock_external_service_dependencies
|
||||
):
|
||||
|
||||
@ -138,7 +138,7 @@ This will help you determine the testing strategy. See [web/testing/testing.md](
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit <https://docs.dify.ai/getting-started/readme> to view the full documentation.
|
||||
Visit <https://docs.dify.ai> to view the full documentation.
|
||||
|
||||
## Community
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||
import type { App } from '@/types/app'
|
||||
import type { I18nKeysByPrefix } from '@/types/i18n'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
@ -17,7 +16,6 @@ import { ToastContext } from '@/app/components/base/toast'
|
||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||
import { isTriggerNode } from '@/app/components/workflow/types'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import {
|
||||
fetchAppDetail,
|
||||
updateAppSiteAccessToken,
|
||||
@ -36,7 +34,6 @@ export type ICardViewProps = {
|
||||
|
||||
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
@ -59,25 +56,13 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
||||
const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false
|
||||
const disableAppCards = !shouldRenderAppCards
|
||||
|
||||
const triggerDocUrl = docLink('/guides/workflow/node/start')
|
||||
const buildTriggerModeMessage = useCallback((featureName: string) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xs text-text-secondary">
|
||||
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
|
||||
</div>
|
||||
<a
|
||||
href={triggerDocUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block cursor-pointer text-xs font-medium text-text-accent hover:underline"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
|
||||
</a>
|
||||
</div>
|
||||
), [t, triggerDocUrl])
|
||||
), [t])
|
||||
|
||||
const disableWebAppTooltip = disableAppCards
|
||||
? buildTriggerModeMessage(t('overview.appInfo.title', { ns: 'appOverview' }))
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import HistoryPanel from './history-panel'
|
||||
|
||||
const mockDocLink = vi.fn(() => 'doc-link')
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => mockDocLink,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/configuration/base/operation-btn', () => ({
|
||||
default: ({ onClick }: { onClick: () => void }) => (
|
||||
<button type="button" data-testid="edit-button" onClick={onClick}>
|
||||
@ -24,12 +18,10 @@ describe('HistoryPanel', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render warning content and link when showWarning is true', () => {
|
||||
it('should render warning content when showWarning is true', () => {
|
||||
render(<HistoryPanel showWarning onShowEditModal={vi.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', () => {
|
||||
|
||||
@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
|
||||
import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type Props = {
|
||||
showWarning: boolean
|
||||
@ -17,8 +16,6 @@ const HistoryPanel: FC<Props> = ({
|
||||
onShowEditModal,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className="mt-2"
|
||||
@ -45,14 +42,6 @@ const HistoryPanel: FC<Props> = ({
|
||||
<div className="flex justify-between rounded-b-xl bg-background-section-burn px-3 py-2 text-xs text-text-secondary">
|
||||
<div>
|
||||
{t('feature.conversationHistory.tip', { ns: 'appDebug' })}
|
||||
<a
|
||||
href={docLink('/learn-more/extended-reading/what-is-llmops', { 'zh-Hans': '/learn-more/extended-reading/prompt-engineering/README' })}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#155EEF]"
|
||||
>
|
||||
{t('feature.conversationHistory.learnMore', { ns: 'appDebug' })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
@ -237,15 +238,15 @@ describe('RetrievalSection', () => {
|
||||
retrievalConfig={retrievalConfig}
|
||||
showMultiModalTip
|
||||
onRetrievalConfigChange={vi.fn()}
|
||||
docLink={docLink}
|
||||
docLink={docLink as unknown as (path?: DocPathWithoutLang) => string}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('dataset.retrieval.semantic_search.title')).toBeInTheDocument()
|
||||
const learnMoreLink = screen.getByRole('link', { name: 'datasetSettings.form.retrievalSetting.learnMore' })
|
||||
expect(learnMoreLink).toHaveAttribute('href', 'https://docs.example/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting')
|
||||
expect(docLink).toHaveBeenCalledWith('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting')
|
||||
expect(learnMoreLink).toHaveAttribute('href', 'https://docs.example/use-dify/knowledge/create-knowledge/setting-indexing-methods')
|
||||
expect(docLink).toHaveBeenCalledWith('/use-dify/knowledge/create-knowledge/setting-indexing-methods')
|
||||
})
|
||||
|
||||
it('propagates retrieval config changes for economical indexing', async () => {
|
||||
@ -263,7 +264,7 @@ describe('RetrievalSection', () => {
|
||||
retrievalConfig={createRetrievalConfig()}
|
||||
showMultiModalTip={false}
|
||||
onRetrievalConfigChange={handleRetrievalChange}
|
||||
docLink={path => path}
|
||||
docLink={path => path || ''}
|
||||
/>,
|
||||
)
|
||||
const [topKIncrement] = screen.getAllByLabelText('increment')
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
@ -84,7 +85,7 @@ type InternalRetrievalSectionProps = CommonSectionProps & {
|
||||
retrievalConfig: RetrievalConfig
|
||||
showMultiModalTip: boolean
|
||||
onRetrievalConfigChange: (value: RetrievalConfig) => void
|
||||
docLink: (path: string) => string
|
||||
docLink: (path?: DocPathWithoutLang) => string
|
||||
}
|
||||
|
||||
const InternalRetrievalSection: FC<InternalRetrievalSectionProps> = ({
|
||||
@ -102,7 +103,7 @@ const InternalRetrievalSection: FC<InternalRetrievalSectionProps> = ({
|
||||
<div>
|
||||
<div className="system-sm-semibold text-text-secondary">{t('form.retrievalSetting.title', { ns: 'datasetSettings' })}</div>
|
||||
<div className="text-xs font-normal leading-[18px] text-text-tertiary">
|
||||
<a target="_blank" rel="noopener noreferrer" href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting')} className="text-text-accent">{t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })}</a>
|
||||
<a target="_blank" rel="noopener noreferrer" href={docLink('/use-dify/knowledge/create-knowledge/setting-indexing-methods')} className="text-text-accent">{t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })}</a>
|
||||
{t('form.retrievalSetting.description', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -240,7 +240,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
||||
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
|
||||
{t('apiBasedExtension.selector.title', { ns: 'common' })}
|
||||
<a
|
||||
href={docLink('/guides/extension/api-based-extension/README')}
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs font-normal text-text-tertiary hover:text-text-accent"
|
||||
|
||||
@ -5,7 +5,6 @@ import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLin
|
||||
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -22,7 +21,6 @@ import { ToastContext } from '@/app/components/base/toast'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { createApp } from '@/service/apps'
|
||||
@ -346,41 +344,26 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP
|
||||
|
||||
function AppPreview({ mode }: { mode: AppModeEnum }) {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const modeToPreviewInfoMap = {
|
||||
[AppModeEnum.CHAT]: {
|
||||
title: t('types.chatbot', { ns: 'app' }),
|
||||
description: t('newApp.chatbotUserDescription', { ns: 'app' }),
|
||||
link: docLink('/guides/application-orchestrate/chatbot-application'),
|
||||
},
|
||||
[AppModeEnum.ADVANCED_CHAT]: {
|
||||
title: t('types.advanced', { ns: 'app' }),
|
||||
description: t('newApp.advancedUserDescription', { ns: 'app' }),
|
||||
link: docLink('/guides/workflow/README', {
|
||||
'zh-Hans': '/guides/workflow/readme',
|
||||
'ja-JP': '/guides/workflow/concepts',
|
||||
}),
|
||||
},
|
||||
[AppModeEnum.AGENT_CHAT]: {
|
||||
title: t('types.agent', { ns: 'app' }),
|
||||
description: t('newApp.agentUserDescription', { ns: 'app' }),
|
||||
link: docLink('/guides/application-orchestrate/agent'),
|
||||
},
|
||||
[AppModeEnum.COMPLETION]: {
|
||||
title: t('newApp.completeApp', { ns: 'app' }),
|
||||
description: t('newApp.completionUserDescription', { ns: 'app' }),
|
||||
link: docLink('/guides/application-orchestrate/text-generator', {
|
||||
'zh-Hans': '/guides/application-orchestrate/readme',
|
||||
'ja-JP': '/guides/application-orchestrate/README',
|
||||
}),
|
||||
},
|
||||
[AppModeEnum.WORKFLOW]: {
|
||||
title: t('types.workflow', { ns: 'app' }),
|
||||
description: t('newApp.workflowUserDescription', { ns: 'app' }),
|
||||
link: docLink('/guides/workflow/README', {
|
||||
'zh-Hans': '/guides/workflow/readme',
|
||||
'ja-JP': '/guides/workflow/concepts',
|
||||
}),
|
||||
},
|
||||
}
|
||||
const previewInfo = modeToPreviewInfoMap[mode]
|
||||
@ -389,7 +372,6 @@ function AppPreview({ mode }: { mode: AppModeEnum }) {
|
||||
<h4 className="system-sm-semibold-uppercase text-text-secondary">{previewInfo.title}</h4>
|
||||
<div className="system-xs-regular mt-1 min-h-8 max-w-96 text-text-tertiary">
|
||||
<span>{previewInfo.description}</span>
|
||||
{previewInfo.link && <Link target="_blank" href={previewInfo.link} className="ml-1 text-text-accent">{t('newApp.learnMore', { ns: 'app' })}</Link>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -245,7 +245,7 @@ function AppCard({
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
|
||||
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
|
||||
onClick={() => window.open(docLink('/use-dify/nodes/user-input'), '_blank')}
|
||||
>
|
||||
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
|
||||
</div>
|
||||
|
||||
@ -305,7 +305,7 @@ describe('CustomizeModal', () => {
|
||||
// Assert
|
||||
expect(mockWindowOpen).toHaveBeenCalledTimes(1)
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/guides/application-publishing/developing-with-apis'),
|
||||
expect.stringContaining('/use-dify/publish/developing-with-apis'),
|
||||
'_blank',
|
||||
)
|
||||
})
|
||||
|
||||
@ -118,7 +118,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
className="mt-2"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
docLink('/guides/application-publishing/developing-with-apis'),
|
||||
docLink('/use-dify/publish/developing-with-apis'),
|
||||
'_blank',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -23,7 +23,6 @@ import Textarea from '@/app/components/base/textarea'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
@ -100,7 +99,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
const [language, setLanguage] = useState(default_language)
|
||||
const [saveLoading, setSaveLoading] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(
|
||||
@ -240,16 +238,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
</div>
|
||||
<div className="system-xs-regular mt-0.5 text-text-tertiary">
|
||||
<span>{t(`${prefixSettings}.modalTip`, { ns: 'appOverview' })}</span>
|
||||
<Link
|
||||
href={docLink('/guides/application-publishing/launch-your-webapp-quickly/README', {
|
||||
'zh-Hans': '/guides/application-publishing/launch-your-webapp-quickly/readme',
|
||||
})}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent"
|
||||
>
|
||||
{t('operation.learnMore', { ns: 'common' })}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* form body */}
|
||||
|
||||
@ -208,7 +208,7 @@ function TriggerCard({ appInfo, onToggleResult }: ITriggerCardProps) {
|
||||
{t('overview.triggerInfo.triggerStatusDescription', { ns: 'appOverview' })}
|
||||
{' '}
|
||||
<Link
|
||||
href={docLink('/guides/workflow/node/trigger')}
|
||||
href={docLink('/use-dify/nodes/trigger/overview')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent hover:underline"
|
||||
|
||||
@ -3,10 +3,8 @@ import {
|
||||
RiClipboardFill,
|
||||
RiClipboardLine,
|
||||
} from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useClipboard } from 'foxact/use-clipboard'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
@ -21,32 +19,27 @@ const prefixEmbedded = 'overview.appInfo.embedded'
|
||||
|
||||
const CopyFeedback = ({ content }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
const { copied, copy, reset } = useClipboard()
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(content)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
}, [copy, content])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
(isCopied
|
||||
(copied
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||
}
|
||||
>
|
||||
<ActionButton>
|
||||
<div
|
||||
onClick={onClickCopy}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={handleCopy}
|
||||
onMouseLeave={reset}
|
||||
>
|
||||
{isCopied && <RiClipboardFill className="h-4 w-4" />}
|
||||
{!isCopied && <RiClipboardLine className="h-4 w-4" />}
|
||||
{copied && <RiClipboardFill className="h-4 w-4" />}
|
||||
{!copied && <RiClipboardLine className="h-4 w-4" />}
|
||||
</div>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@ -57,21 +50,16 @@ export default CopyFeedback
|
||||
|
||||
export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className' | 'content'>) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
const { copied, copy, reset } = useClipboard()
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(content)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
}, [copy, content])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
(isCopied
|
||||
(copied
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||
}
|
||||
@ -81,9 +69,9 @@ export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
onClick={onClickCopy}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={`h-full w-full ${copyStyle.copyIcon} ${isCopied ? copyStyle.copied : ''
|
||||
onClick={handleCopy}
|
||||
onMouseLeave={reset}
|
||||
className={`h-full w-full ${copyStyle.copyIcon} ${copied ? copyStyle.copied : ''
|
||||
}`}
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
'use client'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useClipboard } from 'foxact/use-clipboard'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Copy,
|
||||
@ -18,29 +16,24 @@ const prefixEmbedded = 'overview.appInfo.embedded'
|
||||
|
||||
const CopyIcon = ({ content }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
const { copied, copy, reset } = useClipboard()
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(content)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
}, [copy, content])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
(isCopied
|
||||
(copied
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||
}
|
||||
>
|
||||
<div onMouseLeave={onMouseLeave}>
|
||||
{!isCopied
|
||||
<div onMouseLeave={reset}>
|
||||
{!copied
|
||||
? (
|
||||
<Copy className="mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary" onClick={onClickCopy} />
|
||||
<Copy className="mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary" onClick={handleCopy} />
|
||||
)
|
||||
: (
|
||||
<CopyCheck className="mx-1 h-3.5 w-3.5 text-text-tertiary" />
|
||||
|
||||
@ -2,7 +2,6 @@ import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import { RiCloseLine, RiInformation2Fill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AnnotationReply from '@/app/components/base/features/new-feature-panel/annotation-reply'
|
||||
|
||||
@ -18,7 +17,6 @@ import SpeechToText from '@/app/components/base/features/new-feature-panel/speec
|
||||
import TextToSpeech from '@/app/components/base/features/new-feature-panel/text-to-speech'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
@ -46,7 +44,6 @@ const NewFeaturePanel = ({
|
||||
onAutoAddPromptVariable,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
|
||||
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
|
||||
|
||||
@ -76,14 +73,6 @@ const NewFeaturePanel = ({
|
||||
</div>
|
||||
<div className="system-xs-medium p-1 text-text-primary">
|
||||
<span>{isChatMode ? t('common.fileUploadTip', { ns: 'workflow' }) : t('common.ImageUploadLegacyTip', { ns: 'workflow' })}</span>
|
||||
<a
|
||||
className="text-text-accent"
|
||||
href={docLink('/guides/workflow/bulletin')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('common.featuresDocLink', { ns: 'workflow' })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -319,7 +319,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
<div className="flex h-9 items-center justify-between">
|
||||
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
|
||||
<a
|
||||
href={docLink('/guides/extension/api-based-extension/README')}
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
|
||||
|
||||
@ -3,13 +3,8 @@ import * as React from 'react'
|
||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||
import InputWithCopy from './index'
|
||||
|
||||
// Create a mock function that we can track using vi.hoisted
|
||||
const mockCopyToClipboard = vi.hoisted(() => vi.fn(() => true))
|
||||
|
||||
// Mock the copy-to-clipboard library
|
||||
vi.mock('copy-to-clipboard', () => ({
|
||||
default: mockCopyToClipboard,
|
||||
}))
|
||||
// Mock navigator.clipboard for foxact/use-clipboard
|
||||
const mockWriteText = vi.fn(() => Promise.resolve())
|
||||
|
||||
// Mock the i18n hook with custom translations for test assertions
|
||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
@ -19,15 +14,16 @@ vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
'overview.appInfo.embedded.copied': 'Copied',
|
||||
}))
|
||||
|
||||
// Mock es-toolkit/compat debounce
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: (fn: any) => fn,
|
||||
}))
|
||||
|
||||
describe('InputWithCopy component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCopyToClipboard.mockClear()
|
||||
mockWriteText.mockClear()
|
||||
// Setup navigator.clipboard mock
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: mockWriteText,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('renders correctly with default props', () => {
|
||||
@ -55,7 +51,9 @@ describe('InputWithCopy component', () => {
|
||||
const copyButton = screen.getByRole('button')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('test value')
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('test value')
|
||||
})
|
||||
})
|
||||
|
||||
it('copies custom value when copyValue prop is provided', async () => {
|
||||
@ -65,7 +63,9 @@ describe('InputWithCopy component', () => {
|
||||
const copyButton = screen.getByRole('button')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('custom copy value')
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('custom copy value')
|
||||
})
|
||||
})
|
||||
|
||||
it('calls onCopy callback when copy button is clicked', async () => {
|
||||
@ -76,7 +76,9 @@ describe('InputWithCopy component', () => {
|
||||
const copyButton = screen.getByRole('button')
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
expect(onCopyMock).toHaveBeenCalledWith('test value')
|
||||
await waitFor(() => {
|
||||
expect(onCopyMock).toHaveBeenCalledWith('test value')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows copied state after successful copy', async () => {
|
||||
@ -115,17 +117,19 @@ describe('InputWithCopy component', () => {
|
||||
expect(input).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('handles empty value correctly', () => {
|
||||
it('handles empty value correctly', async () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<InputWithCopy value="" onChange={mockOnChange} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByDisplayValue('')
|
||||
const copyButton = screen.getByRole('button')
|
||||
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(copyButton)
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('')
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('')
|
||||
})
|
||||
})
|
||||
|
||||
it('maintains focus on input after copy', async () => {
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
'use client'
|
||||
import type { InputProps } from '../input'
|
||||
import { RiClipboardFill, RiClipboardLine } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { useClipboard } from 'foxact/use-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ActionButton from '../action-button'
|
||||
@ -30,31 +28,16 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>((
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
// Determine what value to copy
|
||||
const valueToString = typeof value === 'string' ? value : String(value || '')
|
||||
const finalCopyValue = copyValue || valueToString
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
const { copied, copy, reset } = useClipboard()
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(finalCopyValue)
|
||||
setIsCopied(true)
|
||||
onCopy?.(finalCopyValue)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, 2000)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}, [isCopied])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative w-full', wrapperClassName)}>
|
||||
@ -73,21 +56,21 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>((
|
||||
{showCopyButton && (
|
||||
<div
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2"
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseLeave={reset}
|
||||
>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
(isCopied
|
||||
(copied
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
|
||||
}
|
||||
>
|
||||
<ActionButton
|
||||
size="xs"
|
||||
onClick={onClickCopy}
|
||||
onClick={handleCopy}
|
||||
className="hover:bg-components-button-ghost-bg-hover"
|
||||
>
|
||||
{isCopied
|
||||
{copied
|
||||
? (
|
||||
<RiClipboardFill className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { ListChildComponentProps } from 'react-window'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { areEqual, FixedSizeList as List } from 'react-window'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Checkbox from '../../checkbox'
|
||||
import NotionIcon from '../../notion-icon'
|
||||
@ -31,22 +32,6 @@ type NotionPageItem = {
|
||||
depth: number
|
||||
} & DataSourceNotionPage
|
||||
|
||||
type ItemProps = {
|
||||
virtualStart: number
|
||||
virtualSize: number
|
||||
current: NotionPageItem
|
||||
onToggle: (pageId: string) => void
|
||||
checkedIds: Set<string>
|
||||
disabledCheckedIds: Set<string>
|
||||
onCheck: (pageId: string) => void
|
||||
canPreview?: boolean
|
||||
onPreview: (pageId: string) => void
|
||||
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
||||
searchValue: string
|
||||
previewPageId: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
}
|
||||
|
||||
const recursivePushInParentDescendants = (
|
||||
pagesMap: DataSourceNotionPageMap,
|
||||
listTreeMap: NotionPageTreeMap,
|
||||
@ -84,22 +69,34 @@ const recursivePushInParentDescendants = (
|
||||
}
|
||||
}
|
||||
|
||||
const ItemComponent = ({
|
||||
virtualStart,
|
||||
virtualSize,
|
||||
current,
|
||||
onToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds,
|
||||
onCheck,
|
||||
canPreview,
|
||||
onPreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId,
|
||||
pagesMap,
|
||||
}: ItemProps) => {
|
||||
const ItemComponent = ({ index, style, data }: ListChildComponentProps<{
|
||||
dataList: NotionPageItem[]
|
||||
handleToggle: (index: number) => void
|
||||
checkedIds: Set<string>
|
||||
disabledCheckedIds: Set<string>
|
||||
handleCheck: (index: number) => void
|
||||
canPreview?: boolean
|
||||
handlePreview: (index: number) => void
|
||||
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
||||
searchValue: string
|
||||
previewPageId: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
}>) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
dataList,
|
||||
handleToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId,
|
||||
pagesMap,
|
||||
} = data
|
||||
const current = dataList[index]
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
||||
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
||||
const ancestors = currentWithChildrenAndDescendants.ancestors
|
||||
@ -112,7 +109,7 @@ const ItemComponent = ({
|
||||
<div
|
||||
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
|
||||
style={{ marginLeft: current.depth * 8 }}
|
||||
onClick={() => onToggle(current.page_id)}
|
||||
onClick={() => handleToggle(index)}
|
||||
>
|
||||
{
|
||||
current.expand
|
||||
@ -135,21 +132,15 @@ const ItemComponent = ({
|
||||
return (
|
||||
<div
|
||||
className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover', previewPageId === current.page_id && 'bg-state-base-hover')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 8,
|
||||
right: 8,
|
||||
width: 'calc(100% - 16px)',
|
||||
height: virtualSize,
|
||||
transform: `translateY(${virtualStart + 8}px)`,
|
||||
}}
|
||||
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
|
||||
>
|
||||
<Checkbox
|
||||
className="mr-2 shrink-0"
|
||||
checked={checkedIds.has(current.page_id)}
|
||||
disabled={disabled}
|
||||
onCheck={() => onCheck(current.page_id)}
|
||||
onCheck={() => {
|
||||
handleCheck(index)
|
||||
}}
|
||||
/>
|
||||
{!searchValue && renderArrow()}
|
||||
<NotionIcon
|
||||
@ -169,7 +160,7 @@ const ItemComponent = ({
|
||||
className="ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
|
||||
font-medium leading-4 text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
|
||||
hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex"
|
||||
onClick={() => onPreview(current.page_id)}
|
||||
onClick={() => handlePreview(index)}
|
||||
>
|
||||
{t('dataSource.notion.selector.preview', { ns: 'common' })}
|
||||
</div>
|
||||
@ -188,7 +179,7 @@ const ItemComponent = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const Item = memo(ItemComponent)
|
||||
const Item = memo(ItemComponent, areEqual)
|
||||
|
||||
const PageSelector = ({
|
||||
value,
|
||||
@ -202,10 +193,31 @@ const PageSelector = ({
|
||||
onPreview,
|
||||
}: PageSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set())
|
||||
const [dataList, setDataList] = useState<NotionPageItem[]>([])
|
||||
const [localPreviewPageId, setLocalPreviewPageId] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
}))
|
||||
}, [list])
|
||||
|
||||
const searchDataList = list.filter((item) => {
|
||||
return item.page_name.includes(searchValue)
|
||||
}).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
})
|
||||
const currentDataList = searchValue ? searchDataList : dataList
|
||||
const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
|
||||
|
||||
const listMapWithChildrenAndDescendants = useMemo(() => {
|
||||
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
||||
const pageId = next.page_id
|
||||
@ -217,89 +229,47 @@ const PageSelector = ({
|
||||
}, {})
|
||||
}, [list, pagesMap])
|
||||
|
||||
const childrenByParent = useMemo(() => {
|
||||
const map = new Map<string | null, DataSourceNotionPage[]>()
|
||||
for (const item of list) {
|
||||
const isRoot = item.parent_id === 'root' || !pagesMap[item.parent_id]
|
||||
const parentKey = isRoot ? null : item.parent_id
|
||||
const children = map.get(parentKey) || []
|
||||
children.push(item)
|
||||
map.set(parentKey, children)
|
||||
}
|
||||
return map
|
||||
}, [list, pagesMap])
|
||||
|
||||
const dataList = useMemo(() => {
|
||||
const result: NotionPageItem[] = []
|
||||
|
||||
const buildVisibleList = (parentId: string | null, depth: number) => {
|
||||
const items = childrenByParent.get(parentId) || []
|
||||
|
||||
for (const item of items) {
|
||||
const isExpanded = expandedIds.has(item.page_id)
|
||||
result.push({
|
||||
...item,
|
||||
expand: isExpanded,
|
||||
depth,
|
||||
})
|
||||
if (isExpanded) {
|
||||
buildVisibleList(item.page_id, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildVisibleList(null, 0)
|
||||
return result
|
||||
}, [childrenByParent, expandedIds])
|
||||
|
||||
const searchDataList = useMemo(() => list.filter((item) => {
|
||||
return item.page_name.includes(searchValue)
|
||||
}).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
}), [list, searchValue])
|
||||
|
||||
const currentDataList = searchValue ? searchDataList : dataList
|
||||
const currentPreviewPageId = previewPageId === undefined ? localPreviewPageId : previewPageId
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: currentDataList.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 28,
|
||||
overscan: 5,
|
||||
getItemKey: index => currentDataList[index].page_id,
|
||||
})
|
||||
|
||||
const handleToggle = useCallback((pageId: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (prev.has(pageId)) {
|
||||
next.delete(pageId)
|
||||
const descendants = listMapWithChildrenAndDescendants[pageId]?.descendants
|
||||
if (descendants) {
|
||||
for (const descendantId of descendants)
|
||||
next.delete(descendantId)
|
||||
}
|
||||
}
|
||||
else {
|
||||
next.add(pageId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [listMapWithChildrenAndDescendants])
|
||||
|
||||
const handleCheck = useCallback((pageId: string) => {
|
||||
const handleToggle = (index: number) => {
|
||||
const current = dataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
||||
let newDataList = []
|
||||
|
||||
if (current.expand) {
|
||||
current.expand = false
|
||||
|
||||
newDataList = dataList.filter(item => !descendantsIds.includes(item.page_id))
|
||||
}
|
||||
else {
|
||||
current.expand = true
|
||||
|
||||
newDataList = [
|
||||
...dataList.slice(0, index + 1),
|
||||
...childrenIds.map(item => ({
|
||||
...pagesMap[item],
|
||||
expand: false,
|
||||
depth: listMapWithChildrenAndDescendants[item].depth,
|
||||
})),
|
||||
...dataList.slice(index + 1),
|
||||
]
|
||||
}
|
||||
setDataList(newDataList)
|
||||
}
|
||||
|
||||
const copyValue = new Set(value)
|
||||
const handleCheck = (index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
const copyValue = new Set(value)
|
||||
|
||||
if (copyValue.has(pageId)) {
|
||||
if (!searchValue) {
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
copyValue.delete(item)
|
||||
}
|
||||
|
||||
copyValue.delete(pageId)
|
||||
}
|
||||
else {
|
||||
@ -307,17 +277,22 @@ const PageSelector = ({
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
copyValue.add(item)
|
||||
}
|
||||
|
||||
copyValue.add(pageId)
|
||||
}
|
||||
|
||||
onSelect(copyValue)
|
||||
}, [listMapWithChildrenAndDescendants, onSelect, searchValue, value])
|
||||
onSelect(new Set(copyValue))
|
||||
}
|
||||
|
||||
const handlePreview = (index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
|
||||
const handlePreview = useCallback((pageId: string) => {
|
||||
setLocalPreviewPageId(pageId)
|
||||
|
||||
if (onPreview)
|
||||
onPreview(pageId)
|
||||
}, [onPreview])
|
||||
}
|
||||
|
||||
if (!currentDataList.length) {
|
||||
return (
|
||||
@ -328,41 +303,29 @@ const PageSelector = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
<List
|
||||
className="py-2"
|
||||
style={{ height: 296, width: '100%', overflow: 'auto' }}
|
||||
height={296}
|
||||
itemCount={currentDataList.length}
|
||||
itemSize={28}
|
||||
width="100%"
|
||||
itemKey={(index, data) => data.dataList[index].page_id}
|
||||
itemData={{
|
||||
dataList: currentDataList,
|
||||
handleToggle,
|
||||
checkedIds: value,
|
||||
disabledCheckedIds: disabledValue,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId: currentPreviewPageId,
|
||||
pagesMap,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const current = currentDataList[virtualRow.index]
|
||||
return (
|
||||
<Item
|
||||
key={virtualRow.key}
|
||||
virtualStart={virtualRow.start}
|
||||
virtualSize={virtualRow.size}
|
||||
current={current}
|
||||
onToggle={handleToggle}
|
||||
checkedIds={value}
|
||||
disabledCheckedIds={disabledValue}
|
||||
onCheck={handleCheck}
|
||||
canPreview={canPreview}
|
||||
onPreview={handlePreview}
|
||||
listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants}
|
||||
searchValue={searchValue}
|
||||
previewPageId={currentPreviewPageId}
|
||||
pagesMap={pagesMap}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{Item}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -190,7 +190,7 @@ describe('StepThree', () => {
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')
|
||||
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/guides/knowledge-base/integrate-knowledge-within-application')
|
||||
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/use-dify/knowledge/integrate-knowledge-within-application')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noreferrer noopener')
|
||||
})
|
||||
|
||||
@ -87,7 +87,7 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
|
||||
<div className="text-base font-semibold text-text-secondary">{t('stepThree.sideTipTitle', { ns: 'datasetCreation' })}</div>
|
||||
<div className="text-text-tertiary">{t('stepThree.sideTipContent', { ns: 'datasetCreation' })}</div>
|
||||
<a
|
||||
href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')}
|
||||
href={docLink('/use-dify/knowledge/integrate-knowledge-within-application')}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="system-sm-regular text-text-accent"
|
||||
|
||||
@ -214,7 +214,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents')}
|
||||
href={docLink('/use-dify/knowledge/create-knowledge/setting-indexing-methods')}
|
||||
className="text-text-accent"
|
||||
>
|
||||
{t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })}
|
||||
|
||||
@ -24,6 +24,11 @@ vi.mock('@/context/modal-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock i18n context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path?: string) => path ? `https://docs.dify.ai/en${path}` : 'https://docs.dify.ai/en/',
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
|
||||
@ -121,7 +121,7 @@ const DocumentsHeader: FC<DocumentsHeaderProps> = ({
|
||||
className="flex items-center text-text-accent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')}
|
||||
href={docLink('/use-dify/knowledge/integrate-knowledge-within-application')}
|
||||
>
|
||||
<span>{t('list.learnMore', { ns: 'datasetDocuments' })}</span>
|
||||
<RiExternalLinkLine className="h-3 w-3" />
|
||||
|
||||
@ -138,7 +138,7 @@ const OnlineDocuments = ({
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<Header
|
||||
docTitle="Docs"
|
||||
docLink={docLink('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')}
|
||||
docLink={docLink('/use-dify/knowledge/knowledge-pipeline/authorize-data-source')}
|
||||
onClickConfiguration={handleSetting}
|
||||
pluginName={nodeData.datasource_label}
|
||||
currentCredentialId={currentCredentialId}
|
||||
|
||||
@ -11,18 +11,21 @@ import { recursivePushInParentDescendants } from './utils'
|
||||
|
||||
// Note: react-i18next uses global mock from web/vitest.setup.ts
|
||||
|
||||
// Mock @tanstack/react-virtual useVirtualizer hook - renders items directly for testing
|
||||
vi.mock('@tanstack/react-virtual', () => ({
|
||||
useVirtualizer: ({ count, getItemKey }: { count: number, getItemKey?: (index: number) => string }) => ({
|
||||
getVirtualItems: () =>
|
||||
Array.from({ length: count }).map((_, index) => ({
|
||||
index,
|
||||
key: getItemKey ? getItemKey(index) : index,
|
||||
start: index * 28,
|
||||
size: 28,
|
||||
})),
|
||||
getTotalSize: () => count * 28,
|
||||
}),
|
||||
// Mock react-window FixedSizeList - renders items directly for testing
|
||||
vi.mock('react-window', () => ({
|
||||
FixedSizeList: ({ children: ItemComponent, itemCount, itemData, itemKey }: any) => (
|
||||
<div data-testid="virtual-list">
|
||||
{Array.from({ length: itemCount }).map((_, index) => (
|
||||
<ItemComponent
|
||||
key={itemKey?.(index, itemData) || index}
|
||||
index={index}
|
||||
style={{ top: index * 28, left: 0, right: 0, width: '100%', position: 'absolute' }}
|
||||
data={itemData}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
areEqual: (prevProps: any, nextProps: any) => prevProps === nextProps,
|
||||
}))
|
||||
|
||||
// Note: NotionIcon from @/app/components/base/ is NOT mocked - using real component per testing guidelines
|
||||
@ -116,7 +119,7 @@ describe('PageSelector', () => {
|
||||
render(<PageSelector {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render empty state when list is empty', () => {
|
||||
@ -131,7 +134,7 @@ describe('PageSelector', () => {
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Test Page')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('virtual-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render items using FixedSizeList', () => {
|
||||
@ -1163,7 +1166,7 @@ describe('PageSelector', () => {
|
||||
render(<PageSelector {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in page name', () => {
|
||||
@ -1337,7 +1340,7 @@ describe('PageSelector', () => {
|
||||
render(<PageSelector {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('virtual-list')).toBeInTheDocument()
|
||||
if (propVariation.canPreview)
|
||||
expect(screen.getByText('common.dataSource.notion.selector.preview')).toBeInTheDocument()
|
||||
else
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList as List } from 'react-window'
|
||||
import Item from './item'
|
||||
import { recursivePushInParentDescendants } from './utils'
|
||||
|
||||
@ -45,16 +45,29 @@ const PageSelector = ({
|
||||
currentCredentialId,
|
||||
}: PageSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set())
|
||||
const [dataList, setDataList] = useState<NotionPageItem[]>([])
|
||||
const [currentPreviewPageId, setCurrentPreviewPageId] = useState('')
|
||||
const prevCredentialIdRef = useRef(currentCredentialId)
|
||||
|
||||
// Reset expanded state when credential changes (render-time detection)
|
||||
if (prevCredentialIdRef.current !== currentCredentialId) {
|
||||
prevCredentialIdRef.current = currentCredentialId
|
||||
setExpandedIds(new Set())
|
||||
}
|
||||
useEffect(() => {
|
||||
setDataList(list.filter(item => item.parent_id === 'root' || !pagesMap[item.parent_id]).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
}))
|
||||
}, [currentCredentialId])
|
||||
|
||||
const searchDataList = list.filter((item) => {
|
||||
return item.page_name.includes(searchValue)
|
||||
}).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
})
|
||||
const currentDataList = searchValue ? searchDataList : dataList
|
||||
|
||||
const listMapWithChildrenAndDescendants = useMemo(() => {
|
||||
return list.reduce((prev: NotionPageTreeMap, next: DataSourceNotionPage) => {
|
||||
@ -67,86 +80,39 @@ const PageSelector = ({
|
||||
}, {})
|
||||
}, [list, pagesMap])
|
||||
|
||||
// Pre-build children index for O(1) lookup instead of O(n) filter
|
||||
const childrenByParent = useMemo(() => {
|
||||
const map = new Map<string | null, DataSourceNotionPage[]>()
|
||||
for (const item of list) {
|
||||
const isRoot = item.parent_id === 'root' || !pagesMap[item.parent_id]
|
||||
const parentKey = isRoot ? null : item.parent_id
|
||||
const children = map.get(parentKey) || []
|
||||
children.push(item)
|
||||
map.set(parentKey, children)
|
||||
const handleToggle = useCallback((index: number) => {
|
||||
const current = dataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
const descendantsIds = Array.from(currentWithChildrenAndDescendants.descendants)
|
||||
const childrenIds = Array.from(currentWithChildrenAndDescendants.children)
|
||||
let newDataList = []
|
||||
|
||||
if (current.expand) {
|
||||
current.expand = false
|
||||
|
||||
newDataList = dataList.filter(item => !descendantsIds.includes(item.page_id))
|
||||
}
|
||||
return map
|
||||
}, [list, pagesMap])
|
||||
else {
|
||||
current.expand = true
|
||||
|
||||
// Compute visible data list based on expanded state
|
||||
const dataList = useMemo(() => {
|
||||
const result: NotionPageItem[] = []
|
||||
|
||||
const buildVisibleList = (parentId: string | null, depth: number) => {
|
||||
const items = childrenByParent.get(parentId) || []
|
||||
|
||||
for (const item of items) {
|
||||
const isExpanded = expandedIds.has(item.page_id)
|
||||
result.push({
|
||||
...item,
|
||||
expand: isExpanded,
|
||||
depth,
|
||||
})
|
||||
if (isExpanded) {
|
||||
buildVisibleList(item.page_id, depth + 1)
|
||||
}
|
||||
}
|
||||
newDataList = [
|
||||
...dataList.slice(0, index + 1),
|
||||
...childrenIds.map(item => ({
|
||||
...pagesMap[item],
|
||||
expand: false,
|
||||
depth: listMapWithChildrenAndDescendants[item].depth,
|
||||
})),
|
||||
...dataList.slice(index + 1),
|
||||
]
|
||||
}
|
||||
setDataList(newDataList)
|
||||
}, [dataList, listMapWithChildrenAndDescendants, pagesMap])
|
||||
|
||||
buildVisibleList(null, 0)
|
||||
return result
|
||||
}, [childrenByParent, expandedIds])
|
||||
|
||||
const searchDataList = useMemo(() => list.filter((item) => {
|
||||
return item.page_name.includes(searchValue)
|
||||
}).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
expand: false,
|
||||
depth: 0,
|
||||
}
|
||||
}), [list, searchValue])
|
||||
|
||||
const currentDataList = searchValue ? searchDataList : dataList
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: currentDataList.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 28,
|
||||
overscan: 5,
|
||||
getItemKey: index => currentDataList[index].page_id,
|
||||
})
|
||||
|
||||
// Stable callback - no dependencies on dataList
|
||||
const handleToggle = useCallback((pageId: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (prev.has(pageId)) {
|
||||
// Collapse: remove current and all descendants
|
||||
next.delete(pageId)
|
||||
const descendants = listMapWithChildrenAndDescendants[pageId]?.descendants
|
||||
if (descendants) {
|
||||
for (const descendantId of descendants)
|
||||
next.delete(descendantId)
|
||||
}
|
||||
}
|
||||
else {
|
||||
next.add(pageId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [listMapWithChildrenAndDescendants])
|
||||
|
||||
// Stable callback - uses pageId parameter instead of index
|
||||
const handleCheck = useCallback((pageId: string) => {
|
||||
const handleCheck = useCallback((index: number) => {
|
||||
const copyValue = new Set(checkedIds)
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[pageId]
|
||||
|
||||
if (copyValue.has(pageId)) {
|
||||
@ -154,6 +120,7 @@ const PageSelector = ({
|
||||
for (const item of currentWithChildrenAndDescendants.descendants)
|
||||
copyValue.delete(item)
|
||||
}
|
||||
|
||||
copyValue.delete(pageId)
|
||||
}
|
||||
else {
|
||||
@ -171,15 +138,18 @@ const PageSelector = ({
|
||||
}
|
||||
}
|
||||
|
||||
onSelect(copyValue)
|
||||
}, [checkedIds, isMultipleChoice, listMapWithChildrenAndDescendants, onSelect, searchValue])
|
||||
onSelect(new Set(copyValue))
|
||||
}, [currentDataList, isMultipleChoice, listMapWithChildrenAndDescendants, onSelect, searchValue, checkedIds])
|
||||
|
||||
const handlePreview = useCallback((index: number) => {
|
||||
const current = currentDataList[index]
|
||||
const pageId = current.page_id
|
||||
|
||||
// Stable callback
|
||||
const handlePreview = useCallback((pageId: string) => {
|
||||
setCurrentPreviewPageId(pageId)
|
||||
|
||||
if (onPreview)
|
||||
onPreview(pageId)
|
||||
}, [onPreview])
|
||||
}, [currentDataList, onPreview])
|
||||
|
||||
if (!currentDataList.length) {
|
||||
return (
|
||||
@ -190,42 +160,30 @@ const PageSelector = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
<List
|
||||
className="py-2"
|
||||
style={{ height: 296, width: '100%', overflow: 'auto' }}
|
||||
height={296}
|
||||
itemCount={currentDataList.length}
|
||||
itemSize={28}
|
||||
width="100%"
|
||||
itemKey={(index, data) => data.dataList[index].page_id}
|
||||
itemData={{
|
||||
dataList: currentDataList,
|
||||
handleToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds: disabledValue,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId: currentPreviewPageId,
|
||||
pagesMap,
|
||||
isMultipleChoice,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const current = currentDataList[virtualRow.index]
|
||||
return (
|
||||
<Item
|
||||
key={virtualRow.key}
|
||||
virtualStart={virtualRow.start}
|
||||
virtualSize={virtualRow.size}
|
||||
current={current}
|
||||
onToggle={handleToggle}
|
||||
checkedIds={checkedIds}
|
||||
disabledCheckedIds={disabledValue}
|
||||
onCheck={handleCheck}
|
||||
canPreview={canPreview}
|
||||
onPreview={handlePreview}
|
||||
listMapWithChildrenAndDescendants={listMapWithChildrenAndDescendants}
|
||||
searchValue={searchValue}
|
||||
previewPageId={currentPreviewPageId}
|
||||
pagesMap={pagesMap}
|
||||
isMultipleChoice={isMultipleChoice}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{Item}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import type { ListChildComponentProps } from 'react-window'
|
||||
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { areEqual } from 'react-window'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import Radio from '@/app/components/base/radio/ui'
|
||||
@ -21,40 +23,36 @@ type NotionPageItem = {
|
||||
depth: number
|
||||
} & DataSourceNotionPage
|
||||
|
||||
type ItemProps = {
|
||||
virtualStart: number
|
||||
virtualSize: number
|
||||
current: NotionPageItem
|
||||
onToggle: (pageId: string) => void
|
||||
const Item = ({ index, style, data }: ListChildComponentProps<{
|
||||
dataList: NotionPageItem[]
|
||||
handleToggle: (index: number) => void
|
||||
checkedIds: Set<string>
|
||||
disabledCheckedIds: Set<string>
|
||||
onCheck: (pageId: string) => void
|
||||
handleCheck: (index: number) => void
|
||||
canPreview?: boolean
|
||||
onPreview: (pageId: string) => void
|
||||
handlePreview: (index: number) => void
|
||||
listMapWithChildrenAndDescendants: NotionPageTreeMap
|
||||
searchValue: string
|
||||
previewPageId: string
|
||||
pagesMap: DataSourceNotionPageMap
|
||||
isMultipleChoice?: boolean
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
virtualStart,
|
||||
virtualSize,
|
||||
current,
|
||||
onToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds,
|
||||
onCheck,
|
||||
canPreview,
|
||||
onPreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId,
|
||||
pagesMap,
|
||||
isMultipleChoice,
|
||||
}: ItemProps) => {
|
||||
}>) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
dataList,
|
||||
handleToggle,
|
||||
checkedIds,
|
||||
disabledCheckedIds,
|
||||
handleCheck,
|
||||
canPreview,
|
||||
handlePreview,
|
||||
listMapWithChildrenAndDescendants,
|
||||
searchValue,
|
||||
previewPageId,
|
||||
pagesMap,
|
||||
isMultipleChoice,
|
||||
} = data
|
||||
const current = dataList[index]
|
||||
const currentWithChildrenAndDescendants = listMapWithChildrenAndDescendants[current.page_id]
|
||||
const hasChild = currentWithChildrenAndDescendants.descendants.size > 0
|
||||
const ancestors = currentWithChildrenAndDescendants.ancestors
|
||||
@ -67,7 +65,7 @@ const Item = ({
|
||||
<div
|
||||
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
|
||||
style={{ marginLeft: current.depth * 8 }}
|
||||
onClick={() => onToggle(current.page_id)}
|
||||
onClick={() => handleToggle(index)}
|
||||
>
|
||||
{
|
||||
current.expand
|
||||
@ -90,15 +88,7 @@ const Item = ({
|
||||
return (
|
||||
<div
|
||||
className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover', previewPageId === current.page_id && 'bg-state-base-hover')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 8,
|
||||
right: 8,
|
||||
width: 'calc(100% - 16px)',
|
||||
height: virtualSize,
|
||||
transform: `translateY(${virtualStart + 8}px)`,
|
||||
}}
|
||||
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
|
||||
>
|
||||
{isMultipleChoice
|
||||
? (
|
||||
@ -106,7 +96,9 @@ const Item = ({
|
||||
className="mr-2 shrink-0"
|
||||
checked={checkedIds.has(current.page_id)}
|
||||
disabled={disabled}
|
||||
onCheck={() => onCheck(current.page_id)}
|
||||
onCheck={() => {
|
||||
handleCheck(index)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
@ -114,7 +106,9 @@ const Item = ({
|
||||
className="mr-2 shrink-0"
|
||||
isChecked={checkedIds.has(current.page_id)}
|
||||
disabled={disabled}
|
||||
onCheck={() => onCheck(current.page_id)}
|
||||
onCheck={() => {
|
||||
handleCheck(index)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!searchValue && renderArrow()}
|
||||
@ -135,7 +129,7 @@ const Item = ({
|
||||
className="ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
|
||||
font-medium leading-4 text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
|
||||
hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex"
|
||||
onClick={() => onPreview(current.page_id)}
|
||||
onClick={() => handlePreview(index)}
|
||||
>
|
||||
{t('dataSource.notion.selector.preview', { ns: 'common' })}
|
||||
</div>
|
||||
@ -155,4 +149,4 @@ const Item = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Item)
|
||||
export default React.memo(Item, areEqual)
|
||||
|
||||
@ -327,7 +327,7 @@ describe('OnlineDrive', () => {
|
||||
render(<OnlineDrive {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(mockDocLink).toHaveBeenCalledWith('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')
|
||||
expect(mockDocLink).toHaveBeenCalledWith('/use-dify/knowledge/knowledge-pipeline/authorize-data-source')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -196,7 +196,7 @@ const OnlineDrive = ({
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<Header
|
||||
docTitle="Docs"
|
||||
docLink={docLink('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')}
|
||||
docLink={docLink('/use-dify/knowledge/knowledge-pipeline/authorize-data-source')}
|
||||
onClickConfiguration={handleSetting}
|
||||
pluginName={nodeData.datasource_label}
|
||||
currentCredentialId={currentCredentialId}
|
||||
|
||||
@ -158,7 +158,7 @@ const WebsiteCrawl = ({
|
||||
<div className="flex flex-col">
|
||||
<Header
|
||||
docTitle="Docs"
|
||||
docLink={docLink('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')}
|
||||
docLink={docLink('/use-dify/knowledge/knowledge-pipeline/authorize-data-source')}
|
||||
onClickConfiguration={handleSetting}
|
||||
pluginName={nodeData.datasource_label}
|
||||
currentCredentialId={currentCredentialId}
|
||||
|
||||
@ -159,7 +159,7 @@ describe('Processing', () => {
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link', { name: 'datasetPipeline.addDocuments.stepThree.learnMore' })
|
||||
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/guides/knowledge-base/integrate-knowledge-within-application')
|
||||
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/use-dify/knowledge/knowledge-pipeline/authorize-data-source')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noreferrer noopener')
|
||||
})
|
||||
|
||||
@ -44,7 +44,7 @@ const Processing = ({
|
||||
<div className="system-xl-semibold text-text-secondary">{t('stepThree.sideTipTitle', { ns: 'datasetCreation' })}</div>
|
||||
<div className="system-sm-regular text-text-tertiary">{t('stepThree.sideTipContent', { ns: 'datasetCreation' })}</div>
|
||||
<a
|
||||
href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')}
|
||||
href={docLink('/use-dify/knowledge/knowledge-pipeline/authorize-data-source')}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="system-sm-regular text-text-accent"
|
||||
|
||||
@ -57,7 +57,7 @@ const Form: FC<FormProps> = React.memo(({
|
||||
</label>
|
||||
{variable === 'endpoint' && (
|
||||
<a
|
||||
href={docLink('/guides/knowledge-base/connect-external-knowledge-base') || '/'}
|
||||
href={docLink('/use-dify/knowledge/connect-external-knowledge-base') || '/'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="body-xs-regular flex items-center text-text-accent"
|
||||
|
||||
@ -63,7 +63,7 @@ describe('ExternalAPIPanel', () => {
|
||||
render(<ExternalAPIPanel {...defaultProps} />)
|
||||
const docLink = screen.getByText('dataset.externalAPIPanelDocumentation')
|
||||
expect(docLink).toBeInTheDocument()
|
||||
expect(docLink.closest('a')).toHaveAttribute('href', 'https://docs.example.com/guides/knowledge-base/connect-external-knowledge-base')
|
||||
expect(docLink.closest('a')).toHaveAttribute('href', 'https://docs.example.com/use-dify/knowledge/connect-external-knowledge-base')
|
||||
})
|
||||
|
||||
it('should render create button', () => {
|
||||
|
||||
@ -54,7 +54,7 @@ const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose }) => {
|
||||
<div className="body-xs-regular self-stretch text-text-tertiary">{t('externalAPIPanelDescription', { ns: 'dataset' })}</div>
|
||||
<a
|
||||
className="flex cursor-pointer items-center justify-center gap-1 self-stretch"
|
||||
href={docLink('/guides/knowledge-base/connect-external-knowledge-base')}
|
||||
href={docLink('/use-dify/knowledge/connect-external-knowledge-base')}
|
||||
target="_blank"
|
||||
>
|
||||
<RiBookOpenLine className="h-3 w-3 text-text-accent" />
|
||||
|
||||
@ -18,14 +18,14 @@ const InfoPanel = () => {
|
||||
</span>
|
||||
<span className="system-sm-regular text-text-tertiary">
|
||||
{t('connectDatasetIntro.content.front', { ns: 'dataset' })}
|
||||
<a className="system-sm-regular ml-1 text-text-accent" href={docLink('/guides/knowledge-base/external-knowledge-api')} target="_blank" rel="noopener noreferrer">
|
||||
<a className="system-sm-regular ml-1 text-text-accent" href={docLink('/use-dify/knowledge/external-knowledge-api')} target="_blank" rel="noopener noreferrer">
|
||||
{t('connectDatasetIntro.content.link', { ns: 'dataset' })}
|
||||
</a>
|
||||
{t('connectDatasetIntro.content.end', { ns: 'dataset' })}
|
||||
</span>
|
||||
<a
|
||||
className="system-sm-regular self-stretch text-text-accent"
|
||||
href={docLink('/guides/knowledge-base/connect-external-knowledge-base')}
|
||||
href={docLink('/use-dify/knowledge/connect-external-knowledge-base')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@ -146,7 +146,7 @@ describe('ExternalKnowledgeBaseCreate', () => {
|
||||
renderComponent()
|
||||
|
||||
const docLink = screen.getByText('dataset.connectHelper.helper4')
|
||||
expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/guides/knowledge-base/connect-external-knowledge-base')
|
||||
expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/knowledge/connect-external-knowledge-base')
|
||||
expect(docLink).toHaveAttribute('target', '_blank')
|
||||
expect(docLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
@ -61,7 +61,7 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> =
|
||||
<span>{t('connectHelper.helper1', { ns: 'dataset' })}</span>
|
||||
<span className="system-sm-medium text-text-secondary">{t('connectHelper.helper2', { ns: 'dataset' })}</span>
|
||||
<span>{t('connectHelper.helper3', { ns: 'dataset' })}</span>
|
||||
<a className="system-sm-regular self-stretch text-text-accent" href={docLink('/guides/knowledge-base/connect-external-knowledge-base')} target="_blank" rel="noopener noreferrer">
|
||||
<a className="system-sm-regular self-stretch text-text-accent" href={docLink('/use-dify/knowledge/connect-external-knowledge-base')} target="_blank" rel="noopener noreferrer">
|
||||
{t('connectHelper.helper4', { ns: 'dataset' })}
|
||||
</a>
|
||||
<span>
|
||||
|
||||
@ -96,10 +96,7 @@ const ModifyRetrievalModal: FC<Props> = ({
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/guides/knowledge-base/retrieval-test-and-citation#modify-text-retrieval-setting', {
|
||||
'zh-Hans': '/guides/knowledge-base/retrieval-test-and-citation#修改文本检索方式',
|
||||
'ja-JP': '/guides/knowledge-base/retrieval-test-and-citation',
|
||||
})}
|
||||
href={docLink('/use-dify/knowledge/create-knowledge/setting-indexing-methods')}
|
||||
className="text-text-accent"
|
||||
>
|
||||
{t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })}
|
||||
|
||||
@ -15,7 +15,7 @@ const NoLinkedAppsPanel = () => {
|
||||
<div className="my-2 text-xs text-text-tertiary">{t('datasetMenus.emptyTip', { ns: 'common' })}</div>
|
||||
<a
|
||||
className="mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent"
|
||||
href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')}
|
||||
href={docLink('/use-dify/knowledge/integrate-knowledge-within-application')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@ -281,7 +281,7 @@ const Form = () => {
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/chunking-and-cleaning-text')}
|
||||
href={docLink('/use-dify/knowledge/create-knowledge/chunking-and-cleaning-text')}
|
||||
className="text-text-accent"
|
||||
>
|
||||
{t('form.chunkStructure.learnMore', { ns: 'datasetSettings' })}
|
||||
@ -421,10 +421,7 @@ const Form = () => {
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting', {
|
||||
'zh-Hans': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#指定检索方式',
|
||||
'ja-JP': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#検索方法の指定',
|
||||
})}
|
||||
href={docLink('/use-dify/knowledge/create-knowledge/setting-indexing-methods')}
|
||||
className="text-text-accent"
|
||||
>
|
||||
{t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })}
|
||||
|
||||
@ -137,7 +137,7 @@ export default function AppSelector() {
|
||||
<MenuItem>
|
||||
<Link
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
href={docLink('/introduction')}
|
||||
href={docLink('/use-dify/getting-started/introduction')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@ -17,7 +17,7 @@ const Empty = () => {
|
||||
<div className="system-sm-medium mb-1 text-text-secondary">{t('apiBasedExtension.title', { ns: 'common' })}</div>
|
||||
<a
|
||||
className="system-xs-regular flex items-center text-text-accent"
|
||||
href={docLink('/guides/extension/api-based-extension/README')}
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@ -102,7 +102,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
|
||||
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
|
||||
{t('apiBasedExtension.modal.apiEndpoint.title', { ns: 'common' })}
|
||||
<a
|
||||
href={docLink('/user-guide/extension/api-based-extension/README#api-based-extension')}
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs font-normal text-text-accent"
|
||||
|
||||
@ -77,7 +77,7 @@ const EndpointList = ({ detail }: Props) => {
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.endpointsTip', { ns: 'plugin' })}</div>
|
||||
<a
|
||||
href={docLink('/plugins/schema-definition/endpoint')}
|
||||
href={docLink('/develop-plugin/getting-started/getting-started-dify-plugin')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@ -8,8 +8,7 @@ import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { getDocsUrl } from '@/app/components/plugins/utils'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useDebugKey } from '@/service/use-plugins'
|
||||
import KeyValueItem from '../base/key-value-item'
|
||||
|
||||
@ -17,7 +16,7 @@ const i18nPrefix = 'debugInfo'
|
||||
|
||||
const DebugInfo: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const docLink = useDocLink()
|
||||
const { data: info, isLoading } = useDebugKey()
|
||||
|
||||
// info.key likes 4580bdb7-b878-471c-a8a4-bfd760263a53 mask the middle part using *.
|
||||
@ -34,7 +33,7 @@ const DebugInfo: FC = () => {
|
||||
<>
|
||||
<div className="flex items-center gap-1 self-stretch">
|
||||
<span className="system-sm-semibold flex shrink-0 grow basis-0 flex-col items-start justify-center text-text-secondary">{t(`${i18nPrefix}.title`, { ns: 'plugin' })}</span>
|
||||
<a href={getDocsUrl(locale, '/plugins/quick-start/debug-plugin')} target="_blank" className="flex cursor-pointer items-center gap-0.5 text-text-accent-light-mode-only">
|
||||
<a href={docLink('/develop-plugin/features-and-specs/plugin-types/remote-debug-a-plugin')} target="_blank" className="flex cursor-pointer items-center gap-0.5 text-text-accent-light-mode-only">
|
||||
<span className="system-xs-medium">{t(`${i18nPrefix}.viewDocs`, { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="h-3 w-3" />
|
||||
</a>
|
||||
|
||||
@ -24,6 +24,7 @@ vi.mock('@/hooks/use-document-title', () => ({
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: () => 'en-US',
|
||||
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
|
||||
@ -15,10 +15,9 @@ import Button from '@/app/components/base/button'
|
||||
import TabSlider from '@/app/components/base/tab-slider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal'
|
||||
import { getDocsUrl } from '@/app/components/plugins/utils'
|
||||
import { MARKETPLACE_API_PREFIX, SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { usePluginInstallation } from '@/hooks/use-query-params'
|
||||
import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins'
|
||||
@ -47,7 +46,7 @@ const PluginPage = ({
|
||||
marketplace,
|
||||
}: PluginPageProps) => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const docLink = useDocLink()
|
||||
useDocumentTitle(t('metadata.title', { ns: 'plugin' }))
|
||||
|
||||
// Use nuqs hook for installation state
|
||||
@ -175,7 +174,7 @@ const PluginPage = ({
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href={getDocsUrl(locale, '/plugins/publish-plugins/publish-to-dify-marketplace/README')}
|
||||
href={docLink('/develop-plugin/publishing/marketplace-listing/release-to-dify-marketplace')}
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
|
||||
@ -2,7 +2,6 @@ import type {
|
||||
TagKey,
|
||||
} from './constants'
|
||||
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import {
|
||||
categoryKeys,
|
||||
tagKeys,
|
||||
@ -15,15 +14,3 @@ export const getValidTagKeys = (tags: TagKey[]) => {
|
||||
export const getValidCategoryKeys = (category?: string) => {
|
||||
return categoryKeys.find(key => key === category)
|
||||
}
|
||||
|
||||
export const getDocsUrl = (locale: string, path: string) => {
|
||||
let localePath = 'en'
|
||||
|
||||
if (locale === LanguagesSupported[1])
|
||||
localePath = 'zh-hans'
|
||||
|
||||
else if (locale === LanguagesSupported[7])
|
||||
localePath = 'ja-jp'
|
||||
|
||||
return `https://docs.dify.ai/${localePath}${path}`
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
} from '@/app/components/workflow/store'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
@ -55,6 +56,7 @@ const Popup = () => {
|
||||
const { t } = useTranslation()
|
||||
const { datasetId } = useParams()
|
||||
const { push } = useRouter()
|
||||
const docLink = useDocLink()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
@ -186,7 +188,7 @@ const Popup = () => {
|
||||
{t('publishTemplate.success.tip', { ns: 'datasetPipeline' })}
|
||||
</span>
|
||||
<Link
|
||||
href="https://docs.dify.ai"
|
||||
href={docLink()}
|
||||
target="_blank"
|
||||
className="system-xs-medium-uppercase inline-block text-text-accent"
|
||||
>
|
||||
|
||||
@ -6,11 +6,11 @@ import dataSourceEmptyDefault from '@/app/components/workflow/nodes/data-source-
|
||||
import dataSourceDefault from '@/app/components/workflow/nodes/data-source/default'
|
||||
import knowledgeBaseDefault from '@/app/components/workflow/nodes/knowledge-base/default'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
export const useAvailableNodesMetaData = () => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const docLink = useDocLink()
|
||||
|
||||
const mergedNodesMetaData = useMemo(() => [
|
||||
...WORKFLOW_COMMON_NODES,
|
||||
@ -25,14 +25,9 @@ export const useAvailableNodesMetaData = () => {
|
||||
dataSourceEmptyDefault,
|
||||
], [])
|
||||
|
||||
const helpLinkUri = useMemo(() => {
|
||||
if (language === 'zh_Hans')
|
||||
return 'https://docs.dify.ai/zh-hans/guides/knowledge-base/knowledge-pipeline/knowledge-pipeline-orchestration#%E6%AD%A5%E9%AA%A4%E4%B8%80%EF%BC%9A%E6%95%B0%E6%8D%AE%E6%BA%90%E9%85%8D%E7%BD%AE'
|
||||
if (language === 'ja_JP')
|
||||
return 'https://docs.dify.ai/ja-jp/guides/knowledge-base/knowledge-pipeline/knowledge-pipeline-orchestration#%E3%82%B9%E3%83%86%E3%83%83%E3%83%971%EF%BC%9A%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E8%A8%AD%E5%AE%9A'
|
||||
|
||||
return 'https://docs.dify.ai/en/guides/knowledge-base/knowledge-pipeline/knowledge-pipeline-orchestration#step-1%3A-data-source'
|
||||
}, [language])
|
||||
const helpLinkUri = useMemo(() => docLink(
|
||||
'/use-dify/knowledge/knowledge-pipeline/knowledge-pipeline-orchestration',
|
||||
), [docLink])
|
||||
|
||||
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
||||
const { metaData } = node
|
||||
|
||||
@ -8,8 +8,7 @@ import {
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useCreateMCP } from '@/service/use-tools'
|
||||
import MCPModal from './modal'
|
||||
|
||||
@ -19,8 +18,7 @@ type Props = {
|
||||
|
||||
const NewMCPCard = ({ handleCreate }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const docLink = useDocLink()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const { mutateAsync: createMCP } = useCreateMCP()
|
||||
@ -30,13 +28,7 @@ const NewMCPCard = ({ handleCreate }: Props) => {
|
||||
handleCreate(provider)
|
||||
}
|
||||
|
||||
const linkUrl = useMemo(() => {
|
||||
if (language.startsWith('zh_'))
|
||||
return 'https://docs.dify.ai/zh-hans/guides/tools/mcp'
|
||||
if (language.startsWith('ja_jp'))
|
||||
return 'https://docs.dify.ai/ja_jp/guides/tools/mcp'
|
||||
return 'https://docs.dify.ai/en/guides/tools/mcp'
|
||||
}, [language])
|
||||
const linkUrl = useMemo(() => docLink('/use-dify/build/mcp'), [docLink])
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
|
||||
@ -200,7 +200,7 @@ function MCPServiceCard({
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
|
||||
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
|
||||
onClick={() => window.open(docLink('/use-dify/nodes/user-input'), '_blank')}
|
||||
>
|
||||
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
|
||||
</div>
|
||||
|
||||
@ -2,16 +2,12 @@
|
||||
import type { CustomCollectionBackend } from '../types'
|
||||
import {
|
||||
RiAddCircleFill,
|
||||
RiArrowRightUpLine,
|
||||
RiBookOpenLine,
|
||||
} from '@remixicon/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { createCustomCollection } from '@/service/tools'
|
||||
|
||||
type Props = {
|
||||
@ -20,17 +16,8 @@ type Props = {
|
||||
|
||||
const Contribute = ({ onRefreshData }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const docLink = useDocLink()
|
||||
const linkUrl = useMemo(() => {
|
||||
return docLink('/guides/tools#how-to-create-custom-tools', {
|
||||
'zh-Hans': '/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju',
|
||||
})
|
||||
}, [language])
|
||||
|
||||
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
|
||||
const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
|
||||
await createCustomCollection(data)
|
||||
@ -54,13 +41,6 @@ const Contribute = ({ onRefreshData }: Props) => {
|
||||
<div className="system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent">{t('createCustomTool', { ns: 'tools' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent">
|
||||
<a href={linkUrl} target="_blank" rel="noopener noreferrer" className="flex items-center space-x-1">
|
||||
<RiBookOpenLine className="h-3 w-3 shrink-0" />
|
||||
<div className="system-xs-regular grow truncate" title={t('customToolTip', { ns: 'tools' }) || ''}>{t('customToolTip', { ns: 'tools' })}</div>
|
||||
<RiArrowRightUpLine className="h-3 w-3 shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isShowEditCollectionToolModal && (
|
||||
|
||||
@ -126,18 +126,6 @@ describe('WorkflowOnboardingModal', () => {
|
||||
expect(descriptionDiv).toHaveTextContent('workflow.onboarding.aboutStartNode')
|
||||
})
|
||||
|
||||
it('should render learn more link', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const learnMoreLink = screen.getByText('workflow.onboarding.learnMore')
|
||||
expect(learnMoreLink).toBeInTheDocument()
|
||||
expect(learnMoreLink.closest('a')).toHaveAttribute('href', 'https://docs.example.com/guides/workflow/node/start')
|
||||
expect(learnMoreLink.closest('a')).toHaveAttribute('target', '_blank')
|
||||
expect(learnMoreLink.closest('a')).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should render StartNodeSelectionPanel', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
@ -547,16 +535,6 @@ describe('WorkflowOnboardingModal', () => {
|
||||
expect(heading).toHaveTextContent('workflow.onboarding.title')
|
||||
})
|
||||
|
||||
it('should have external link with proper attributes', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('workflow.onboarding.learnMore').closest('a')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should have keyboard navigation support via ESC key', () => {
|
||||
// Arrange
|
||||
renderComponent({ isShow: true })
|
||||
@ -595,16 +573,6 @@ describe('WorkflowOnboardingModal', () => {
|
||||
const title = screen.getByText('workflow.onboarding.title')
|
||||
expect(title).toHaveClass('text-text-primary')
|
||||
})
|
||||
|
||||
it('should have underlined learn more link', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('workflow.onboarding.learnMore').closest('a')
|
||||
expect(link).toHaveClass('underline')
|
||||
expect(link).toHaveClass('cursor-pointer')
|
||||
})
|
||||
})
|
||||
|
||||
// Integration Tests
|
||||
@ -654,9 +622,6 @@ describe('WorkflowOnboardingModal', () => {
|
||||
const heading = container.querySelector('h3')
|
||||
expect(heading).toBeInTheDocument()
|
||||
|
||||
// Assert - Description with link
|
||||
expect(screen.getByText('workflow.onboarding.learnMore').closest('a')).toBeInTheDocument()
|
||||
|
||||
// Assert - Selection panel
|
||||
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument()
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import StartNodeSelectionPanel from './start-node-selection-panel'
|
||||
|
||||
type WorkflowOnboardingModalProps = {
|
||||
@ -23,7 +22,6 @@ const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
||||
onSelectStartNode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
const handleSelectUserInput = useCallback(() => {
|
||||
onSelectStartNode(BlockEnum.Start)
|
||||
@ -63,15 +61,6 @@ const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
||||
<div className="body-xs-regular leading-4 text-text-tertiary">
|
||||
{t('onboarding.description', { ns: 'workflow' })}
|
||||
{' '}
|
||||
<a
|
||||
href={docLink('/guides/workflow/node/start')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-text-accent-hover cursor-pointer text-text-accent underline"
|
||||
>
|
||||
{t('onboarding.learnMore', { ns: 'workflow' })}
|
||||
</a>
|
||||
{' '}
|
||||
{t('onboarding.aboutStartNode', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
|
||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||
@ -44,7 +45,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
const { metaData } = node
|
||||
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
|
||||
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
|
||||
const helpLinkPath = `guides/workflow/node/${metaData.helpLinkUri}`
|
||||
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
|
||||
return {
|
||||
...node,
|
||||
metaData: {
|
||||
|
||||
@ -251,10 +251,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
{' '}
|
||||
<br />
|
||||
<Link
|
||||
href={docLink('/guides/workflow/node/agent#select-an-agent-strategy', {
|
||||
'zh-Hans': '/guides/workflow/node/agent#选择-agent-策略',
|
||||
'ja-JP': '/guides/workflow/node/agent#エージェント戦略の選択',
|
||||
})}
|
||||
href={docLink('/use-dify/nodes/agent')}
|
||||
className="text-text-accent-secondary"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@ -5,7 +5,6 @@ import Input from '@/app/components/base/input'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type DefaultValueProps = {
|
||||
forms: DefaultValueForm[]
|
||||
@ -16,7 +15,6 @@ const DefaultValue = ({
|
||||
onFormChange,
|
||||
}: DefaultValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => {
|
||||
return (payload: any) => {
|
||||
let value
|
||||
@ -35,15 +33,6 @@ const DefaultValue = ({
|
||||
<div className="body-xs-regular mb-2 text-text-tertiary">
|
||||
{t('nodes.common.errorHandle.defaultValue.desc', { ns: 'workflow' })}
|
||||
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/README', {
|
||||
'zh-Hans': '/guides/workflow/error-handling/readme',
|
||||
})}
|
||||
target="_blank"
|
||||
className="text-text-accent"
|
||||
>
|
||||
{t('common.learnMore', { ns: 'workflow' })}
|
||||
</a>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{
|
||||
|
||||
@ -19,7 +19,7 @@ const FailBranchCard = () => {
|
||||
{t('nodes.common.errorHandle.failBranch.customizeTip', { ns: 'workflow' })}
|
||||
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/error-type')}
|
||||
href={docLink('/use-dify/debug/error-type')}
|
||||
target="_blank"
|
||||
className="text-text-accent"
|
||||
>
|
||||
|
||||
@ -6,7 +6,6 @@ import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ListEmpty from '@/app/components/base/list-empty'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import VarReferenceVars from './var-reference-vars'
|
||||
|
||||
type Props = {
|
||||
@ -31,7 +30,7 @@ const VarReferencePopup: FC<Props> = ({
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const showManageRagInputFields = useMemo(() => !!pipelineId, [pipelineId])
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
const docLink = useDocLink()
|
||||
|
||||
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
|
||||
return (
|
||||
<div
|
||||
@ -58,17 +57,6 @@ const VarReferencePopup: FC<Props> = ({
|
||||
description={(
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{t('variableReference.assignedVarsDescription', { ns: 'workflow' })}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent-secondary"
|
||||
href={docLink('/guides/workflow/variables#conversation-variables', {
|
||||
'zh-Hans': '/guides/workflow/variables#会话变量',
|
||||
'ja-JP': '/guides/workflow/variables#会話変数',
|
||||
})}
|
||||
>
|
||||
{t('variableReference.conversationVars', { ns: 'workflow' })}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -31,7 +31,7 @@ const Instruction = ({
|
||||
<div className="system-xs-regular">
|
||||
<p className="text-text-tertiary">{t('nodes.knowledgeBase.chunkStructureTip.message', { ns: 'workflow' })}</p>
|
||||
<a
|
||||
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/chunking-and-cleaning-text')}
|
||||
href={docLink('/use-dify/knowledge/create-knowledge/chunking-and-cleaning-text')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent"
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useRetrievalSetting } from './hooks'
|
||||
import SearchMethodOption from './search-method-option'
|
||||
|
||||
@ -50,6 +51,7 @@ const RetrievalSetting = ({
|
||||
showMultiModalTip,
|
||||
}: RetrievalSettingProps) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const {
|
||||
options,
|
||||
hybridSearchModeOptions,
|
||||
@ -61,7 +63,7 @@ const RetrievalSetting = ({
|
||||
title: t('form.retrievalSetting.title', { ns: 'datasetSettings' }),
|
||||
subTitle: (
|
||||
<div className="body-xs-regular flex items-center text-text-tertiary">
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings" className="text-text-accent">{t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })}</a>
|
||||
<a target="_blank" rel="noopener noreferrer" href={docLink('/use-dify/knowledge/create-knowledge/setting-indexing-methods')} className="text-text-accent">{t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })}</a>
|
||||
|
||||
{t('nodes.knowledgeBase.aboutRetrieval', { ns: 'workflow' })}
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import type { FC } from 'react'
|
||||
import type { SchemaRoot } from '../../types'
|
||||
import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { RiBracesLine, RiCloseLine, RiTimelineView } from '@remixicon/react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { SegmentedControl } from '../../../../../base/segmented-control'
|
||||
import { Type } from '../../types'
|
||||
import {
|
||||
@ -55,7 +53,6 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor)
|
||||
const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA)
|
||||
const [json, setJson] = useState(() => JSON.stringify(jsonSchema, null, 2))
|
||||
@ -253,15 +250,6 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
|
||||
</div>
|
||||
{/* Footer */}
|
||||
<div className="flex items-center gap-x-2 p-6 pt-5">
|
||||
<a
|
||||
className="flex grow items-center gap-x-1 text-text-accent"
|
||||
href={docLink('/guides/workflow/structured-outputs')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="system-xs-regular">{t('nodes.llm.jsonSchema.doc', { ns: 'workflow' })}</span>
|
||||
<RiExternalLinkLine className="h-3 w-3" />
|
||||
</a>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button variant="secondary" onClick={handleResetDefaults}>
|
||||
|
||||
@ -21,13 +21,11 @@ import VariableItem from '@/app/components/workflow/panel/chat-variable-panel/co
|
||||
import VariableModalTrigger from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal-trigger'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
|
||||
|
||||
const ChatVariablePanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const store = useStoreApi()
|
||||
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
|
||||
@ -148,17 +146,6 @@ const ChatVariablePanel = () => {
|
||||
<div className="system-2xs-medium-uppercase inline-block rounded-[5px] border border-divider-deep px-[5px] py-[3px] text-text-tertiary">TIPS</div>
|
||||
<div className="system-sm-regular mb-4 mt-1 text-text-secondary">
|
||||
{t('chatVariable.panelDescription', { ns: 'workflow' })}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent"
|
||||
href={docLink('/guides/workflow/variables#conversation-variables', {
|
||||
'zh-Hans': '/guides/workflow/variables#会话变量',
|
||||
'ja-JP': '/guides/workflow/variables#会話変数',
|
||||
})}
|
||||
>
|
||||
{t('chatVariable.docLink', { ns: 'workflow' })}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="radius-lg flex flex-col border border-workflow-block-border bg-workflow-block-bg p-3 pb-4 shadow-md">
|
||||
|
||||
@ -211,7 +211,7 @@ const NodePanel: FC<Props> = ({
|
||||
<StatusContainer status="stopped">
|
||||
{nodeInfo.error}
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/error-type')}
|
||||
href={docLink('/use-dify/debug/error-type')}
|
||||
target="_blank"
|
||||
className="text-text-accent"
|
||||
>
|
||||
|
||||
@ -139,7 +139,7 @@ const StatusPanel: FC<ResultProps> = ({
|
||||
<div className="system-xs-medium text-text-warning">
|
||||
{error}
|
||||
<a
|
||||
href={docLink('/guides/workflow/error-handling/error-type')}
|
||||
href={docLink('/use-dify/debug/error-type')}
|
||||
target="_blank"
|
||||
className="text-text-accent"
|
||||
>
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
const Empty: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-3 rounded-xl bg-background-section p-8">
|
||||
@ -15,7 +17,7 @@ const Empty: FC = () => {
|
||||
<div className="system-xs-regular text-text-tertiary">{t('debug.variableInspect.emptyTip', { ns: 'workflow' })}</div>
|
||||
<a
|
||||
className="system-xs-regular cursor-pointer text-text-accent"
|
||||
href="https://docs.dify.ai/en/guides/workflow/debug-and-preview/variable-inspect"
|
||||
href={docLink('/use-dify/debug/variable-inspect')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@ -163,7 +163,7 @@ const EducationApplyAge = () => {
|
||||
<div className="mb-4 mt-5 h-px bg-gradient-to-r from-[rgba(16,24,40,0.08)]"></div>
|
||||
<a
|
||||
className="system-xs-regular flex items-center text-text-accent"
|
||||
href={docLink('/getting-started/dify-for-education')}
|
||||
href={docLink('/use-dify/workspace/subscription-management#dify-for-education')}
|
||||
target="_blank"
|
||||
>
|
||||
{t('learn', { ns: 'education' })}
|
||||
|
||||
@ -25,7 +25,7 @@ const i18nPrefix = 'notice'
|
||||
const ExpireNoticeModal: React.FC<Props> = ({ expireAt, expired, onClose }) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const eduDocLink = docLink('/getting-started/dify-for-education')
|
||||
const eduDocLink = docLink('/use-dify/workspace/subscription-management#dify-for-education')
|
||||
const { formatTime } = useTimestamp()
|
||||
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
|
||||
const { mutateAsync } = useEducationVerify()
|
||||
|
||||
@ -34,7 +34,7 @@ function Confirm({
|
||||
const docLink = useDocLink()
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
const [isVisible, setIsVisible] = useState(isShow)
|
||||
const eduDocLink = docLink('/getting-started/dify-for-education')
|
||||
const eduDocLink = docLink('/use-dify/workspace/subscription-management#dify-for-education')
|
||||
|
||||
const handleClick = () => {
|
||||
window.open(eduDocLink, '_blank', 'noopener,noreferrer')
|
||||
|
||||
@ -14,7 +14,7 @@ import { zodSubmitValidator } from '@/app/components/base/form/utils/zod-submit-
|
||||
import Input from '@/app/components/base/input'
|
||||
import { validPassword } from '@/config'
|
||||
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { LICENSE_LINK } from '@/constants/link'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -35,7 +35,6 @@ const accountFormSchema = z.object({
|
||||
const InstallForm = () => {
|
||||
useDocumentTitle('')
|
||||
const { t, i18n } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const router = useRouter()
|
||||
const [showPassword, setShowPassword] = React.useState(false)
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
@ -219,7 +218,7 @@ const InstallForm = () => {
|
||||
className="text-text-accent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/policies/open-source')}
|
||||
href={LICENSE_LINK}
|
||||
>
|
||||
{t('license.link', { ns: 'login' })}
|
||||
</Link>
|
||||
|
||||
@ -11,8 +11,8 @@ import Input from '@/app/components/base/input'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { LICENSE_LINK } from '@/constants/link'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { languages, LanguagesSupported } from '@/i18n-config/language'
|
||||
import { activateMember } from '@/service/common'
|
||||
@ -23,7 +23,6 @@ import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
export default function InviteSettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const docLink = useDocLink()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const token = decodeURIComponent(searchParams.get('invite_token') as string)
|
||||
@ -161,7 +160,7 @@ export default function InviteSettingsPage() {
|
||||
className="system-xs-medium text-text-accent-secondary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/policies/open-source')}
|
||||
href={LICENSE_LINK}
|
||||
>
|
||||
{t('license.link', { ns: 'login' })}
|
||||
</Link>
|
||||
|
||||
@ -2,14 +2,13 @@
|
||||
import type { Reducer } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useReducer } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { LICENSE_LINK } from '@/constants/link'
|
||||
import { languages, LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useOneMoreStep } from '@/service/use-common'
|
||||
import { timezones } from '@/utils/timezone'
|
||||
@ -48,7 +47,6 @@ const reducer: Reducer<IState, IAction> = (state: IState, action: IAction) => {
|
||||
|
||||
const OneMoreStep = () => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@ -159,7 +157,7 @@ const OneMoreStep = () => {
|
||||
className="system-xs-medium text-text-accent-secondary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink('/policies/agreement/README')}
|
||||
href={LICENSE_LINK}
|
||||
>
|
||||
{t('license.link', { ns: 'login' })}
|
||||
</Link>
|
||||
|
||||
1
web/constants/link.ts
Normal file
1
web/constants/link.ts
Normal file
@ -0,0 +1 @@
|
||||
export const LICENSE_LINK = 'https://github.com/langgenius/dify?tab=License-1-ov-file#readme'
|
||||
@ -1,6 +1,8 @@
|
||||
import type { Locale } from '@/i18n-config/language'
|
||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||
import { useTranslation } from '#i18n'
|
||||
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language'
|
||||
import { apiReferencePathTranslations } from '@/types/doc-paths'
|
||||
|
||||
export const useLocale = () => {
|
||||
const { i18n } = useTranslation()
|
||||
@ -19,15 +21,24 @@ export const useGetPricingPageLanguage = () => {
|
||||
}
|
||||
|
||||
export const defaultDocBaseUrl = 'https://docs.dify.ai'
|
||||
export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => {
|
||||
export type DocPathMap = Partial<Record<Locale, DocPathWithoutLang>>
|
||||
|
||||
export const useDocLink = (baseUrl?: string): ((path?: DocPathWithoutLang, pathMap?: DocPathMap) => string) => {
|
||||
let baseDocUrl = baseUrl || defaultDocBaseUrl
|
||||
baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl
|
||||
const locale = useLocale()
|
||||
const docLanguage = getDocLanguage(locale)
|
||||
return (path?: string, pathMap?: { [index: string]: string }): string => {
|
||||
return (path?: DocPathWithoutLang, pathMap?: DocPathMap): string => {
|
||||
const pathUrl = path || ''
|
||||
let targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl
|
||||
targetPath = (targetPath.startsWith('/')) ? targetPath.slice(1) : targetPath
|
||||
return `${baseDocUrl}/${docLanguage}/${targetPath}`
|
||||
|
||||
// Translate API reference paths for non-English locales
|
||||
if (targetPath.startsWith('/api-reference/') && docLanguage !== 'en') {
|
||||
const translatedPath = apiReferencePathTranslations[targetPath]?.[docLanguage as 'zh' | 'ja']
|
||||
if (translatedPath)
|
||||
targetPath = translatedPath
|
||||
}
|
||||
|
||||
return `${baseDocUrl}/${docLanguage}${targetPath}`
|
||||
}
|
||||
}
|
||||
|
||||
@ -1131,11 +1131,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/input-with-copy/index.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/input/index.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -1309,6 +1304,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/base/notion-page-selector/page-selector/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/pagination/index.tsx": {
|
||||
"unicorn/prefer-number-properties": {
|
||||
"count": 1
|
||||
@ -1707,7 +1707,12 @@
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/index.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-documents/page-selector/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/online-drive/connect/index.spec.tsx": {
|
||||
@ -4242,6 +4247,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"middleware.ts": {
|
||||
"node/prefer-global/buffer": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"models/common.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
|
||||
@ -23,7 +23,7 @@ export default antfu(
|
||||
},
|
||||
},
|
||||
nextjs: true,
|
||||
ignores: ['public'],
|
||||
ignores: ['public', 'types/doc-paths.ts'],
|
||||
typescript: {
|
||||
overrides: {
|
||||
'ts/consistent-type-definitions': ['error', 'type'],
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
export const useDatasetApiAccessUrl = () => {
|
||||
const locale = useGetLanguage()
|
||||
const docLink = useDocLink()
|
||||
|
||||
const apiReferenceUrl = useMemo(() => {
|
||||
if (locale === 'zh_Hans')
|
||||
return 'https://docs.dify.ai/api-reference/%E6%95%B0%E6%8D%AE%E9%9B%86'
|
||||
if (locale === 'ja_JP')
|
||||
return 'https://docs.dify.ai/api-reference/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BB%E3%83%83%E3%83%88'
|
||||
return 'https://docs.dify.ai/api-reference/datasets'
|
||||
}, [locale])
|
||||
|
||||
return apiReferenceUrl
|
||||
return docLink('/api-reference/datasets/get-knowledge-base-list')
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { DocLanguage } from '@/types/doc-paths'
|
||||
import data from './languages'
|
||||
|
||||
export type Item = {
|
||||
@ -22,9 +23,9 @@ export const getLanguage = (locale: Locale): Locale => {
|
||||
return LanguagesSupported[0].replace('-', '_') as Locale
|
||||
}
|
||||
|
||||
const DOC_LANGUAGE: Record<string, string> = {
|
||||
'zh-Hans': 'zh-hans',
|
||||
'ja-JP': 'ja-jp',
|
||||
const DOC_LANGUAGE: Record<string, DocLanguage | undefined> = {
|
||||
'zh-Hans': 'zh',
|
||||
'ja-JP': 'ja',
|
||||
'en-US': 'en',
|
||||
}
|
||||
|
||||
@ -56,7 +57,7 @@ export const localeMap: Record<Locale, string> = {
|
||||
'ar-TN': 'ar',
|
||||
}
|
||||
|
||||
export const getDocLanguage = (locale: string) => {
|
||||
export const getDocLanguage = (locale: string): DocLanguage => {
|
||||
return DOC_LANGUAGE[locale] || 'en'
|
||||
}
|
||||
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "حصة",
|
||||
"modelProvider.card.quotaExhausted": "نفدت الحصة",
|
||||
"modelProvider.card.removeKey": "إزالة مفتاح API",
|
||||
"modelProvider.card.tip": "ستعطى الأولوية للحصة المدفوعة. سيتم استخدام الحصة التجريبية بعد نفاد الحصة المدفوعة.",
|
||||
"modelProvider.card.tip": "تدعم أرصدة الرسائل نماذج من OpenAI. ستعطى الأولوية للحصة المدفوعة. سيتم استخدام الحصة المجانية بعد نفاد الحصة المدفوعة.",
|
||||
"modelProvider.card.tokens": "رموز",
|
||||
"modelProvider.collapse": "طي",
|
||||
"modelProvider.config": "تكوين",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "KONTINGENT",
|
||||
"modelProvider.card.quotaExhausted": "Kontingent erschöpft",
|
||||
"modelProvider.card.removeKey": "API-Schlüssel entfernen",
|
||||
"modelProvider.card.tip": "Der bezahlten Kontingent wird Vorrang gegeben. Das Testkontingent wird nach dem Verbrauch des bezahlten Kontingents verwendet.",
|
||||
"modelProvider.card.tip": "Nachrichtenguthaben unterstützen Modelle von OpenAI. Der bezahlten Kontingent wird Vorrang gegeben. Das kostenlose Kontingent wird nach dem Verbrauch des bezahlten Kontingents verwendet.",
|
||||
"modelProvider.card.tokens": "Token",
|
||||
"modelProvider.collapse": "Einklappen",
|
||||
"modelProvider.config": "Konfigurieren",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "CUOTA",
|
||||
"modelProvider.card.quotaExhausted": "Cuota agotada",
|
||||
"modelProvider.card.removeKey": "Eliminar CLAVE API",
|
||||
"modelProvider.card.tip": "Se dará prioridad al uso de la cuota pagada. La cuota de prueba se utilizará después de que se agote la cuota pagada.",
|
||||
"modelProvider.card.tip": "Créditos de mensajes admite modelos de OpenAI. Se dará prioridad a la cuota pagada. La cuota gratuita se utilizará después de que se agote la cuota pagada.",
|
||||
"modelProvider.card.tokens": "Tokens",
|
||||
"modelProvider.collapse": "Colapsar",
|
||||
"modelProvider.config": "Configurar",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "سهمیه",
|
||||
"modelProvider.card.quotaExhausted": "سهمیه تمام شده",
|
||||
"modelProvider.card.removeKey": "حذف کلید API",
|
||||
"modelProvider.card.tip": "اولویت به سهمیه پرداخت شده داده میشود. سهمیه آزمایشی پس از اتمام سهمیه پرداخت شده استفاده خواهد شد.",
|
||||
"modelProvider.card.tip": "اعتبار پیام از مدلهای OpenAI پشتیبانی میکند. اولویت به سهمیه پرداخت شده داده میشود. سهمیه رایگان پس از اتمام سهمیه پرداخت شده استفاده خواهد شد.",
|
||||
"modelProvider.card.tokens": "توکنها",
|
||||
"modelProvider.collapse": "جمع کردن",
|
||||
"modelProvider.config": "پیکربندی",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "QUOTA",
|
||||
"modelProvider.card.quotaExhausted": "Quota épuisé",
|
||||
"modelProvider.card.removeKey": "Supprimer la clé API",
|
||||
"modelProvider.card.tip": "La priorité sera donnée au quota payant. Le quota d'essai sera utilisé après épuisement du quota payant.",
|
||||
"modelProvider.card.tip": "Les crédits de messages prennent en charge les modèles d'OpenAI. La priorité sera donnée au quota payant. Le quota gratuit sera utilisé après épuisement du quota payant.",
|
||||
"modelProvider.card.tokens": "Jetons",
|
||||
"modelProvider.collapse": "Effondrer",
|
||||
"modelProvider.config": "Configuration",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "कोटा",
|
||||
"modelProvider.card.quotaExhausted": "कोटा समाप्त",
|
||||
"modelProvider.card.removeKey": "API कुंजी निकालें",
|
||||
"modelProvider.card.tip": "भुगतान किए गए कोटा को प्राथमिकता दी जाएगी। भुगतान किए गए कोटा के समाप्त होने के बाद परीक्षण कोटा का उपयोग किया जाएगा।",
|
||||
"modelProvider.card.tip": "संदेश क्रेडिट OpenAI के मॉडल का समर्थन करते हैं। भुगतान किए गए कोटा को प्राथमिकता दी जाएगी। भुगतान किए गए कोटा के समाप्त होने के बाद मुफ्त कोटा का उपयोग किया जाएगा।",
|
||||
"modelProvider.card.tokens": "टोकन",
|
||||
"modelProvider.collapse": "संक्षिप्त करें",
|
||||
"modelProvider.config": "कॉन्फ़िग",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "KUOTA",
|
||||
"modelProvider.card.quotaExhausted": "Kuota habis",
|
||||
"modelProvider.card.removeKey": "Menghapus Kunci API",
|
||||
"modelProvider.card.tip": "Prioritas akan diberikan pada kuota yang dibayarkan. Kuota Trial akan digunakan setelah kuota yang dibayarkan habis.",
|
||||
"modelProvider.card.tip": "Kredit pesan mendukung model dari OpenAI. Prioritas akan diberikan pada kuota yang dibayarkan. Kuota gratis akan digunakan setelah kuota yang dibayarkan habis.",
|
||||
"modelProvider.card.tokens": "Token",
|
||||
"modelProvider.collapse": "Roboh",
|
||||
"modelProvider.config": "Konfigurasi",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "QUOTA",
|
||||
"modelProvider.card.quotaExhausted": "Quota esaurita",
|
||||
"modelProvider.card.removeKey": "Rimuovi API Key",
|
||||
"modelProvider.card.tip": "Verrà data priorità alla quota pagata. La quota di prova sarà utilizzata dopo l'esaurimento della quota pagata.",
|
||||
"modelProvider.card.tip": "I crediti di messaggi supportano modelli di OpenAI. Verrà data priorità alla quota pagata. La quota gratuita sarà utilizzata dopo l'esaurimento della quota pagata.",
|
||||
"modelProvider.card.tokens": "Token",
|
||||
"modelProvider.collapse": "Comprimi",
|
||||
"modelProvider.config": "Configura",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "할당량",
|
||||
"modelProvider.card.quotaExhausted": "할당량이 다 사용되었습니다",
|
||||
"modelProvider.card.removeKey": "API 키 제거",
|
||||
"modelProvider.card.tip": "지불된 할당량에 우선순위가 부여됩니다. 평가판 할당량은 유료 할당량이 소진된 후 사용됩니다.",
|
||||
"modelProvider.card.tip": "메시지 크레딧은 OpenAI의 모델을 지원합니다. 유료 할당량에 우선순위가 부여됩니다. 무료 할당량은 유료 할당량이 소진된 후 사용됩니다.",
|
||||
"modelProvider.card.tokens": "토큰",
|
||||
"modelProvider.collapse": "축소",
|
||||
"modelProvider.config": "설정",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "LIMIT",
|
||||
"modelProvider.card.quotaExhausted": "Wyczerpany limit",
|
||||
"modelProvider.card.removeKey": "Usuń klucz API",
|
||||
"modelProvider.card.tip": "Priorytet zostanie nadany płatnemu limitowi. Po wyczerpaniu limitu próbnego zostanie użyty limit płatny.",
|
||||
"modelProvider.card.tip": "Kredyty wiadomości obsługują modele od OpenAI. Priorytet zostanie nadany płatnemu limitowi. Darmowy limit zostanie użyty po wyczerpaniu płatnego limitu.",
|
||||
"modelProvider.card.tokens": "Tokeny",
|
||||
"modelProvider.collapse": "Zwiń",
|
||||
"modelProvider.config": "Konfiguracja",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "QUOTA",
|
||||
"modelProvider.card.quotaExhausted": "Quota esgotada",
|
||||
"modelProvider.card.removeKey": "Remover Chave da API",
|
||||
"modelProvider.card.tip": "A prioridade será dada à quota paga. A quota de teste será usada após a quota paga ser esgotada.",
|
||||
"modelProvider.card.tip": "Créditos de mensagens suportam modelos do OpenAI. A prioridade será dada à quota paga. A quota gratuita será usada após a quota paga ser esgotada.",
|
||||
"modelProvider.card.tokens": "Tokens",
|
||||
"modelProvider.collapse": "Recolher",
|
||||
"modelProvider.config": "Configuração",
|
||||
|
||||
@ -350,7 +350,7 @@
|
||||
"modelProvider.card.quota": "COTĂ",
|
||||
"modelProvider.card.quotaExhausted": "Cotă epuizată",
|
||||
"modelProvider.card.removeKey": "Elimină cheia API",
|
||||
"modelProvider.card.tip": "Prioritate va fi acordată cotei plătite. Cota de probă va fi utilizată după epuizarea cotei plătite.",
|
||||
"modelProvider.card.tip": "Creditele de mesaje acceptă modele de la OpenAI. Prioritate va fi acordată cotei plătite. Cota gratuită va fi utilizată după epuizarea cotei plătite.",
|
||||
"modelProvider.card.tokens": "Jetoane",
|
||||
"modelProvider.collapse": "Restrânge",
|
||||
"modelProvider.config": "Configurare",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user