mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Merge branch 'main' into feat/grouping-branching
# Conflicts: # web/package.json
This commit is contained in:
@ -7,8 +7,12 @@ logs
|
||||
|
||||
# node
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
.husky
|
||||
.next
|
||||
.pnpm-store
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
@ -22,3 +26,7 @@ node_modules
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
|
||||
# git
|
||||
.git
|
||||
.gitignore
|
||||
@ -47,6 +47,8 @@ NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
||||
|
||||
# The maximum number of tokens for segmentation
|
||||
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||
# Used by web/docker/entrypoint.sh to overwrite/export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH at container startup (Docker only)
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||
|
||||
# Maximum loop count in the workflow
|
||||
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT=100
|
||||
|
||||
@ -12,7 +12,8 @@ RUN apk add --no-cache tzdata
|
||||
RUN corepack enable
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
ENV NEXT_PUBLIC_BASE_PATH=""
|
||||
ARG NEXT_PUBLIC_BASE_PATH=""
|
||||
ENV NEXT_PUBLIC_BASE_PATH="$NEXT_PUBLIC_BASE_PATH"
|
||||
|
||||
|
||||
# install packages
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
/* eslint-disable dify-i18n/require-ns-option */
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
import Form from '@/app/components/datasets/settings/form'
|
||||
import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
|
||||
|
||||
const Settings = async () => {
|
||||
const locale = await getLocaleOnServer()
|
||||
const { t } = await getTranslation(locale, 'dataset-settings')
|
||||
const Settings = () => {
|
||||
const { t } = useTranslation('datasetSettings')
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from '../index.module.css'
|
||||
|
||||
type DataSourceTypeSelectorProps = {
|
||||
currentType: DataSourceType
|
||||
disabled: boolean
|
||||
onChange: (type: DataSourceType) => void
|
||||
onClearPreviews: (type: DataSourceType) => void
|
||||
}
|
||||
|
||||
type DataSourceLabelKey
|
||||
= | 'stepOne.dataSourceType.file'
|
||||
| 'stepOne.dataSourceType.notion'
|
||||
| 'stepOne.dataSourceType.web'
|
||||
|
||||
type DataSourceOption = {
|
||||
type: DataSourceType
|
||||
iconClass?: string
|
||||
labelKey: DataSourceLabelKey
|
||||
}
|
||||
|
||||
const DATA_SOURCE_OPTIONS: DataSourceOption[] = [
|
||||
{
|
||||
type: DataSourceType.FILE,
|
||||
labelKey: 'stepOne.dataSourceType.file',
|
||||
},
|
||||
{
|
||||
type: DataSourceType.NOTION,
|
||||
iconClass: s.notion,
|
||||
labelKey: 'stepOne.dataSourceType.notion',
|
||||
},
|
||||
{
|
||||
type: DataSourceType.WEB,
|
||||
iconClass: s.web,
|
||||
labelKey: 'stepOne.dataSourceType.web',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Data source type selector component for choosing between file, notion, and web sources.
|
||||
*/
|
||||
function DataSourceTypeSelector({
|
||||
currentType,
|
||||
disabled,
|
||||
onChange,
|
||||
onClearPreviews,
|
||||
}: DataSourceTypeSelectorProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isWebEnabled = ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL
|
||||
|
||||
const handleTypeChange = useCallback((type: DataSourceType) => {
|
||||
if (disabled)
|
||||
return
|
||||
onChange(type)
|
||||
onClearPreviews(type)
|
||||
}, [disabled, onChange, onClearPreviews])
|
||||
|
||||
const visibleOptions = useMemo(() => DATA_SOURCE_OPTIONS.filter((option) => {
|
||||
if (option.type === DataSourceType.WEB)
|
||||
return isWebEnabled
|
||||
return true
|
||||
}), [isWebEnabled])
|
||||
|
||||
return (
|
||||
<div className="mb-8 grid grid-cols-3 gap-4">
|
||||
{visibleOptions.map(option => (
|
||||
<div
|
||||
key={option.type}
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
currentType === option.type && s.active,
|
||||
disabled && currentType !== option.type && s.disabled,
|
||||
)}
|
||||
onClick={() => handleTypeChange(option.type)}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, option.iconClass)} />
|
||||
<span
|
||||
title={t(option.labelKey, { ns: 'datasetCreation' }) || undefined}
|
||||
className="truncate"
|
||||
>
|
||||
{t(option.labelKey, { ns: 'datasetCreation' })}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataSourceTypeSelector
|
||||
@ -0,0 +1,3 @@
|
||||
export { default as DataSourceTypeSelector } from './data-source-type-selector'
|
||||
export { default as NextStepButton } from './next-step-button'
|
||||
export { default as PreviewPanel } from './preview-panel'
|
||||
@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type NextStepButtonProps = {
|
||||
disabled: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable next step button component for dataset creation flow.
|
||||
*/
|
||||
function NextStepButton({ disabled, onClick }: NextStepButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex max-w-[640px] justify-end gap-2">
|
||||
<Button disabled={disabled} variant="primary" onClick={onClick}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NextStepButton
|
||||
@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
|
||||
import FilePreview from '../../file-preview'
|
||||
import NotionPagePreview from '../../notion-page-preview'
|
||||
import WebsitePreview from '../../website/preview'
|
||||
|
||||
type PreviewPanelProps = {
|
||||
currentFile: File | undefined
|
||||
currentNotionPage: NotionPage | undefined
|
||||
currentWebsite: CrawlResultItem | undefined
|
||||
notionCredentialId: string
|
||||
isShowPlanUpgradeModal: boolean
|
||||
hideFilePreview: () => void
|
||||
hideNotionPagePreview: () => void
|
||||
hideWebsitePreview: () => void
|
||||
hidePlanUpgradeModal: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Right panel component for displaying file, notion page, or website previews.
|
||||
*/
|
||||
function PreviewPanel({
|
||||
currentFile,
|
||||
currentNotionPage,
|
||||
currentWebsite,
|
||||
notionCredentialId,
|
||||
isShowPlanUpgradeModal,
|
||||
hideFilePreview,
|
||||
hideNotionPagePreview,
|
||||
hideWebsitePreview,
|
||||
hidePlanUpgradeModal,
|
||||
}: PreviewPanelProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="h-full w-1/2 overflow-y-auto">
|
||||
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
|
||||
{currentNotionPage && (
|
||||
<NotionPagePreview
|
||||
currentPage={currentNotionPage}
|
||||
hidePreview={hideNotionPagePreview}
|
||||
notionCredentialId={notionCredentialId}
|
||||
/>
|
||||
)}
|
||||
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
|
||||
{isShowPlanUpgradeModal && (
|
||||
<PlanUpgradeModal
|
||||
show
|
||||
onClose={hidePlanUpgradeModal}
|
||||
title={t('upgrade.uploadMultiplePages.title', { ns: 'billing' })!}
|
||||
description={t('upgrade.uploadMultiplePages.description', { ns: 'billing' })!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewPanel
|
||||
@ -0,0 +1,2 @@
|
||||
export { default as usePreviewState } from './use-preview-state'
|
||||
export type { PreviewActions, PreviewState, UsePreviewStateReturn } from './use-preview-state'
|
||||
@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
export type PreviewState = {
|
||||
currentFile: File | undefined
|
||||
currentNotionPage: NotionPage | undefined
|
||||
currentWebsite: CrawlResultItem | undefined
|
||||
}
|
||||
|
||||
export type PreviewActions = {
|
||||
showFilePreview: (file: File) => void
|
||||
hideFilePreview: () => void
|
||||
showNotionPagePreview: (page: NotionPage) => void
|
||||
hideNotionPagePreview: () => void
|
||||
showWebsitePreview: (website: CrawlResultItem) => void
|
||||
hideWebsitePreview: () => void
|
||||
}
|
||||
|
||||
export type UsePreviewStateReturn = PreviewState & PreviewActions
|
||||
|
||||
/**
|
||||
* Custom hook for managing preview state across different data source types.
|
||||
* Handles file, notion page, and website preview visibility.
|
||||
*/
|
||||
function usePreviewState(): UsePreviewStateReturn {
|
||||
const [currentFile, setCurrentFile] = useState<File | undefined>()
|
||||
const [currentNotionPage, setCurrentNotionPage] = useState<NotionPage | undefined>()
|
||||
const [currentWebsite, setCurrentWebsite] = useState<CrawlResultItem | undefined>()
|
||||
|
||||
const showFilePreview = useCallback((file: File) => {
|
||||
setCurrentFile(file)
|
||||
}, [])
|
||||
|
||||
const hideFilePreview = useCallback(() => {
|
||||
setCurrentFile(undefined)
|
||||
}, [])
|
||||
|
||||
const showNotionPagePreview = useCallback((page: NotionPage) => {
|
||||
setCurrentNotionPage(page)
|
||||
}, [])
|
||||
|
||||
const hideNotionPagePreview = useCallback(() => {
|
||||
setCurrentNotionPage(undefined)
|
||||
}, [])
|
||||
|
||||
const showWebsitePreview = useCallback((website: CrawlResultItem) => {
|
||||
setCurrentWebsite(website)
|
||||
}, [])
|
||||
|
||||
const hideWebsitePreview = useCallback(() => {
|
||||
setCurrentWebsite(undefined)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
currentFile,
|
||||
currentNotionPage,
|
||||
currentWebsite,
|
||||
showFilePreview,
|
||||
hideFilePreview,
|
||||
showNotionPagePreview,
|
||||
hideNotionPagePreview,
|
||||
showWebsitePreview,
|
||||
hideWebsitePreview,
|
||||
}
|
||||
}
|
||||
|
||||
export default usePreviewState
|
||||
1204
web/app/components/datasets/create/step-one/index.spec.tsx
Normal file
1204
web/app/components/datasets/create/step-one/index.spec.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,29 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
|
||||
import type { DataSourceProvider, NotionPage } from '@/models/common'
|
||||
import type { CrawlOptions, CrawlResultItem, FileItem } from '@/models/datasets'
|
||||
import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react'
|
||||
import { RiFolder6Line } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import NotionConnector from '@/app/components/base/notion-connector'
|
||||
import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
|
||||
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
|
||||
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import EmptyDatasetCreationModal from '../empty-dataset-creation-modal'
|
||||
import FilePreview from '../file-preview'
|
||||
import FileUploader from '../file-uploader'
|
||||
import NotionPagePreview from '../notion-page-preview'
|
||||
import Website from '../website'
|
||||
import WebsitePreview from '../website/preview'
|
||||
import { DataSourceTypeSelector, NextStepButton, PreviewPanel } from './components'
|
||||
import { usePreviewState } from './hooks'
|
||||
import s from './index.module.css'
|
||||
import UpgradeCard from './upgrade-card'
|
||||
|
||||
@ -50,6 +46,24 @@ type IStepOneProps = {
|
||||
authedDataSourceList: DataSourceAuth[]
|
||||
}
|
||||
|
||||
// Helper function to check if notion is authenticated
|
||||
function checkNotionAuth(authedDataSourceList: DataSourceAuth[]): boolean {
|
||||
const notionSource = authedDataSourceList.find(item => item.provider === 'notion_datasource')
|
||||
return Boolean(notionSource && notionSource.credentials_list.length > 0)
|
||||
}
|
||||
|
||||
// Helper function to get notion credential list
|
||||
function getNotionCredentialList(authedDataSourceList: DataSourceAuth[]) {
|
||||
return authedDataSourceList.find(item => item.provider === 'notion_datasource')?.credentials_list || []
|
||||
}
|
||||
|
||||
// Lookup table for checking multiple items by data source type
|
||||
const MULTIPLE_ITEMS_CHECK: Record<DataSourceType, (props: { files: FileItem[], notionPages: NotionPage[], websitePages: CrawlResultItem[] }) => boolean> = {
|
||||
[DataSourceType.FILE]: ({ files }) => files.length > 1,
|
||||
[DataSourceType.NOTION]: ({ notionPages }) => notionPages.length > 1,
|
||||
[DataSourceType.WEB]: ({ websitePages }) => websitePages.length > 1,
|
||||
}
|
||||
|
||||
const StepOne = ({
|
||||
datasetId,
|
||||
dataSourceType: inCreatePageDataSourceType,
|
||||
@ -72,76 +86,47 @@ const StepOne = ({
|
||||
onCrawlOptionsChange,
|
||||
authedDataSourceList,
|
||||
}: IStepOneProps) => {
|
||||
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [currentFile, setCurrentFile] = useState<File | undefined>()
|
||||
const [currentNotionPage, setCurrentNotionPage] = useState<NotionPage | undefined>()
|
||||
const [currentWebsite, setCurrentWebsite] = useState<CrawlResultItem | undefined>()
|
||||
const { t } = useTranslation()
|
||||
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
|
||||
const modalShowHandle = () => setShowModal(true)
|
||||
const modalCloseHandle = () => setShowModal(false)
|
||||
// Preview state management
|
||||
const {
|
||||
currentFile,
|
||||
currentNotionPage,
|
||||
currentWebsite,
|
||||
showFilePreview,
|
||||
hideFilePreview,
|
||||
showNotionPagePreview,
|
||||
hideNotionPagePreview,
|
||||
showWebsitePreview,
|
||||
hideWebsitePreview,
|
||||
} = usePreviewState()
|
||||
|
||||
const updateCurrentFile = useCallback((file: File) => {
|
||||
setCurrentFile(file)
|
||||
}, [])
|
||||
// Empty dataset modal state
|
||||
const [showModal, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false)
|
||||
|
||||
const hideFilePreview = useCallback(() => {
|
||||
setCurrentFile(undefined)
|
||||
}, [])
|
||||
|
||||
const updateCurrentPage = useCallback((page: NotionPage) => {
|
||||
setCurrentNotionPage(page)
|
||||
}, [])
|
||||
|
||||
const hideNotionPagePreview = useCallback(() => {
|
||||
setCurrentNotionPage(undefined)
|
||||
}, [])
|
||||
|
||||
const updateWebsite = useCallback((website: CrawlResultItem) => {
|
||||
setCurrentWebsite(website)
|
||||
}, [])
|
||||
|
||||
const hideWebsitePreview = useCallback(() => {
|
||||
setCurrentWebsite(undefined)
|
||||
}, [])
|
||||
// Plan upgrade modal state
|
||||
const [isShowPlanUpgradeModal, { setTrue: showPlanUpgradeModal, setFalse: hidePlanUpgradeModal }] = useBoolean(false)
|
||||
|
||||
// Computed values
|
||||
const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type)
|
||||
const isInCreatePage = shouldShowDataSourceTypeList
|
||||
const dataSourceType = isInCreatePage ? inCreatePageDataSourceType : dataset?.data_source_type
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const allFileLoaded = (files.length > 0 && files.every(file => file.file.id))
|
||||
const hasNotin = notionPages.length > 0
|
||||
// Default to FILE type when no type is provided from either source
|
||||
const dataSourceType = isInCreatePage
|
||||
? (inCreatePageDataSourceType ?? DataSourceType.FILE)
|
||||
: (dataset?.data_source_type ?? DataSourceType.FILE)
|
||||
|
||||
const allFileLoaded = files.length > 0 && files.every(file => file.file.id)
|
||||
const hasNotion = notionPages.length > 0
|
||||
const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace
|
||||
const isShowVectorSpaceFull = (allFileLoaded || hasNotin) && isVectorSpaceFull && enableBilling
|
||||
const isShowVectorSpaceFull = (allFileLoaded || hasNotion) && isVectorSpaceFull && enableBilling
|
||||
const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox
|
||||
const notSupportBatchUpload = !supportBatchUpload
|
||||
|
||||
const [isShowPlanUpgradeModal, {
|
||||
setTrue: showPlanUpgradeModal,
|
||||
setFalse: hidePlanUpgradeModal,
|
||||
}] = useBoolean(false)
|
||||
const onStepChange = useCallback(() => {
|
||||
if (notSupportBatchUpload) {
|
||||
let isMultiple = false
|
||||
if (dataSourceType === DataSourceType.FILE && files.length > 1)
|
||||
isMultiple = true
|
||||
const isNotionAuthed = useMemo(() => checkNotionAuth(authedDataSourceList), [authedDataSourceList])
|
||||
const notionCredentialList = useMemo(() => getNotionCredentialList(authedDataSourceList), [authedDataSourceList])
|
||||
|
||||
if (dataSourceType === DataSourceType.NOTION && notionPages.length > 1)
|
||||
isMultiple = true
|
||||
|
||||
if (dataSourceType === DataSourceType.WEB && websitePages.length > 1)
|
||||
isMultiple = true
|
||||
|
||||
if (isMultiple) {
|
||||
showPlanUpgradeModal()
|
||||
return
|
||||
}
|
||||
}
|
||||
doOnStepChange()
|
||||
}, [dataSourceType, doOnStepChange, files.length, notSupportBatchUpload, notionPages.length, showPlanUpgradeModal, websitePages.length])
|
||||
|
||||
const nextDisabled = useMemo(() => {
|
||||
const fileNextDisabled = useMemo(() => {
|
||||
if (!files.length)
|
||||
return true
|
||||
if (files.some(file => !file.file.id))
|
||||
@ -149,109 +134,50 @@ const StepOne = ({
|
||||
return isShowVectorSpaceFull
|
||||
}, [files, isShowVectorSpaceFull])
|
||||
|
||||
const isNotionAuthed = useMemo(() => {
|
||||
if (!authedDataSourceList)
|
||||
return false
|
||||
const notionSource = authedDataSourceList.find(item => item.provider === 'notion_datasource')
|
||||
if (!notionSource)
|
||||
return false
|
||||
return notionSource.credentials_list.length > 0
|
||||
}, [authedDataSourceList])
|
||||
// Clear previews when switching data source type
|
||||
const handleClearPreviews = useCallback((newType: DataSourceType) => {
|
||||
if (newType !== DataSourceType.FILE)
|
||||
hideFilePreview()
|
||||
if (newType !== DataSourceType.NOTION)
|
||||
hideNotionPagePreview()
|
||||
if (newType !== DataSourceType.WEB)
|
||||
hideWebsitePreview()
|
||||
}, [hideFilePreview, hideNotionPagePreview, hideWebsitePreview])
|
||||
|
||||
const notionCredentialList = useMemo(() => {
|
||||
return authedDataSourceList.find(item => item.provider === 'notion_datasource')?.credentials_list || []
|
||||
}, [authedDataSourceList])
|
||||
// Handle step change with batch upload check
|
||||
const onStepChange = useCallback(() => {
|
||||
if (!supportBatchUpload && dataSourceType) {
|
||||
const checkFn = MULTIPLE_ITEMS_CHECK[dataSourceType]
|
||||
if (checkFn?.({ files, notionPages, websitePages })) {
|
||||
showPlanUpgradeModal()
|
||||
return
|
||||
}
|
||||
}
|
||||
doOnStepChange()
|
||||
}, [dataSourceType, doOnStepChange, files, supportBatchUpload, notionPages, showPlanUpgradeModal, websitePages])
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-x-auto">
|
||||
<div className="flex h-full w-full min-w-[1440px]">
|
||||
{/* Left Panel - Form */}
|
||||
<div className="relative h-full w-1/2 overflow-y-auto">
|
||||
<div className="flex justify-end">
|
||||
<div className={cn(s.form)}>
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
{shouldShowDataSourceTypeList && (
|
||||
<>
|
||||
<div className={cn(s.stepHeader, 'system-md-semibold text-text-secondary')}>
|
||||
{t('steps.one', { ns: 'datasetCreation' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className="mb-8 grid grid-cols-3 gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
dataSourceType === DataSourceType.FILE && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.FILE)
|
||||
hideNotionPagePreview()
|
||||
hideWebsitePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon)} />
|
||||
<span
|
||||
title={t('stepOne.dataSourceType.file', { ns: 'datasetCreation' })!}
|
||||
className="truncate"
|
||||
>
|
||||
{t('stepOne.dataSourceType.file', { ns: 'datasetCreation' })}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
dataSourceType === DataSourceType.NOTION && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.NOTION)
|
||||
hideFilePreview()
|
||||
hideWebsitePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.notion)} />
|
||||
<span
|
||||
title={t('stepOne.dataSourceType.notion', { ns: 'datasetCreation' })!}
|
||||
className="truncate"
|
||||
>
|
||||
{t('stepOne.dataSourceType.notion', { ns: 'datasetCreation' })}
|
||||
</span>
|
||||
</div>
|
||||
{(ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL) && (
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
'system-sm-medium',
|
||||
dataSourceType === DataSourceType.WEB && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.WEB)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.web)} />
|
||||
<span
|
||||
title={t('stepOne.dataSourceType.web', { ns: 'datasetCreation' })!}
|
||||
className="truncate"
|
||||
>
|
||||
{t('stepOne.dataSourceType.web', { ns: 'datasetCreation' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<DataSourceTypeSelector
|
||||
currentType={dataSourceType}
|
||||
disabled={dataSourceTypeDisable}
|
||||
onChange={changeType}
|
||||
onClearPreviews={handleClearPreviews}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* File Data Source */}
|
||||
{dataSourceType === DataSourceType.FILE && (
|
||||
<>
|
||||
<FileUploader
|
||||
@ -260,7 +186,7 @@ const StepOne = ({
|
||||
prepareFileList={updateFileList}
|
||||
onFileListUpdate={updateFileList}
|
||||
onFileUpdate={updateFile}
|
||||
onPreview={updateCurrentFile}
|
||||
onPreview={showFilePreview}
|
||||
supportBatchUpload={supportBatchUpload}
|
||||
/>
|
||||
{isShowVectorSpaceFull && (
|
||||
@ -268,24 +194,17 @@ const StepOne = ({
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[640px] justify-end gap-2">
|
||||
<Button disabled={nextDisabled} variant="primary" onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
|
||||
<div className="mt-5">
|
||||
<div className="mb-4 h-px bg-divider-subtle"></div>
|
||||
<UpgradeCard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<NextStepButton disabled={fileNextDisabled} onClick={onStepChange} />
|
||||
{enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
|
||||
<div className="mt-5">
|
||||
<div className="mb-4 h-px bg-divider-subtle" />
|
||||
<UpgradeCard />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Notion Data Source */}
|
||||
{dataSourceType === DataSourceType.NOTION && (
|
||||
<>
|
||||
{!isNotionAuthed && <NotionConnector onSetting={onSetting} />}
|
||||
@ -295,7 +214,7 @@ const StepOne = ({
|
||||
<NotionPageSelector
|
||||
value={notionPages.map(page => page.page_id)}
|
||||
onSelect={updateNotionPages}
|
||||
onPreview={updateCurrentPage}
|
||||
onPreview={showNotionPagePreview}
|
||||
credentialList={notionCredentialList}
|
||||
onSelectCredential={updateNotionCredentialId}
|
||||
datasetId={datasetId}
|
||||
@ -306,23 +225,21 @@ const StepOne = ({
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[640px] justify-end gap-2">
|
||||
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant="primary" onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<NextStepButton
|
||||
disabled={isShowVectorSpaceFull || !notionPages.length}
|
||||
onClick={onStepChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Web Data Source */}
|
||||
{dataSourceType === DataSourceType.WEB && (
|
||||
<>
|
||||
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
|
||||
<Website
|
||||
onPreview={updateWebsite}
|
||||
onPreview={showWebsitePreview}
|
||||
checkedCrawlResult={websitePages}
|
||||
onCheckedCrawlResultChange={updateWebsitePages}
|
||||
onCrawlProviderChange={onWebsiteCrawlProviderChange}
|
||||
@ -337,48 +254,43 @@ const StepOne = ({
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex max-w-[640px] justify-end gap-2">
|
||||
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant="primary" onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<NextStepButton
|
||||
disabled={isShowVectorSpaceFull || !websitePages.length}
|
||||
onClick={onStepChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Empty Dataset Creation Link */}
|
||||
{!datasetId && (
|
||||
<>
|
||||
<div className="my-8 h-px max-w-[640px] bg-divider-regular" />
|
||||
<span className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
|
||||
<span
|
||||
className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent"
|
||||
onClick={openModal}
|
||||
>
|
||||
<RiFolder6Line className="mr-1 size-4" />
|
||||
{t('stepOne.emptyDatasetCreation', { ns: 'datasetCreation' })}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
|
||||
<EmptyDatasetCreationModal show={showModal} onHide={closeModal} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-1/2 overflow-y-auto">
|
||||
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
|
||||
{currentNotionPage && (
|
||||
<NotionPagePreview
|
||||
currentPage={currentNotionPage}
|
||||
hidePreview={hideNotionPagePreview}
|
||||
notionCredentialId={notionCredentialId}
|
||||
/>
|
||||
)}
|
||||
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
|
||||
{isShowPlanUpgradeModal && (
|
||||
<PlanUpgradeModal
|
||||
show
|
||||
onClose={hidePlanUpgradeModal}
|
||||
title={t('upgrade.uploadMultiplePages.title', { ns: 'billing' })!}
|
||||
description={t('upgrade.uploadMultiplePages.description', { ns: 'billing' })!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Preview */}
|
||||
<PreviewPanel
|
||||
currentFile={currentFile}
|
||||
currentNotionPage={currentNotionPage}
|
||||
currentWebsite={currentWebsite}
|
||||
notionCredentialId={notionCredentialId}
|
||||
isShowPlanUpgradeModal={isShowPlanUpgradeModal}
|
||||
hideFilePreview={hideFilePreview}
|
||||
hideNotionPagePreview={hideNotionPagePreview}
|
||||
hideWebsitePreview={hideWebsitePreview}
|
||||
hidePlanUpgradeModal={hidePlanUpgradeModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -31,14 +31,19 @@ const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/an
|
||||
const ModelProviderPage = ({ searchText }: Props) => {
|
||||
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
|
||||
const { t } = useTranslation()
|
||||
const { data: textGenerationDefaultModel } = useDefaultModel(ModelTypeEnum.textGeneration)
|
||||
const { data: embeddingsDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankDefaultModel } = useDefaultModel(ModelTypeEnum.rerank)
|
||||
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
|
||||
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
|
||||
const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textGeneration)
|
||||
const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(ModelTypeEnum.rerank)
|
||||
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } = useDefaultModel(ModelTypeEnum.speech2text)
|
||||
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts)
|
||||
const { modelProviders: providers } = useProviderContext()
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
|
||||
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|
||||
|| isEmbeddingsDefaultModelLoading
|
||||
|| isRerankDefaultModelLoading
|
||||
|| isSpeech2textDefaultModelLoading
|
||||
|| isTTSDefaultModelLoading
|
||||
const defaultModelNotConfigured = !isDefaultModelLoading && !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
|
||||
const [configuredProviders, notConfiguredProviders] = useMemo(() => {
|
||||
const configuredProviders: ModelProvider[] = []
|
||||
const notConfiguredProviders: ModelProvider[] = []
|
||||
@ -106,6 +111,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
rerankDefaultModel={rerankDefaultModel}
|
||||
speech2textDefaultModel={speech2textDefaultModel}
|
||||
ttsDefaultModel={ttsDefaultModel}
|
||||
isLoading={isDefaultModelLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
DefaultModel,
|
||||
DefaultModelResponse,
|
||||
} from '../declarations'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { RiEqualizer2Line, RiLoader2Line } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
@ -32,6 +32,7 @@ type SystemModelSelectorProps = {
|
||||
speech2textDefaultModel: DefaultModelResponse | undefined
|
||||
ttsDefaultModel: DefaultModelResponse | undefined
|
||||
notConfigured: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
textGenerationDefaultModel,
|
||||
@ -40,6 +41,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
speech2textDefaultModel,
|
||||
ttsDefaultModel,
|
||||
notConfigured,
|
||||
isLoading,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
@ -129,13 +131,16 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
|
||||
crossAxis: 8,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<PortalToFollowElemTrigger asChild onClick={() => setOpen(v => !v)}>
|
||||
<Button
|
||||
className="relative"
|
||||
variant={notConfigured ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<RiEqualizer2Line className="mr-1 h-3.5 w-3.5" />
|
||||
{isLoading
|
||||
? <RiLoader2Line className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
: <RiEqualizer2Line className="mr-1 h-3.5 w-3.5" />}
|
||||
{t('modelProvider.systemModelSettings', { ns: 'common' })}
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Import component after mocks are set up
|
||||
import Description from './index'
|
||||
|
||||
// ================================
|
||||
@ -30,20 +28,18 @@ const commonTranslations: Record<string, string> = {
|
||||
'operation.in': 'in',
|
||||
}
|
||||
|
||||
// Mock getLocaleOnServer and translate
|
||||
vi.mock('@/i18n-config/server', () => ({
|
||||
getLocaleOnServer: vi.fn(() => Promise.resolve(mockDefaultLocale)),
|
||||
getTranslation: vi.fn((locale: string, ns: string) => {
|
||||
return Promise.resolve({
|
||||
t: (key: string) => {
|
||||
if (ns === 'plugin')
|
||||
return pluginTranslations[key] || key
|
||||
if (ns === 'common')
|
||||
return commonTranslations[key] || key
|
||||
return key
|
||||
},
|
||||
})
|
||||
}),
|
||||
// Mock i18n hooks
|
||||
vi.mock('#i18n', () => ({
|
||||
useLocale: vi.fn(() => mockDefaultLocale),
|
||||
useTranslation: vi.fn((ns: string) => ({
|
||||
t: (key: string) => {
|
||||
if (ns === 'plugin')
|
||||
return pluginTranslations[key] || key
|
||||
if (ns === 'common')
|
||||
return commonTranslations[key] || key
|
||||
return key
|
||||
},
|
||||
})),
|
||||
}))
|
||||
|
||||
// ================================
|
||||
@ -59,29 +55,29 @@ describe('Description', () => {
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render h1 heading with empower text', async () => {
|
||||
render(await Description({}))
|
||||
it('should render h1 heading with empower text', () => {
|
||||
render(<Description />)
|
||||
|
||||
const heading = screen.getByRole('heading', { level: 1 })
|
||||
expect(heading).toBeInTheDocument()
|
||||
expect(heading).toHaveTextContent('Empower your AI development')
|
||||
})
|
||||
|
||||
it('should render h2 subheading', async () => {
|
||||
render(await Description({}))
|
||||
it('should render h2 subheading', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply correct CSS classes to h1', async () => {
|
||||
render(await Description({}))
|
||||
it('should apply correct CSS classes to h1', () => {
|
||||
render(<Description />)
|
||||
|
||||
const heading = screen.getByRole('heading', { level: 1 })
|
||||
expect(heading).toHaveClass('title-4xl-semi-bold')
|
||||
@ -90,8 +86,8 @@ describe('Description', () => {
|
||||
expect(heading).toHaveClass('text-text-primary')
|
||||
})
|
||||
|
||||
it('should apply correct CSS classes to h2', async () => {
|
||||
render(await Description({}))
|
||||
it('should apply correct CSS classes to h2', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toHaveClass('body-md-regular')
|
||||
@ -104,14 +100,18 @@ describe('Description', () => {
|
||||
// Non-Chinese Locale Rendering Tests
|
||||
// ================================
|
||||
describe('Non-Chinese Locale Rendering', () => {
|
||||
it('should render discover text for en-US locale', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
beforeEach(() => {
|
||||
mockDefaultLocale = 'en-US'
|
||||
})
|
||||
|
||||
it('should render discover text for en-US locale', () => {
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText(/Discover/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all category names', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
it('should render all category names', () => {
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText('Models')).toBeInTheDocument()
|
||||
expect(screen.getByText('Tools')).toBeInTheDocument()
|
||||
@ -122,36 +122,36 @@ describe('Description', () => {
|
||||
expect(screen.getByText('Bundles')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "and" conjunction text', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
it('should render "and" conjunction text', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading.textContent).toContain('and')
|
||||
})
|
||||
|
||||
it('should render "in" preposition at the end for non-Chinese locales', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
it('should render "in" preposition at the end for non-Chinese locales', () => {
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText('in')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Dify Marketplace text at the end for non-Chinese locales', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
it('should render Dify Marketplace text at the end for non-Chinese locales', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading.textContent).toContain('Dify Marketplace')
|
||||
})
|
||||
|
||||
it('should render category spans with styled underline effect', async () => {
|
||||
const { container } = render(await Description({ locale: 'en-US' }))
|
||||
it('should render category spans with styled underline effect', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const styledSpans = container.querySelectorAll('.body-md-medium.relative.z-\\[1\\]')
|
||||
// 7 category spans (models, tools, datasources, triggers, agents, extensions, bundles)
|
||||
expect(styledSpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should apply text-text-secondary class to category spans', async () => {
|
||||
const { container } = render(await Description({ locale: 'en-US' }))
|
||||
it('should apply text-text-secondary class to category spans', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const styledSpans = container.querySelectorAll('.text-text-secondary')
|
||||
expect(styledSpans.length).toBeGreaterThanOrEqual(7)
|
||||
@ -162,29 +162,33 @@ describe('Description', () => {
|
||||
// Chinese (zh-Hans) Locale Rendering Tests
|
||||
// ================================
|
||||
describe('Chinese (zh-Hans) Locale Rendering', () => {
|
||||
it('should render "in" text at the beginning for zh-Hans locale', async () => {
|
||||
render(await Description({ locale: 'zh-Hans' }))
|
||||
beforeEach(() => {
|
||||
mockDefaultLocale = 'zh-Hans'
|
||||
})
|
||||
|
||||
it('should render "in" text at the beginning for zh-Hans locale', () => {
|
||||
render(<Description />)
|
||||
|
||||
// In zh-Hans mode, "in" appears at the beginning
|
||||
const inElements = screen.getAllByText('in')
|
||||
expect(inElements.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('should render Dify Marketplace text for zh-Hans locale', async () => {
|
||||
render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render Dify Marketplace text for zh-Hans locale', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading.textContent).toContain('Dify Marketplace')
|
||||
})
|
||||
|
||||
it('should render discover text for zh-Hans locale', async () => {
|
||||
render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render discover text for zh-Hans locale', () => {
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText(/Discover/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all categories for zh-Hans locale', async () => {
|
||||
render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render all categories for zh-Hans locale', () => {
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText('Models')).toBeInTheDocument()
|
||||
expect(screen.getByText('Tools')).toBeInTheDocument()
|
||||
@ -195,8 +199,8 @@ describe('Description', () => {
|
||||
expect(screen.getByText('Bundles')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render both zh-Hans specific elements and shared elements', async () => {
|
||||
render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render both zh-Hans specific elements and shared elements', () => {
|
||||
render(<Description />)
|
||||
|
||||
// zh-Hans has specific element order: "in" -> Dify Marketplace -> Discover
|
||||
// then the same category list with "and" -> Bundles
|
||||
@ -206,61 +210,57 @@ describe('Description', () => {
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Locale Prop Variations Tests
|
||||
// Locale Variations Tests
|
||||
// ================================
|
||||
describe('Locale Prop Variations', () => {
|
||||
it('should use default locale when locale prop is undefined', async () => {
|
||||
describe('Locale Variations', () => {
|
||||
it('should use en-US locale by default', () => {
|
||||
mockDefaultLocale = 'en-US'
|
||||
render(await Description({}))
|
||||
render(<Description />)
|
||||
|
||||
// Should use the default locale from getLocaleOnServer
|
||||
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use provided locale prop instead of default', async () => {
|
||||
it('should handle ja-JP locale as non-Chinese', () => {
|
||||
mockDefaultLocale = 'ja-JP'
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
|
||||
// The locale prop should be used, triggering non-Chinese rendering
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle ja-JP locale as non-Chinese', async () => {
|
||||
render(await Description({ locale: 'ja-JP' }))
|
||||
render(<Description />)
|
||||
|
||||
// Should render in non-Chinese format (discover first, then "in Dify Marketplace" at end)
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading.textContent).toContain('Dify Marketplace')
|
||||
})
|
||||
|
||||
it('should handle ko-KR locale as non-Chinese', async () => {
|
||||
render(await Description({ locale: 'ko-KR' }))
|
||||
it('should handle ko-KR locale as non-Chinese', () => {
|
||||
mockDefaultLocale = 'ko-KR'
|
||||
render(<Description />)
|
||||
|
||||
// Should render in non-Chinese format
|
||||
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle de-DE locale as non-Chinese', async () => {
|
||||
render(await Description({ locale: 'de-DE' }))
|
||||
it('should handle de-DE locale as non-Chinese', () => {
|
||||
mockDefaultLocale = 'de-DE'
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle fr-FR locale as non-Chinese', async () => {
|
||||
render(await Description({ locale: 'fr-FR' }))
|
||||
it('should handle fr-FR locale as non-Chinese', () => {
|
||||
mockDefaultLocale = 'fr-FR'
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle pt-BR locale as non-Chinese', async () => {
|
||||
render(await Description({ locale: 'pt-BR' }))
|
||||
it('should handle pt-BR locale as non-Chinese', () => {
|
||||
mockDefaultLocale = 'pt-BR'
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle es-ES locale as non-Chinese', async () => {
|
||||
render(await Description({ locale: 'es-ES' }))
|
||||
it('should handle es-ES locale as non-Chinese', () => {
|
||||
mockDefaultLocale = 'es-ES'
|
||||
render(<Description />)
|
||||
|
||||
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
|
||||
})
|
||||
@ -270,24 +270,27 @@ describe('Description', () => {
|
||||
// Conditional Rendering Tests
|
||||
// ================================
|
||||
describe('Conditional Rendering', () => {
|
||||
it('should render zh-Hans specific content when locale is zh-Hans', async () => {
|
||||
const { container } = render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render zh-Hans specific content when locale is zh-Hans', () => {
|
||||
mockDefaultLocale = 'zh-Hans'
|
||||
const { container } = render(<Description />)
|
||||
|
||||
// zh-Hans has additional span with mr-1 before "in" text at the start
|
||||
const mrSpan = container.querySelector('span.mr-1')
|
||||
expect(mrSpan).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render non-Chinese specific content when locale is not zh-Hans', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
it('should render non-Chinese specific content when locale is not zh-Hans', () => {
|
||||
mockDefaultLocale = 'en-US'
|
||||
render(<Description />)
|
||||
|
||||
// Non-Chinese has "in" and "Dify Marketplace" at the end
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading.textContent).toContain('Dify Marketplace')
|
||||
})
|
||||
|
||||
it('should not render zh-Hans intro content for non-Chinese locales', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
it('should not render zh-Hans intro content for non-Chinese locales', () => {
|
||||
mockDefaultLocale = 'en-US'
|
||||
render(<Description />)
|
||||
|
||||
// For en-US, the order should be Discover ... in Dify Marketplace
|
||||
// The "in" text should only appear once at the end
|
||||
@ -303,8 +306,9 @@ describe('Description', () => {
|
||||
expect(inIndex).toBeLessThan(marketplaceIndex)
|
||||
})
|
||||
|
||||
it('should render zh-Hans with proper word order', async () => {
|
||||
render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render zh-Hans with proper word order', () => {
|
||||
mockDefaultLocale = 'zh-Hans'
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
const content = subheading.textContent || ''
|
||||
@ -323,58 +327,58 @@ describe('Description', () => {
|
||||
// Category Styling Tests
|
||||
// ================================
|
||||
describe('Category Styling', () => {
|
||||
it('should apply underline effect with after pseudo-element styling', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should apply underline effect with after pseudo-element styling', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const categorySpan = container.querySelector('.after\\:absolute')
|
||||
expect(categorySpan).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply correct after pseudo-element classes', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should apply correct after pseudo-element classes', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
// Check for the specific after pseudo-element classes
|
||||
const categorySpans = container.querySelectorAll('.after\\:bottom-\\[1\\.5px\\]')
|
||||
expect(categorySpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should apply full width to after element', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should apply full width to after element', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const categorySpans = container.querySelectorAll('.after\\:w-full')
|
||||
expect(categorySpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should apply correct height to after element', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should apply correct height to after element', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const categorySpans = container.querySelectorAll('.after\\:h-2')
|
||||
expect(categorySpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should apply bg-text-text-selected to after element', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should apply bg-text-text-selected to after element', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const categorySpans = container.querySelectorAll('.after\\:bg-text-text-selected')
|
||||
expect(categorySpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should have z-index 1 on category spans', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should have z-index 1 on category spans', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const categorySpans = container.querySelectorAll('.z-\\[1\\]')
|
||||
expect(categorySpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should apply left margin to category spans', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should apply left margin to category spans', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
const categorySpans = container.querySelectorAll('.ml-1')
|
||||
expect(categorySpans.length).toBeGreaterThanOrEqual(7)
|
||||
})
|
||||
|
||||
it('should apply both left and right margin to specific spans', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should apply both left and right margin to specific spans', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
// Extensions and Bundles spans have both ml-1 and mr-1
|
||||
const extensionsBundlesSpans = container.querySelectorAll('.ml-1.mr-1')
|
||||
@ -386,28 +390,17 @@ describe('Description', () => {
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty props object', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render fragment as root element', async () => {
|
||||
const { container } = render(await Description({}))
|
||||
it('should render fragment as root element', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
// Fragment renders h1 and h2 as direct children
|
||||
expect(container.querySelector('h1')).toBeInTheDocument()
|
||||
expect(container.querySelector('h2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle locale prop with undefined value', async () => {
|
||||
render(await Description({ locale: undefined }))
|
||||
|
||||
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle zh-Hant as non-Chinese simplified', async () => {
|
||||
render(await Description({ locale: 'zh-Hant' }))
|
||||
it('should handle zh-Hant as non-Chinese simplified', () => {
|
||||
mockDefaultLocale = 'zh-Hant'
|
||||
render(<Description />)
|
||||
|
||||
// zh-Hant is different from zh-Hans, should use non-Chinese format
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
@ -426,8 +419,8 @@ describe('Description', () => {
|
||||
// Content Structure Tests
|
||||
// ================================
|
||||
describe('Content Structure', () => {
|
||||
it('should have comma separators between categories', async () => {
|
||||
render(await Description({}))
|
||||
it('should have comma separators between categories', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
const content = subheading.textContent || ''
|
||||
@ -436,8 +429,8 @@ describe('Description', () => {
|
||||
expect(content).toMatch(/Models[^\n\r,\u2028\u2029]*,.*Tools[^\n\r,\u2028\u2029]*,.*Data Sources[^\n\r,\u2028\u2029]*,.*Triggers[^\n\r,\u2028\u2029]*,.*Agent Strategies[^\n\r,\u2028\u2029]*,.*Extensions/)
|
||||
})
|
||||
|
||||
it('should have "and" before last category (Bundles)', async () => {
|
||||
render(await Description({}))
|
||||
it('should have "and" before last category (Bundles)', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
const content = subheading.textContent || ''
|
||||
@ -449,8 +442,9 @@ describe('Description', () => {
|
||||
expect(andIndex).toBeLessThan(bundlesIndex)
|
||||
})
|
||||
|
||||
it('should render all text elements in correct order for en-US', async () => {
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
it('should render all text elements in correct order for en-US', () => {
|
||||
mockDefaultLocale = 'en-US'
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
const content = subheading.textContent || ''
|
||||
@ -477,8 +471,9 @@ describe('Description', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should render all text elements in correct order for zh-Hans', async () => {
|
||||
render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render all text elements in correct order for zh-Hans', () => {
|
||||
mockDefaultLocale = 'zh-Hans'
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
const content = subheading.textContent || ''
|
||||
@ -499,82 +494,48 @@ describe('Description', () => {
|
||||
// Layout Tests
|
||||
// ================================
|
||||
describe('Layout', () => {
|
||||
it('should have shrink-0 on h1 heading', async () => {
|
||||
render(await Description({}))
|
||||
it('should have shrink-0 on h1 heading', () => {
|
||||
render(<Description />)
|
||||
|
||||
const heading = screen.getByRole('heading', { level: 1 })
|
||||
expect(heading).toHaveClass('shrink-0')
|
||||
})
|
||||
|
||||
it('should have shrink-0 on h2 subheading', async () => {
|
||||
render(await Description({}))
|
||||
it('should have shrink-0 on h2 subheading', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toHaveClass('shrink-0')
|
||||
})
|
||||
|
||||
it('should have flex layout on h2', async () => {
|
||||
render(await Description({}))
|
||||
it('should have flex layout on h2', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toHaveClass('flex')
|
||||
})
|
||||
|
||||
it('should have items-center on h2', async () => {
|
||||
render(await Description({}))
|
||||
it('should have items-center on h2', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toHaveClass('items-center')
|
||||
})
|
||||
|
||||
it('should have justify-center on h2', async () => {
|
||||
render(await Description({}))
|
||||
it('should have justify-center on h2', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toHaveClass('justify-center')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Translation Function Tests
|
||||
// ================================
|
||||
describe('Translation Functions', () => {
|
||||
it('should call getTranslation for plugin namespace', async () => {
|
||||
const { getTranslation } = await import('@/i18n-config/server')
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
|
||||
expect(getTranslation).toHaveBeenCalledWith('en-US', 'plugin')
|
||||
})
|
||||
|
||||
it('should call getTranslation for common namespace', async () => {
|
||||
const { getTranslation } = await import('@/i18n-config/server')
|
||||
render(await Description({ locale: 'en-US' }))
|
||||
|
||||
expect(getTranslation).toHaveBeenCalledWith('en-US', 'common')
|
||||
})
|
||||
|
||||
it('should call getLocaleOnServer when locale prop is undefined', async () => {
|
||||
const { getLocaleOnServer } = await import('@/i18n-config/server')
|
||||
render(await Description({}))
|
||||
|
||||
expect(getLocaleOnServer).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use locale prop when provided', async () => {
|
||||
const { getTranslation } = await import('@/i18n-config/server')
|
||||
render(await Description({ locale: 'ja-JP' }))
|
||||
|
||||
expect(getTranslation).toHaveBeenCalledWith('ja-JP', 'plugin')
|
||||
expect(getTranslation).toHaveBeenCalledWith('ja-JP', 'common')
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Accessibility Tests
|
||||
// ================================
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper heading hierarchy', async () => {
|
||||
render(await Description({}))
|
||||
it('should have proper heading hierarchy', () => {
|
||||
render(<Description />)
|
||||
|
||||
const h1 = screen.getByRole('heading', { level: 1 })
|
||||
const h2 = screen.getByRole('heading', { level: 2 })
|
||||
@ -583,22 +544,22 @@ describe('Description', () => {
|
||||
expect(h2).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have readable text content', async () => {
|
||||
render(await Description({}))
|
||||
it('should have readable text content', () => {
|
||||
render(<Description />)
|
||||
|
||||
const h1 = screen.getByRole('heading', { level: 1 })
|
||||
expect(h1.textContent).not.toBe('')
|
||||
})
|
||||
|
||||
it('should have visible h1 heading', async () => {
|
||||
render(await Description({}))
|
||||
it('should have visible h1 heading', () => {
|
||||
render(<Description />)
|
||||
|
||||
const heading = screen.getByRole('heading', { level: 1 })
|
||||
expect(heading).toBeVisible()
|
||||
})
|
||||
|
||||
it('should have visible h2 heading', async () => {
|
||||
render(await Description({}))
|
||||
it('should have visible h2 heading', () => {
|
||||
render(<Description />)
|
||||
|
||||
const subheading = screen.getByRole('heading', { level: 2 })
|
||||
expect(subheading).toBeVisible()
|
||||
@ -615,8 +576,8 @@ describe('Description Integration', () => {
|
||||
mockDefaultLocale = 'en-US'
|
||||
})
|
||||
|
||||
it('should render complete component structure', async () => {
|
||||
const { container } = render(await Description({ locale: 'en-US' }))
|
||||
it('should render complete component structure', () => {
|
||||
const { container } = render(<Description />)
|
||||
|
||||
// Main headings
|
||||
expect(container.querySelector('h1')).toBeInTheDocument()
|
||||
@ -627,8 +588,9 @@ describe('Description Integration', () => {
|
||||
expect(categorySpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should render complete zh-Hans structure', async () => {
|
||||
const { container } = render(await Description({ locale: 'zh-Hans' }))
|
||||
it('should render complete zh-Hans structure', () => {
|
||||
mockDefaultLocale = 'zh-Hans'
|
||||
const { container } = render(<Description />)
|
||||
|
||||
// Main headings
|
||||
expect(container.querySelector('h1')).toBeInTheDocument()
|
||||
@ -639,14 +601,16 @@ describe('Description Integration', () => {
|
||||
expect(categorySpans.length).toBe(7)
|
||||
})
|
||||
|
||||
it('should correctly switch between zh-Hans and en-US layouts', async () => {
|
||||
it('should correctly differentiate between zh-Hans and en-US layouts', () => {
|
||||
// Render en-US
|
||||
const { container: enContainer, unmount: unmountEn } = render(await Description({ locale: 'en-US' }))
|
||||
mockDefaultLocale = 'en-US'
|
||||
const { container: enContainer, unmount: unmountEn } = render(<Description />)
|
||||
const enContent = enContainer.querySelector('h2')?.textContent || ''
|
||||
unmountEn()
|
||||
|
||||
// Render zh-Hans
|
||||
const { container: zhContainer } = render(await Description({ locale: 'zh-Hans' }))
|
||||
mockDefaultLocale = 'zh-Hans'
|
||||
const { container: zhContainer } = render(<Description />)
|
||||
const zhContent = zhContainer.querySelector('h2')?.textContent || ''
|
||||
|
||||
// Both should have all categories
|
||||
@ -666,14 +630,16 @@ describe('Description Integration', () => {
|
||||
expect(zhMarketplaceIndex).toBeLessThan(zhDiscoverIndex)
|
||||
})
|
||||
|
||||
it('should maintain consistent styling across locales', async () => {
|
||||
it('should maintain consistent styling across locales', () => {
|
||||
// Render en-US
|
||||
const { container: enContainer, unmount: unmountEn } = render(await Description({ locale: 'en-US' }))
|
||||
mockDefaultLocale = 'en-US'
|
||||
const { container: enContainer, unmount: unmountEn } = render(<Description />)
|
||||
const enCategoryCount = enContainer.querySelectorAll('.body-md-medium').length
|
||||
unmountEn()
|
||||
|
||||
// Render zh-Hans
|
||||
const { container: zhContainer } = render(await Description({ locale: 'zh-Hans' }))
|
||||
mockDefaultLocale = 'zh-Hans'
|
||||
const { container: zhContainer } = render(<Description />)
|
||||
const zhCategoryCount = zhContainer.querySelectorAll('.body-md-medium').length
|
||||
|
||||
// Both should have same number of styled category spans
|
||||
|
||||
@ -1,17 +1,11 @@
|
||||
/* eslint-disable dify-i18n/require-ns-option */
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
|
||||
type DescriptionProps = {
|
||||
locale?: Locale
|
||||
}
|
||||
const Description = async ({
|
||||
locale: localeFromProps,
|
||||
}: DescriptionProps) => {
|
||||
const localeDefault = await getLocaleOnServer()
|
||||
const { t } = await getTranslation(localeFromProps || localeDefault, 'plugin')
|
||||
const { t: tCommon } = await getTranslation(localeFromProps || localeDefault, 'common')
|
||||
const isZhHans = localeFromProps === 'zh-Hans'
|
||||
const Description = () => {
|
||||
const { t } = useTranslation('plugin')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const locale = useLocale()
|
||||
|
||||
const isZhHans = locale === 'zh-Hans'
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -42,7 +42,7 @@ const Marketplace = async ({
|
||||
scrollContainerId={scrollContainerId}
|
||||
showSearchParams={showSearchParams}
|
||||
>
|
||||
<Description locale={locale} />
|
||||
<Description />
|
||||
<StickySearchAndSwitchWrapper
|
||||
locale={locale}
|
||||
pluginTypeSwitchClassName={pluginTypeSwitchClassName}
|
||||
|
||||
@ -550,6 +550,7 @@ export const useIsNodeInLoop = (loopId: string) => {
|
||||
return false
|
||||
|
||||
if (node.parentId === loopId)
|
||||
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
@ -5,14 +5,9 @@ import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import type { QuestionClassifierNodeType } from '../nodes/question-classifier/types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
} from '../types'
|
||||
import type { Edge, Node } from '../types'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import {
|
||||
getConnectedEdges,
|
||||
} from 'reactflow'
|
||||
import { getConnectedEdges } from 'reactflow'
|
||||
import { getIterationStartNode, getLoopStartNode } from '@/app/components/workflow/utils/node'
|
||||
import { correctModelProvider } from '@/utils'
|
||||
import {
|
||||
@ -24,22 +19,22 @@ import {
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from '../constants'
|
||||
import {
|
||||
CUSTOM_GROUP_NODE,
|
||||
GROUP_CHILDREN_Z_INDEX,
|
||||
} from '../custom-group-node'
|
||||
import { CUSTOM_GROUP_NODE, GROUP_CHILDREN_Z_INDEX } from '../custom-group-node'
|
||||
import { branchNameCorrect } from '../nodes/if-else/utils'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
||||
import {
|
||||
BlockEnum,
|
||||
ErrorHandleMode,
|
||||
} from '../types'
|
||||
import { BlockEnum, ErrorHandleMode } from '../types'
|
||||
|
||||
const WHITE = 'WHITE'
|
||||
const GRAY = 'GRAY'
|
||||
const BLACK = 'BLACK'
|
||||
const isCyclicUtil = (nodeId: string, color: Record<string, string>, adjList: Record<string, string[]>, stack: string[]) => {
|
||||
|
||||
const isCyclicUtil = (
|
||||
nodeId: string,
|
||||
color: Record<string, string>,
|
||||
adjList: Record<string, string[]>,
|
||||
stack: string[],
|
||||
) => {
|
||||
color[nodeId] = GRAY
|
||||
stack.push(nodeId)
|
||||
|
||||
@ -50,8 +45,12 @@ const isCyclicUtil = (nodeId: string, color: Record<string, string>, adjList: Re
|
||||
stack.push(childId)
|
||||
return true
|
||||
}
|
||||
if (color[childId] === WHITE && isCyclicUtil(childId, color, adjList, stack))
|
||||
if (
|
||||
color[childId] === WHITE
|
||||
&& isCyclicUtil(childId, color, adjList, stack)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
color[nodeId] = BLACK
|
||||
if (stack.length > 0 && stack[stack.length - 1] === nodeId)
|
||||
@ -69,8 +68,7 @@ const getCycleEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
adjList[node.id] = []
|
||||
}
|
||||
|
||||
for (const edge of edges)
|
||||
adjList[edge.source]?.push(edge.target)
|
||||
for (const edge of edges) adjList[edge.source]?.push(edge.target)
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (color[nodes[i].id] === WHITE)
|
||||
@ -90,22 +88,34 @@ const getCycleEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
}
|
||||
|
||||
export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
const hasIterationNode = nodes.some(node => node.data.type === BlockEnum.Iteration)
|
||||
const hasIterationNode = nodes.some(
|
||||
node => node.data.type === BlockEnum.Iteration,
|
||||
)
|
||||
const hasLoopNode = nodes.some(node => node.data.type === BlockEnum.Loop)
|
||||
const hasGroupNode = nodes.some(node => node.type === CUSTOM_GROUP_NODE)
|
||||
const hasBusinessGroupNode = nodes.some(node => node.data.type === BlockEnum.Group)
|
||||
const hasBusinessGroupNode = nodes.some(
|
||||
node => node.data.type === BlockEnum.Group,
|
||||
)
|
||||
|
||||
if (!hasIterationNode && !hasLoopNode && !hasGroupNode && !hasBusinessGroupNode) {
|
||||
if (
|
||||
!hasIterationNode
|
||||
&& !hasLoopNode
|
||||
&& !hasGroupNode
|
||||
&& !hasBusinessGroupNode
|
||||
) {
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
|
||||
const nodesMap = nodes.reduce((prev, next) => {
|
||||
prev[next.id] = next
|
||||
return prev
|
||||
}, {} as Record<string, Node>)
|
||||
const nodesMap = nodes.reduce(
|
||||
(prev, next) => {
|
||||
prev[next.id] = next
|
||||
return prev
|
||||
},
|
||||
{} as Record<string, Node>,
|
||||
)
|
||||
|
||||
const iterationNodesWithStartNode = []
|
||||
const iterationNodesWithoutStartNode = []
|
||||
@ -117,8 +127,12 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
if (currentNode.data.type === BlockEnum.Iteration) {
|
||||
if (currentNode.data.start_node_id) {
|
||||
if (nodesMap[currentNode.data.start_node_id]?.type !== CUSTOM_ITERATION_START_NODE)
|
||||
if (
|
||||
nodesMap[currentNode.data.start_node_id]?.type
|
||||
!== CUSTOM_ITERATION_START_NODE
|
||||
) {
|
||||
iterationNodesWithStartNode.push(currentNode)
|
||||
}
|
||||
}
|
||||
else {
|
||||
iterationNodesWithoutStartNode.push(currentNode)
|
||||
@ -127,8 +141,12 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
if (currentNode.data.type === BlockEnum.Loop) {
|
||||
if (currentNode.data.start_node_id) {
|
||||
if (nodesMap[currentNode.data.start_node_id]?.type !== CUSTOM_LOOP_START_NODE)
|
||||
if (
|
||||
nodesMap[currentNode.data.start_node_id]?.type
|
||||
!== CUSTOM_LOOP_START_NODE
|
||||
) {
|
||||
loopNodesWithStartNode.push(currentNode)
|
||||
}
|
||||
}
|
||||
else {
|
||||
loopNodesWithoutStartNode.push(currentNode)
|
||||
@ -137,7 +155,10 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
}
|
||||
|
||||
const newIterationStartNodesMap = {} as Record<string, Node>
|
||||
const newIterationStartNodes = [...iterationNodesWithStartNode, ...iterationNodesWithoutStartNode].map((iterationNode, index) => {
|
||||
const newIterationStartNodes = [
|
||||
...iterationNodesWithStartNode,
|
||||
...iterationNodesWithoutStartNode,
|
||||
].map((iterationNode, index) => {
|
||||
const newNode = getIterationStartNode(iterationNode.id)
|
||||
newNode.id = newNode.id + index
|
||||
newIterationStartNodesMap[iterationNode.id] = newNode
|
||||
@ -145,24 +166,34 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
})
|
||||
|
||||
const newLoopStartNodesMap = {} as Record<string, Node>
|
||||
const newLoopStartNodes = [...loopNodesWithStartNode, ...loopNodesWithoutStartNode].map((loopNode, index) => {
|
||||
const newLoopStartNodes = [
|
||||
...loopNodesWithStartNode,
|
||||
...loopNodesWithoutStartNode,
|
||||
].map((loopNode, index) => {
|
||||
const newNode = getLoopStartNode(loopNode.id)
|
||||
newNode.id = newNode.id + index
|
||||
newLoopStartNodesMap[loopNode.id] = newNode
|
||||
return newNode
|
||||
})
|
||||
|
||||
const newEdges = [...iterationNodesWithStartNode, ...loopNodesWithStartNode].map((nodeItem) => {
|
||||
const newEdges = [
|
||||
...iterationNodesWithStartNode,
|
||||
...loopNodesWithStartNode,
|
||||
].map((nodeItem) => {
|
||||
const isIteration = nodeItem.data.type === BlockEnum.Iteration
|
||||
const newNode = (isIteration ? newIterationStartNodesMap : newLoopStartNodesMap)[nodeItem.id]
|
||||
const newNode = (
|
||||
isIteration ? newIterationStartNodesMap : newLoopStartNodesMap
|
||||
)[nodeItem.id]
|
||||
const startNode = nodesMap[nodeItem.data.start_node_id]
|
||||
const source = newNode.id
|
||||
const sourceHandle = 'source'
|
||||
const target = startNode.id
|
||||
const targetHandle = 'target'
|
||||
|
||||
const parentNode = nodes.find(node => node.id === startNode.parentId) || null
|
||||
const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration
|
||||
const parentNode
|
||||
= nodes.find(node => node.id === startNode.parentId) || null
|
||||
const isInIteration
|
||||
= !!parentNode && parentNode.data.type === BlockEnum.Iteration
|
||||
const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop
|
||||
|
||||
return {
|
||||
@ -185,11 +216,18 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
}
|
||||
})
|
||||
nodes.forEach((node) => {
|
||||
if (node.data.type === BlockEnum.Iteration && newIterationStartNodesMap[node.id])
|
||||
(node.data as IterationNodeType).start_node_id = newIterationStartNodesMap[node.id].id
|
||||
if (
|
||||
node.data.type === BlockEnum.Iteration
|
||||
&& newIterationStartNodesMap[node.id]
|
||||
) {
|
||||
(node.data as IterationNodeType).start_node_id
|
||||
= newIterationStartNodesMap[node.id].id
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.Loop && newLoopStartNodesMap[node.id])
|
||||
(node.data as LoopNodeType).start_node_id = newLoopStartNodesMap[node.id].id
|
||||
if (node.data.type === BlockEnum.Loop && newLoopStartNodesMap[node.id]) {
|
||||
(node.data as LoopNodeType).start_node_id
|
||||
= newLoopStartNodesMap[node.id].id
|
||||
}
|
||||
})
|
||||
|
||||
// Derive Group internal edges (input → entries, leaves → exits)
|
||||
@ -240,7 +278,7 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
targetHandle: 'target',
|
||||
data: {
|
||||
sourceType: leafNode.data.type,
|
||||
targetType: '' as any, // Exit port has empty type
|
||||
targetType: '' as string, // Exit port has empty type
|
||||
_isGroupInternal: true,
|
||||
_groupId: groupNode.id,
|
||||
},
|
||||
@ -260,7 +298,12 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
return
|
||||
|
||||
const groupData = groupNode.data as GroupNodeData
|
||||
const { members = [], headNodeIds = [], leafNodeIds = [], handlers = [] } = groupData
|
||||
const {
|
||||
members = [],
|
||||
headNodeIds = [],
|
||||
leafNodeIds = [],
|
||||
handlers = [],
|
||||
} = groupData
|
||||
const memberSet = new Set(members.map(m => m.id))
|
||||
const headSet = new Set(headNodeIds)
|
||||
const leafSet = new Set(leafNodeIds)
|
||||
@ -293,7 +336,8 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
if (leafSet.has(edge.source) && !memberSet.has(edge.target)) {
|
||||
const edgeSourceHandle = edge.sourceHandle || 'source'
|
||||
const handler = handlers.find(
|
||||
h => h.nodeId === edge.source && h.sourceHandle === edgeSourceHandle,
|
||||
h =>
|
||||
h.nodeId === edge.source && h.sourceHandle === edgeSourceHandle,
|
||||
)
|
||||
if (handler) {
|
||||
groupTempEdges.push({
|
||||
@ -321,7 +365,10 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
}
|
||||
|
||||
export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
|
||||
const { nodes, edges } = preprocessNodesAndEdges(
|
||||
cloneDeep(originNodes),
|
||||
cloneDeep(originEdges),
|
||||
)
|
||||
const firstNode = nodes[0]
|
||||
|
||||
if (!firstNode?.position) {
|
||||
@ -333,23 +380,35 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
const iterationOrLoopNodeMap = nodes.reduce((acc, node) => {
|
||||
if (node.parentId) {
|
||||
if (acc[node.parentId])
|
||||
acc[node.parentId].push({ nodeId: node.id, nodeType: node.data.type })
|
||||
else
|
||||
acc[node.parentId] = [{ nodeId: node.id, nodeType: node.data.type }]
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, { nodeId: string, nodeType: BlockEnum }[]>)
|
||||
const iterationOrLoopNodeMap = nodes.reduce(
|
||||
(acc, node) => {
|
||||
if (node.parentId) {
|
||||
if (acc[node.parentId]) {
|
||||
acc[node.parentId].push({
|
||||
nodeId: node.id,
|
||||
nodeType: node.data.type,
|
||||
})
|
||||
}
|
||||
else {
|
||||
acc[node.parentId] = [{ nodeId: node.id, nodeType: node.data.type }]
|
||||
}
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, { nodeId: string, nodeType: BlockEnum }[]>,
|
||||
)
|
||||
|
||||
return nodes.map((node) => {
|
||||
if (!node.type)
|
||||
node.type = CUSTOM_NODE
|
||||
|
||||
const connectedEdges = getConnectedEdges([node], edges)
|
||||
node.data._connectedSourceHandleIds = connectedEdges.filter(edge => edge.source === node.id).map(edge => edge.sourceHandle || 'source')
|
||||
node.data._connectedTargetHandleIds = connectedEdges.filter(edge => edge.target === node.id).map(edge => edge.targetHandle || 'target')
|
||||
node.data._connectedSourceHandleIds = connectedEdges
|
||||
.filter(edge => edge.source === node.id)
|
||||
.map(edge => edge.sourceHandle || 'source')
|
||||
node.data._connectedTargetHandleIds = connectedEdges
|
||||
.filter(edge => edge.target === node.id)
|
||||
.map(edge => edge.targetHandle || 'target')
|
||||
|
||||
if (node.data.type === BlockEnum.IfElse) {
|
||||
const nodeData = node.data as IfElseNodeType
|
||||
@ -364,18 +423,27 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
]
|
||||
}
|
||||
node.data._targetBranches = branchNameCorrect([
|
||||
...(node.data as IfElseNodeType).cases.map(item => ({ id: item.case_id, name: '' })),
|
||||
...(node.data as IfElseNodeType).cases.map(item => ({
|
||||
id: item.case_id,
|
||||
name: '',
|
||||
})),
|
||||
{ id: 'false', name: '' },
|
||||
])
|
||||
// delete conditions and logical_operator if cases is not empty
|
||||
if (nodeData.cases.length > 0 && nodeData.conditions && nodeData.logical_operator) {
|
||||
if (
|
||||
nodeData.cases.length > 0
|
||||
&& nodeData.conditions
|
||||
&& nodeData.logical_operator
|
||||
) {
|
||||
delete nodeData.conditions
|
||||
delete nodeData.logical_operator
|
||||
}
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.QuestionClassifier) {
|
||||
node.data._targetBranches = (node.data as QuestionClassifierNodeType).classes.map((topic) => {
|
||||
node.data._targetBranches = (
|
||||
node.data as QuestionClassifierNodeType
|
||||
).classes.map((topic) => {
|
||||
return topic
|
||||
})
|
||||
}
|
||||
@ -395,28 +463,46 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
iterationNodeData._children = iterationOrLoopNodeMap[node.id] || []
|
||||
iterationNodeData.is_parallel = iterationNodeData.is_parallel || false
|
||||
iterationNodeData.parallel_nums = iterationNodeData.parallel_nums || 10
|
||||
iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated
|
||||
iterationNodeData.error_handle_mode
|
||||
= iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated
|
||||
}
|
||||
|
||||
// TODO: loop error handle mode
|
||||
if (node.data.type === BlockEnum.Loop) {
|
||||
const loopNodeData = node.data as LoopNodeType
|
||||
loopNodeData._children = iterationOrLoopNodeMap[node.id] || []
|
||||
loopNodeData.error_handle_mode = loopNodeData.error_handle_mode || ErrorHandleMode.Terminated
|
||||
loopNodeData.error_handle_mode
|
||||
= loopNodeData.error_handle_mode || ErrorHandleMode.Terminated
|
||||
}
|
||||
|
||||
// legacy provider handle
|
||||
if (node.data.type === BlockEnum.LLM)
|
||||
(node as any).data.model.provider = correctModelProvider((node as any).data.model.provider)
|
||||
if (node.data.type === BlockEnum.LLM) {
|
||||
(node as any).data.model.provider = correctModelProvider(
|
||||
(node as any).data.model.provider,
|
||||
)
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.KnowledgeRetrieval && (node as any).data.multiple_retrieval_config?.reranking_model)
|
||||
(node as any).data.multiple_retrieval_config.reranking_model.provider = correctModelProvider((node as any).data.multiple_retrieval_config?.reranking_model.provider)
|
||||
if (
|
||||
node.data.type === BlockEnum.KnowledgeRetrieval
|
||||
&& (node as any).data.multiple_retrieval_config?.reranking_model
|
||||
) {
|
||||
(node as any).data.multiple_retrieval_config.reranking_model.provider
|
||||
= correctModelProvider(
|
||||
(node as any).data.multiple_retrieval_config?.reranking_model.provider,
|
||||
)
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.QuestionClassifier)
|
||||
(node as any).data.model.provider = correctModelProvider((node as any).data.model.provider)
|
||||
if (node.data.type === BlockEnum.QuestionClassifier) {
|
||||
(node as any).data.model.provider = correctModelProvider(
|
||||
(node as any).data.model.provider,
|
||||
)
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.ParameterExtractor)
|
||||
(node as any).data.model.provider = correctModelProvider((node as any).data.model.provider)
|
||||
if (node.data.type === BlockEnum.ParameterExtractor) {
|
||||
(node as any).data.model.provider = correctModelProvider(
|
||||
(node as any).data.model.provider,
|
||||
)
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) {
|
||||
node.data.retry_config = {
|
||||
@ -426,14 +512,21 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version && !(node as Node<ToolNodeType>).data.tool_node_version) {
|
||||
if (
|
||||
node.data.type === BlockEnum.Tool
|
||||
&& !(node as Node<ToolNodeType>).data.version
|
||||
&& !(node as Node<ToolNodeType>).data.tool_node_version
|
||||
) {
|
||||
(node as Node<ToolNodeType>).data.tool_node_version = '2'
|
||||
|
||||
const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations
|
||||
if (toolConfigurations && Object.keys(toolConfigurations).length > 0) {
|
||||
const newValues = { ...toolConfigurations }
|
||||
Object.keys(toolConfigurations).forEach((key) => {
|
||||
if (typeof toolConfigurations[key] !== 'object' || toolConfigurations[key] === null) {
|
||||
if (
|
||||
typeof toolConfigurations[key] !== 'object'
|
||||
|| toolConfigurations[key] === null
|
||||
) {
|
||||
newValues[key] = {
|
||||
type: 'constant',
|
||||
value: toolConfigurations[key],
|
||||
@ -449,50 +542,62 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
}
|
||||
|
||||
export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
|
||||
const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
|
||||
const { nodes, edges } = preprocessNodesAndEdges(
|
||||
cloneDeep(originNodes),
|
||||
cloneDeep(originEdges),
|
||||
)
|
||||
let selectedNode: Node | null = null
|
||||
const nodesMap = nodes.reduce((acc, node) => {
|
||||
acc[node.id] = node
|
||||
const nodesMap = nodes.reduce(
|
||||
(acc, node) => {
|
||||
acc[node.id] = node
|
||||
|
||||
if (node.data?.selected)
|
||||
selectedNode = node
|
||||
if (node.data?.selected)
|
||||
selectedNode = node
|
||||
|
||||
return acc
|
||||
}, {} as Record<string, Node>)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Node>,
|
||||
)
|
||||
|
||||
const cycleEdges = getCycleEdges(nodes, edges)
|
||||
return edges.filter((edge) => {
|
||||
return !cycleEdges.find(cycEdge => cycEdge.source === edge.source && cycEdge.target === edge.target)
|
||||
}).map((edge) => {
|
||||
edge.type = 'custom'
|
||||
return edges
|
||||
.filter((edge) => {
|
||||
return !cycleEdges.find(
|
||||
cycEdge =>
|
||||
cycEdge.source === edge.source && cycEdge.target === edge.target,
|
||||
)
|
||||
})
|
||||
.map((edge) => {
|
||||
edge.type = 'custom'
|
||||
|
||||
if (!edge.sourceHandle)
|
||||
edge.sourceHandle = 'source'
|
||||
if (!edge.sourceHandle)
|
||||
edge.sourceHandle = 'source'
|
||||
|
||||
if (!edge.targetHandle)
|
||||
edge.targetHandle = 'target'
|
||||
if (!edge.targetHandle)
|
||||
edge.targetHandle = 'target'
|
||||
|
||||
if (!edge.data?.sourceType && edge.source && nodesMap[edge.source]) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
sourceType: nodesMap[edge.source].data.type!,
|
||||
} as any
|
||||
}
|
||||
if (!edge.data?.sourceType && edge.source && nodesMap[edge.source]) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
sourceType: nodesMap[edge.source].data.type!,
|
||||
} as any
|
||||
}
|
||||
|
||||
if (!edge.data?.targetType && edge.target && nodesMap[edge.target]) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
targetType: nodesMap[edge.target].data.type!,
|
||||
} as any
|
||||
}
|
||||
if (!edge.data?.targetType && edge.target && nodesMap[edge.target]) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
targetType: nodesMap[edge.target].data.type!,
|
||||
} as any
|
||||
}
|
||||
|
||||
if (selectedNode) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_connectedNodeIsSelected: edge.source === selectedNode.id || edge.target === selectedNode.id,
|
||||
} as any
|
||||
}
|
||||
if (selectedNode) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_connectedNodeIsSelected:
|
||||
edge.source === selectedNode.id || edge.target === selectedNode.id,
|
||||
} as any
|
||||
}
|
||||
|
||||
return edge
|
||||
})
|
||||
return edge
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import noAsAnyInT from './rules/no-as-any-in-t.js'
|
||||
import noExtraKeys from './rules/no-extra-keys.js'
|
||||
import noLegacyNamespacePrefix from './rules/no-legacy-namespace-prefix.js'
|
||||
import requireNsOption from './rules/require-ns-option.js'
|
||||
import validI18nKeys from './rules/valid-i18n-keys.js'
|
||||
|
||||
/** @type {import('eslint').ESLint.Plugin} */
|
||||
const plugin = {
|
||||
@ -10,8 +12,10 @@ const plugin = {
|
||||
},
|
||||
rules: {
|
||||
'no-as-any-in-t': noAsAnyInT,
|
||||
'no-extra-keys': noExtraKeys,
|
||||
'no-legacy-namespace-prefix': noLegacyNamespacePrefix,
|
||||
'require-ns-option': requireNsOption,
|
||||
'valid-i18n-keys': validI18nKeys,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
70
web/eslint-rules/rules/no-extra-keys.js
Normal file
70
web/eslint-rules/rules/no-extra-keys.js
Normal file
@ -0,0 +1,70 @@
|
||||
import fs from 'node:fs'
|
||||
import path, { normalize, sep } from 'node:path'
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure non-English JSON files don\'t have extra keys not present in en-US',
|
||||
},
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Program(node) {
|
||||
const { filename, sourceCode } = context
|
||||
|
||||
if (!filename.endsWith('.json'))
|
||||
return
|
||||
|
||||
const parts = normalize(filename).split(sep)
|
||||
// e.g., i18n/ar-TN/common.json -> jsonFile = common.json, lang = ar-TN
|
||||
const jsonFile = parts.at(-1)
|
||||
const lang = parts.at(-2)
|
||||
|
||||
// Skip English files
|
||||
if (lang === 'en-US')
|
||||
return
|
||||
|
||||
let currentJson = {}
|
||||
let englishJson = {}
|
||||
|
||||
try {
|
||||
currentJson = JSON.parse(sourceCode.text)
|
||||
// Look for the same filename in en-US folder
|
||||
// e.g., i18n/ar-TN/common.json -> i18n/en-US/common.json
|
||||
const englishFilePath = path.join(path.dirname(filename), '..', 'en-US', jsonFile ?? '')
|
||||
englishJson = JSON.parse(fs.readFileSync(englishFilePath, 'utf8'))
|
||||
}
|
||||
catch (error) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const extraKeys = Object.keys(currentJson).filter(
|
||||
key => !Object.prototype.hasOwnProperty.call(englishJson, key),
|
||||
)
|
||||
|
||||
for (const key of extraKeys) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Key "${key}" is present in ${lang}/${jsonFile} but not in en-US/${jsonFile}`,
|
||||
fix(fixer) {
|
||||
const newJson = Object.fromEntries(
|
||||
Object.entries(currentJson).filter(([k]) => !extraKeys.includes(k)),
|
||||
)
|
||||
|
||||
const newText = `${JSON.stringify(newJson, null, 2)}\n`
|
||||
|
||||
return fixer.replaceText(node, newText)
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
61
web/eslint-rules/rules/valid-i18n-keys.js
Normal file
61
web/eslint-rules/rules/valid-i18n-keys.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { cleanJsonText } from '../utils.js'
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure i18n JSON keys are flat and valid as object paths',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Program(node) {
|
||||
const { filename, sourceCode } = context
|
||||
|
||||
if (!filename.endsWith('.json'))
|
||||
return
|
||||
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(cleanJsonText(sourceCode.text))
|
||||
}
|
||||
catch {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Invalid JSON format',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const keys = Object.keys(json)
|
||||
const keyPrefixes = new Set()
|
||||
|
||||
for (const key of keys) {
|
||||
if (key.includes('.')) {
|
||||
const parts = key.split('.')
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const prefix = parts.slice(0, i).join('.')
|
||||
if (keys.includes(prefix)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Invalid key structure: '${key}' conflicts with '${prefix}'`,
|
||||
})
|
||||
}
|
||||
keyPrefixes.add(prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
if (keyPrefixes.has(key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Invalid key structure: '${key}' is a prefix of another key`,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
10
web/eslint-rules/utils.js
Normal file
10
web/eslint-rules/utils.js
Normal file
@ -0,0 +1,10 @@
|
||||
export const cleanJsonText = (text) => {
|
||||
const cleaned = text.replaceAll(/,\s*\}/g, '}')
|
||||
try {
|
||||
JSON.parse(cleaned)
|
||||
return cleaned
|
||||
}
|
||||
catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
@ -130,15 +130,6 @@ export default antfu(
|
||||
sonarjs: sonar,
|
||||
},
|
||||
},
|
||||
// allow generated i18n files (like i18n/*/workflow.ts) to exceed max-lines
|
||||
{
|
||||
files: ['i18n/**'],
|
||||
rules: {
|
||||
'sonarjs/max-lines': 'off',
|
||||
'max-lines': 'off',
|
||||
'jsonc/sort-keys': 'error',
|
||||
},
|
||||
},
|
||||
tailwind.configs['flat/recommended'],
|
||||
{
|
||||
settings: {
|
||||
@ -188,7 +179,22 @@ export default antfu(
|
||||
// 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
|
||||
'dify-i18n/no-as-any-in-t': 'error',
|
||||
// 'dify-i18n/no-legacy-namespace-prefix': 'error',
|
||||
'dify-i18n/require-ns-option': 'error',
|
||||
// 'dify-i18n/require-ns-option': 'error',
|
||||
},
|
||||
},
|
||||
// i18n JSON validation rules
|
||||
{
|
||||
files: ['i18n/**/*.json'],
|
||||
plugins: {
|
||||
'dify-i18n': difyI18n,
|
||||
},
|
||||
rules: {
|
||||
'sonarjs/max-lines': 'off',
|
||||
'max-lines': 'off',
|
||||
'jsonc/sort-keys': 'error',
|
||||
|
||||
'dify-i18n/valid-i18n-keys': 'error',
|
||||
'dify-i18n/no-extra-keys': 'error',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
10
web/i18n-config/lib.client.ts
Normal file
10
web/i18n-config/lib.client.ts
Normal file
@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { NamespaceCamelCase } from './i18next-config'
|
||||
import { useTranslation as useTranslationOriginal } from 'react-i18next'
|
||||
|
||||
export function useTranslation(ns?: NamespaceCamelCase) {
|
||||
return useTranslationOriginal(ns)
|
||||
}
|
||||
|
||||
export { useLocale } from '@/context/i18n'
|
||||
16
web/i18n-config/lib.server.ts
Normal file
16
web/i18n-config/lib.server.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { NamespaceCamelCase } from './i18next-config'
|
||||
import { use } from 'react'
|
||||
import { getLocaleOnServer, getTranslation } from './server'
|
||||
|
||||
async function getI18nConfig(ns?: NamespaceCamelCase) {
|
||||
const lang = await getLocaleOnServer()
|
||||
return getTranslation(lang, ns)
|
||||
}
|
||||
|
||||
export function useTranslation(ns?: NamespaceCamelCase) {
|
||||
return use(getI18nConfig(ns))
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
return use(getLocaleOnServer())
|
||||
}
|
||||
@ -2,13 +2,13 @@ import type { i18n as I18nInstance } from 'i18next'
|
||||
import type { Locale } from '.'
|
||||
import type { NamespaceCamelCase, NamespaceKebabCase } from './i18next-config'
|
||||
import { match } from '@formatjs/intl-localematcher'
|
||||
import { camelCase, kebabCase } from 'es-toolkit/compat'
|
||||
import { kebabCase } from 'es-toolkit/compat'
|
||||
import { createInstance } from 'i18next'
|
||||
import resourcesToBackend from 'i18next-resources-to-backend'
|
||||
import Negotiator from 'negotiator'
|
||||
import { cookies, headers } from 'next/headers'
|
||||
import { initReactI18next } from 'react-i18next/initReactI18next'
|
||||
import serverOnlyContext from '@/utils/server-only-context'
|
||||
import { serverOnlyContext } from '@/utils/server-only-context'
|
||||
import { i18n } from '.'
|
||||
|
||||
const [getLocaleCache, setLocaleCache] = serverOnlyContext<Locale | null>(null)
|
||||
@ -35,15 +35,14 @@ const getOrCreateI18next = async (lng: Locale) => {
|
||||
return instance
|
||||
}
|
||||
|
||||
export async function getTranslation(lng: Locale, ns: NamespaceKebabCase) {
|
||||
const camelNs = camelCase(ns) as NamespaceCamelCase
|
||||
export async function getTranslation(lng: Locale, ns?: NamespaceCamelCase) {
|
||||
const i18nextInstance = await getOrCreateI18next(lng)
|
||||
|
||||
if (!i18nextInstance.hasLoadedNamespace(camelNs))
|
||||
await i18nextInstance.loadNamespaces(camelNs)
|
||||
if (ns && !i18nextInstance.hasLoadedNamespace(ns))
|
||||
await i18nextInstance.loadNamespaces(ns)
|
||||
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(lng, camelNs),
|
||||
t: i18nextInstance.getFixedT(lng, ns),
|
||||
i18n: i18nextInstance,
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "أوقات الاتصال",
|
||||
"modelProvider.card.buyQuota": "شراء حصة",
|
||||
"modelProvider.card.callTimes": "أوقات الاتصال",
|
||||
"modelProvider.card.modelAPI": "النماذج {{modelName}} تستخدم مفتاح واجهة برمجة التطبيقات.",
|
||||
"modelProvider.card.modelNotSupported": "النماذج {{modelName}} غير مثبتة.",
|
||||
"modelProvider.card.modelSupported": "النماذج {{modelName}} تستخدم هذا الحصة.",
|
||||
"modelProvider.card.onTrial": "في التجربة",
|
||||
"modelProvider.card.paid": "مدفوع",
|
||||
"modelProvider.card.priorityUse": "أولوية الاستخدام",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "الرموز المجانية المتاحة المتبقية",
|
||||
"modelProvider.rerankModel.key": "نموذج إعادة الترتيب",
|
||||
"modelProvider.rerankModel.tip": "سيعيد نموذج إعادة الترتيب ترتيب قائمة المستندات المرشحة بناءً على المطابقة الدلالية مع استعلام المستخدم، مما يحسن نتائج الترتيب الدلالي",
|
||||
"modelProvider.resetDate": "إعادة الضبط على {{date}}",
|
||||
"modelProvider.searchModel": "نموذج البحث",
|
||||
"modelProvider.selectModel": "اختر نموذجك",
|
||||
"modelProvider.selector.emptySetting": "يرجى الانتقال إلى الإعدادات للتكوين",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Anrufzeiten",
|
||||
"modelProvider.card.buyQuota": "Kontingent kaufen",
|
||||
"modelProvider.card.callTimes": "Anrufzeiten",
|
||||
"modelProvider.card.modelAPI": "{{modelName}}-Modelle verwenden den API-Schlüssel.",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}}-Modelle sind nicht installiert.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}}-Modelle verwenden dieses Kontingent.",
|
||||
"modelProvider.card.onTrial": "In Probe",
|
||||
"modelProvider.card.paid": "Bezahlt",
|
||||
"modelProvider.card.priorityUse": "Priorisierte Nutzung",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Verbleibende verfügbare kostenlose Token",
|
||||
"modelProvider.rerankModel.key": "Rerank-Modell",
|
||||
"modelProvider.rerankModel.tip": "Rerank-Modell wird die Kandidatendokumentenliste basierend auf der semantischen Übereinstimmung mit der Benutzeranfrage neu ordnen und die Ergebnisse der semantischen Rangordnung verbessern",
|
||||
"modelProvider.resetDate": "Zurücksetzen bei {{date}}",
|
||||
"modelProvider.searchModel": "Suchmodell",
|
||||
"modelProvider.selectModel": "Wählen Sie Ihr Modell",
|
||||
"modelProvider.selector.emptySetting": "Bitte gehen Sie zu den Einstellungen, um zu konfigurieren",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Tiempos de llamada",
|
||||
"modelProvider.card.buyQuota": "Comprar Cuota",
|
||||
"modelProvider.card.callTimes": "Tiempos de llamada",
|
||||
"modelProvider.card.modelAPI": "Los modelos {{modelName}} están usando la clave de API.",
|
||||
"modelProvider.card.modelNotSupported": "Los modelos {{modelName}} no están instalados.",
|
||||
"modelProvider.card.modelSupported": "Los modelos {{modelName}} están utilizando esta cuota.",
|
||||
"modelProvider.card.onTrial": "En prueba",
|
||||
"modelProvider.card.paid": "Pagado",
|
||||
"modelProvider.card.priorityUse": "Uso prioritario",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Tokens gratuitos restantes disponibles",
|
||||
"modelProvider.rerankModel.key": "Modelo de Reordenar",
|
||||
"modelProvider.rerankModel.tip": "El modelo de reordenar reordenará la lista de documentos candidatos basada en la coincidencia semántica con la consulta del usuario, mejorando los resultados de clasificación semántica",
|
||||
"modelProvider.resetDate": "Reiniciar en {{date}}",
|
||||
"modelProvider.searchModel": "Modelo de búsqueda",
|
||||
"modelProvider.selectModel": "Selecciona tu modelo",
|
||||
"modelProvider.selector.emptySetting": "Por favor ve a configuraciones para configurar",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "تعداد فراخوانی",
|
||||
"modelProvider.card.buyQuota": "خرید سهمیه",
|
||||
"modelProvider.card.callTimes": "تعداد فراخوانی",
|
||||
"modelProvider.card.modelAPI": "مدلهای {{modelName}} در حال استفاده از کلید API هستند.",
|
||||
"modelProvider.card.modelNotSupported": "مدلهای {{modelName}} نصب نشدهاند.",
|
||||
"modelProvider.card.modelSupported": "مدلهای {{modelName}} از این سهمیه استفاده میکنند.",
|
||||
"modelProvider.card.onTrial": "در حال آزمایش",
|
||||
"modelProvider.card.paid": "پرداخت شده",
|
||||
"modelProvider.card.priorityUse": "استفاده با اولویت",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "توکنهای رایگان باقیمانده در دسترس",
|
||||
"modelProvider.rerankModel.key": "مدل رتبهبندی مجدد",
|
||||
"modelProvider.rerankModel.tip": "مدل رتبهبندی مجدد، لیست اسناد کاندید را بر اساس تطابق معنایی با پرسش کاربر مرتب میکند و نتایج رتبهبندی معنایی را بهبود میبخشد",
|
||||
"modelProvider.resetDate": "بازنشانی در {{date}}",
|
||||
"modelProvider.searchModel": "جستجوی مدل",
|
||||
"modelProvider.selectModel": "مدل خود را انتخاب کنید",
|
||||
"modelProvider.selector.emptySetting": "لطفاً به تنظیمات بروید تا پیکربندی کنید",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Temps d'appel",
|
||||
"modelProvider.card.buyQuota": "Acheter Quota",
|
||||
"modelProvider.card.callTimes": "Temps d'appel",
|
||||
"modelProvider.card.modelAPI": "Les modèles {{modelName}} utilisent la clé API.",
|
||||
"modelProvider.card.modelNotSupported": "Les modèles {{modelName}} ne sont pas installés.",
|
||||
"modelProvider.card.modelSupported": "Les modèles {{modelName}} utilisent ce quota.",
|
||||
"modelProvider.card.onTrial": "En Essai",
|
||||
"modelProvider.card.paid": "Payé",
|
||||
"modelProvider.card.priorityUse": "Utilisation prioritaire",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Tokens gratuits restants disponibles",
|
||||
"modelProvider.rerankModel.key": "Modèle de Réorganisation",
|
||||
"modelProvider.rerankModel.tip": "Le modèle de réorganisation réorganisera la liste des documents candidats en fonction de la correspondance sémantique avec la requête de l'utilisateur, améliorant ainsi les résultats du classement sémantique.",
|
||||
"modelProvider.resetDate": "Réinitialiser sur {{date}}",
|
||||
"modelProvider.searchModel": "Modèle de recherche",
|
||||
"modelProvider.selectModel": "Sélectionnez votre modèle",
|
||||
"modelProvider.selector.emptySetting": "Veuillez aller dans les paramètres pour configurer",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "कॉल समय",
|
||||
"modelProvider.card.buyQuota": "कोटा खरीदें",
|
||||
"modelProvider.card.callTimes": "कॉल समय",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} मॉडल एपीआई कुंजी का उपयोग कर रहे हैं।",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} मॉडल इंस्टॉल नहीं हैं।",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} मॉडल इस कोटा का उपयोग कर रहे हैं।",
|
||||
"modelProvider.card.onTrial": "परीक्षण पर",
|
||||
"modelProvider.card.paid": "भुगतान किया हुआ",
|
||||
"modelProvider.card.priorityUse": "प्राथमिकता उपयोग",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "बचे हुए उपलब्ध मुफ्त टोकन",
|
||||
"modelProvider.rerankModel.key": "रीरैंक मॉडल",
|
||||
"modelProvider.rerankModel.tip": "रीरैंक मॉडल उपयोगकर्ता प्रश्न के साथ सांविधिक मेल के आधार पर उम्मीदवार दस्तावेज़ सूची को पुनः क्रमित करेगा, सांविधिक रैंकिंग के परिणामों में सुधार करेगा।",
|
||||
"modelProvider.resetDate": "{{date}} पर रीसेट करें",
|
||||
"modelProvider.searchModel": "खोज मॉडल",
|
||||
"modelProvider.selectModel": "अपने मॉडल का चयन करें",
|
||||
"modelProvider.selector.emptySetting": "कॉन्फ़िगर करने के लिए कृपया सेटिंग्स पर जाएं",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Waktu panggilan",
|
||||
"modelProvider.card.buyQuota": "Beli Kuota",
|
||||
"modelProvider.card.callTimes": "Waktu panggilan",
|
||||
"modelProvider.card.modelAPI": "Model {{modelName}} sedang menggunakan API Key.",
|
||||
"modelProvider.card.modelNotSupported": "Model {{modelName}} tidak terpasang.",
|
||||
"modelProvider.card.modelSupported": "Model {{modelName}} sedang menggunakan kuota ini.",
|
||||
"modelProvider.card.onTrial": "Sedang Diadili",
|
||||
"modelProvider.card.paid": "Dibayar",
|
||||
"modelProvider.card.priorityUse": "Penggunaan prioritas",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Token gratis yang masih tersedia",
|
||||
"modelProvider.rerankModel.key": "Peringkat ulang Model",
|
||||
"modelProvider.rerankModel.tip": "Model rerank akan menyusun ulang daftar dokumen kandidat berdasarkan kecocokan semantik dengan kueri pengguna, meningkatkan hasil peringkat semantik",
|
||||
"modelProvider.resetDate": "Atur ulang pada {{date}}",
|
||||
"modelProvider.searchModel": "Model pencarian",
|
||||
"modelProvider.selectModel": "Pilih model Anda",
|
||||
"modelProvider.selector.emptySetting": "Silakan buka pengaturan untuk mengonfigurasi",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Numero di chiamate",
|
||||
"modelProvider.card.buyQuota": "Acquista Quota",
|
||||
"modelProvider.card.callTimes": "Numero di chiamate",
|
||||
"modelProvider.card.modelAPI": "I modelli {{modelName}} stanno utilizzando la chiave API.",
|
||||
"modelProvider.card.modelNotSupported": "I modelli {{modelName}} non sono installati.",
|
||||
"modelProvider.card.modelSupported": "I modelli {{modelName}} stanno utilizzando questa quota.",
|
||||
"modelProvider.card.onTrial": "In Prova",
|
||||
"modelProvider.card.paid": "Pagato",
|
||||
"modelProvider.card.priorityUse": "Uso prioritario",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Token gratuiti rimanenti disponibili",
|
||||
"modelProvider.rerankModel.key": "Modello di Rerank",
|
||||
"modelProvider.rerankModel.tip": "Il modello di rerank riordinerà la lista dei documenti candidati basandosi sulla corrispondenza semantica con la query dell'utente, migliorando i risultati del ranking semantico",
|
||||
"modelProvider.resetDate": "Reimposta su {{date}}",
|
||||
"modelProvider.searchModel": "Modello di ricerca",
|
||||
"modelProvider.selectModel": "Seleziona il tuo modello",
|
||||
"modelProvider.selector.emptySetting": "Per favore vai alle impostazioni per configurare",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "호출 횟수",
|
||||
"modelProvider.card.buyQuota": "Buy Quota",
|
||||
"modelProvider.card.callTimes": "호출 횟수",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} 모델이 API 키를 사용하고 있습니다.",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} 모델이 설치되지 않았습니다.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} 모델이 이 할당량을 사용하고 있습니다.",
|
||||
"modelProvider.card.onTrial": "트라이얼 중",
|
||||
"modelProvider.card.paid": "유료",
|
||||
"modelProvider.card.priorityUse": "우선 사용",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "남은 무료 토큰 사용 가능",
|
||||
"modelProvider.rerankModel.key": "재랭크 모델",
|
||||
"modelProvider.rerankModel.tip": "재랭크 모델은 사용자 쿼리와의 의미적 일치를 기반으로 후보 문서 목록을 재배열하여 의미적 순위를 향상시킵니다.",
|
||||
"modelProvider.resetDate": "{{date}}에서 재설정",
|
||||
"modelProvider.searchModel": "검색 모델",
|
||||
"modelProvider.selectModel": "모델 선택",
|
||||
"modelProvider.selector.emptySetting": "설정으로 이동하여 구성하세요",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Czasy wywołań",
|
||||
"modelProvider.card.buyQuota": "Kup limit",
|
||||
"modelProvider.card.callTimes": "Czasy wywołań",
|
||||
"modelProvider.card.modelAPI": "Modele {{modelName}} używają klucza API.",
|
||||
"modelProvider.card.modelNotSupported": "Modele {{modelName}} nie są zainstalowane.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} modeli korzysta z tej kwoty.",
|
||||
"modelProvider.card.onTrial": "Na próbę",
|
||||
"modelProvider.card.paid": "Płatny",
|
||||
"modelProvider.card.priorityUse": "Używanie z priorytetem",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Pozostałe dostępne darmowe tokeny",
|
||||
"modelProvider.rerankModel.key": "Model ponownego rankingu",
|
||||
"modelProvider.rerankModel.tip": "Model ponownego rankingu zmieni kolejność listy dokumentów kandydatów na podstawie semantycznego dopasowania z zapytaniem użytkownika, poprawiając wyniki rankingu semantycznego",
|
||||
"modelProvider.resetDate": "Reset na {{date}}",
|
||||
"modelProvider.searchModel": "Model wyszukiwania",
|
||||
"modelProvider.selectModel": "Wybierz swój model",
|
||||
"modelProvider.selector.emptySetting": "Przejdź do ustawień, aby skonfigurować",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Chamadas",
|
||||
"modelProvider.card.buyQuota": "Comprar Quota",
|
||||
"modelProvider.card.callTimes": "Chamadas",
|
||||
"modelProvider.card.modelAPI": "Os modelos {{modelName}} estão usando a Chave de API.",
|
||||
"modelProvider.card.modelNotSupported": "Modelos {{modelName}} não estão instalados.",
|
||||
"modelProvider.card.modelSupported": "Modelos {{modelName}} estão usando esta cota.",
|
||||
"modelProvider.card.onTrial": "Em Teste",
|
||||
"modelProvider.card.paid": "Pago",
|
||||
"modelProvider.card.priorityUse": "Uso prioritário",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Tokens gratuitos disponíveis restantes",
|
||||
"modelProvider.rerankModel.key": "Modelo de Reordenação",
|
||||
"modelProvider.rerankModel.tip": "O modelo de reordenaenação reorganizará a lista de documentos candidatos com base na correspondência semântica com a consulta do usuário, melhorando os resultados da classificação semântica",
|
||||
"modelProvider.resetDate": "Redefinir em {{date}}",
|
||||
"modelProvider.searchModel": "Modelo de pesquisa",
|
||||
"modelProvider.selectModel": "Selecione seu modelo",
|
||||
"modelProvider.selector.emptySetting": "Por favor, vá para configurações para configurar",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Apeluri",
|
||||
"modelProvider.card.buyQuota": "Cumpără cotă",
|
||||
"modelProvider.card.callTimes": "Apeluri",
|
||||
"modelProvider.card.modelAPI": "Modelele {{modelName}} folosesc cheia API.",
|
||||
"modelProvider.card.modelNotSupported": "Modelele {{modelName}} nu sunt instalate.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} modele utilizează această cotă.",
|
||||
"modelProvider.card.onTrial": "În probă",
|
||||
"modelProvider.card.paid": "Plătit",
|
||||
"modelProvider.card.priorityUse": "Utilizare prioritară",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Jetoane gratuite disponibile rămase",
|
||||
"modelProvider.rerankModel.key": "Model de reordonare",
|
||||
"modelProvider.rerankModel.tip": "Modelul de reordonare va reordona lista de documente candidate pe baza potrivirii semantice cu interogarea utilizatorului, îmbunătățind rezultatele clasificării semantice",
|
||||
"modelProvider.resetDate": "Resetați la {{date}}",
|
||||
"modelProvider.searchModel": "Model de căutare",
|
||||
"modelProvider.selectModel": "Selectați modelul dvs.",
|
||||
"modelProvider.selector.emptySetting": "Vă rugăm să mergeți la setări pentru a configura",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Количество вызовов",
|
||||
"modelProvider.card.buyQuota": "Купить квоту",
|
||||
"modelProvider.card.callTimes": "Количество вызовов",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} модели используют ключ API.",
|
||||
"modelProvider.card.modelNotSupported": "Модели {{modelName}} не установлены.",
|
||||
"modelProvider.card.modelSupported": "Эту квоту используют модели {{modelName}}.",
|
||||
"modelProvider.card.onTrial": "Пробная версия",
|
||||
"modelProvider.card.paid": "Платный",
|
||||
"modelProvider.card.priorityUse": "Приоритетное использование",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Оставшиеся доступные бесплатные токены",
|
||||
"modelProvider.rerankModel.key": "Модель повторного ранжирования",
|
||||
"modelProvider.rerankModel.tip": "Модель повторного ранжирования изменит порядок списка документов-кандидатов на основе семантического соответствия запросу пользователя, улучшая результаты семантического ранжирования",
|
||||
"modelProvider.resetDate": "Сброс на {{date}}",
|
||||
"modelProvider.searchModel": "Поиск модели",
|
||||
"modelProvider.selectModel": "Выберите свою модель",
|
||||
"modelProvider.selector.emptySetting": "Пожалуйста, перейдите в настройки для настройки",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Število klicev",
|
||||
"modelProvider.card.buyQuota": "Kupi kvoto",
|
||||
"modelProvider.card.callTimes": "Časi klicev",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} modeli uporabljajo API ključ.",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} modeli niso nameščeni.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} modeli uporabljajo to kvoto.",
|
||||
"modelProvider.card.onTrial": "Na preizkusu",
|
||||
"modelProvider.card.paid": "Plačano",
|
||||
"modelProvider.card.priorityUse": "Prednostna uporaba",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Preostali razpoložljivi brezplačni žetoni",
|
||||
"modelProvider.rerankModel.key": "Model za prerazvrstitev",
|
||||
"modelProvider.rerankModel.tip": "Model za prerazvrstitev bo prerazporedil seznam kandidatskih dokumentov na podlagi semantične ujemanja z uporabniško poizvedbo, s čimer se izboljšajo rezultati semantičnega razvrščanja.",
|
||||
"modelProvider.resetDate": "Ponastavi na {{date}}",
|
||||
"modelProvider.searchModel": "Model iskanja",
|
||||
"modelProvider.selectModel": "Izberite svoj model",
|
||||
"modelProvider.selector.emptySetting": "Prosimo, pojdite v nastavitve za konfiguracijo",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "เวลาโทร",
|
||||
"modelProvider.card.buyQuota": "ซื้อโควต้า",
|
||||
"modelProvider.card.callTimes": "เวลาโทร",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} โมเดลกำลังใช้คีย์ API",
|
||||
"modelProvider.card.modelNotSupported": "โมเดล {{modelName}} ยังไม่ได้ติดตั้ง",
|
||||
"modelProvider.card.modelSupported": "โมเดล {{modelName}} กำลังใช้โควต้านี้อยู่",
|
||||
"modelProvider.card.onTrial": "ทดลองใช้",
|
||||
"modelProvider.card.paid": "จ่าย",
|
||||
"modelProvider.card.priorityUse": "ลําดับความสําคัญในการใช้งาน",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "โทเค็นฟรีที่เหลืออยู่",
|
||||
"modelProvider.rerankModel.key": "จัดอันดับโมเดลใหม่",
|
||||
"modelProvider.rerankModel.tip": "โมเดล Rerank จะจัดลําดับรายการเอกสารผู้สมัครใหม่ตามการจับคู่ความหมายกับการสืบค้นของผู้ใช้ ซึ่งช่วยปรับปรุงผลลัพธ์ของการจัดอันดับความหมาย",
|
||||
"modelProvider.resetDate": "รีเซ็ตเมื่อ {{date}}",
|
||||
"modelProvider.searchModel": "ค้นหารุ่น",
|
||||
"modelProvider.selectModel": "เลือกรุ่นของคุณ",
|
||||
"modelProvider.selector.emptySetting": "โปรดไปที่การตั้งค่าเพื่อกําหนดค่า",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Çağrı Süreleri",
|
||||
"modelProvider.card.buyQuota": "Kota Satın Al",
|
||||
"modelProvider.card.callTimes": "Çağrı Süreleri",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} modelleri API Anahtarını kullanıyor.",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} modelleri yüklü değil.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} modelleri bu kotayı kullanıyor.",
|
||||
"modelProvider.card.onTrial": "Deneme Sürümünde",
|
||||
"modelProvider.card.paid": "Ücretli",
|
||||
"modelProvider.card.priorityUse": "Öncelikli Kullan",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Kalan kullanılabilir ücretsiz tokenler",
|
||||
"modelProvider.rerankModel.key": "Yeniden Sıralama Modeli",
|
||||
"modelProvider.rerankModel.tip": "Yeniden sıralama modeli, kullanıcı sorgusuyla anlam eşleştirmesine dayalı olarak aday belge listesini yeniden sıralayacak ve anlam sıralama sonuçlarını iyileştirecektir.",
|
||||
"modelProvider.resetDate": "{{date}} üzerine sıfırlama",
|
||||
"modelProvider.searchModel": "Model ara",
|
||||
"modelProvider.selectModel": "Modelinizi seçin",
|
||||
"modelProvider.selector.emptySetting": "Lütfen ayarlara gidip yapılandırın",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Кількість викликів",
|
||||
"modelProvider.card.buyQuota": "Придбати квоту",
|
||||
"modelProvider.card.callTimes": "Кількість викликів",
|
||||
"modelProvider.card.modelAPI": "Моделі {{modelName}} використовують API-ключ.",
|
||||
"modelProvider.card.modelNotSupported": "Моделі {{modelName}} не встановлені.",
|
||||
"modelProvider.card.modelSupported": "Моделі {{modelName}} використовують цю квоту.",
|
||||
"modelProvider.card.onTrial": "У пробному періоді",
|
||||
"modelProvider.card.paid": "Оплачено",
|
||||
"modelProvider.card.priorityUse": "Пріоритетне використання",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Залишилося доступних безкоштовних токенів",
|
||||
"modelProvider.rerankModel.key": "Модель повторного ранжування",
|
||||
"modelProvider.rerankModel.tip": "Модель повторного ранжування змінить порядок списку документів-кандидатів на основі семантичної відповідності запиту користувача, покращуючи результати семантичного ранжування.",
|
||||
"modelProvider.resetDate": "Скинути на {{date}}",
|
||||
"modelProvider.searchModel": "Пошукова модель",
|
||||
"modelProvider.selectModel": "Виберіть свою модель",
|
||||
"modelProvider.selector.emptySetting": "Перейдіть до налаштувань, щоб налаштувати",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "Số lần gọi",
|
||||
"modelProvider.card.buyQuota": "Mua Quota",
|
||||
"modelProvider.card.callTimes": "Số lần gọi",
|
||||
"modelProvider.card.modelAPI": "Các mô hình {{modelName}} đang sử dụng Khóa API.",
|
||||
"modelProvider.card.modelNotSupported": "Các mô hình {{modelName}} chưa được cài đặt.",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} mô hình đang sử dụng hạn mức này.",
|
||||
"modelProvider.card.onTrial": "Thử nghiệm",
|
||||
"modelProvider.card.paid": "Đã thanh toán",
|
||||
"modelProvider.card.priorityUse": "Ưu tiên sử dụng",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "Số lượng mã thông báo miễn phí còn lại",
|
||||
"modelProvider.rerankModel.key": "Mô hình Sắp xếp lại",
|
||||
"modelProvider.rerankModel.tip": "Mô hình sắp xếp lại sẽ sắp xếp lại danh sách tài liệu ứng cử viên dựa trên sự phù hợp ngữ nghĩa với truy vấn của người dùng, cải thiện kết quả của việc xếp hạng ngữ nghĩa",
|
||||
"modelProvider.resetDate": "Đặt lại vào {{date}}",
|
||||
"modelProvider.searchModel": "Mô hình tìm kiếm",
|
||||
"modelProvider.selectModel": "Chọn mô hình của bạn",
|
||||
"modelProvider.selector.emptySetting": "Vui lòng vào cài đặt để cấu hình",
|
||||
|
||||
@ -339,9 +339,6 @@
|
||||
"modelProvider.callTimes": "呼叫次數",
|
||||
"modelProvider.card.buyQuota": "購買額度",
|
||||
"modelProvider.card.callTimes": "呼叫次數",
|
||||
"modelProvider.card.modelAPI": "{{modelName}} 模型正在使用 API 金鑰。",
|
||||
"modelProvider.card.modelNotSupported": "{{modelName}} 模型未安裝。",
|
||||
"modelProvider.card.modelSupported": "{{modelName}} 模型正在使用這個配額。",
|
||||
"modelProvider.card.onTrial": "試用中",
|
||||
"modelProvider.card.paid": "已購買",
|
||||
"modelProvider.card.priorityUse": "優先使用",
|
||||
@ -397,7 +394,6 @@
|
||||
"modelProvider.quotaTip": "剩餘免費額度",
|
||||
"modelProvider.rerankModel.key": "Rerank 模型",
|
||||
"modelProvider.rerankModel.tip": "重排序模型將根據候選文件列表與使用者問題語義匹配度進行重新排序,從而改進語義排序的結果",
|
||||
"modelProvider.resetDate": "在 {{date}} 重置",
|
||||
"modelProvider.searchModel": "搜尋模型",
|
||||
"modelProvider.selectModel": "選擇您的模型",
|
||||
"modelProvider.selector.emptySetting": "請前往設定進行配置",
|
||||
|
||||
@ -4,6 +4,12 @@
|
||||
"version": "1.11.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a",
|
||||
"imports": {
|
||||
"#i18n": {
|
||||
"react-server": "./i18n-config/lib.server.ts",
|
||||
"default": "./i18n-config/lib.client.ts"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v22.11.0"
|
||||
},
|
||||
@ -47,7 +53,7 @@
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.31.3",
|
||||
"@amplitude/analytics-browser": "^2.33.1",
|
||||
"@amplitude/plugin-session-replay-browser": "^1.23.6",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
|
||||
81
web/pnpm-lock.yaml
generated
81
web/pnpm-lock.yaml
generated
@ -58,8 +58,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@amplitude/analytics-browser':
|
||||
specifier: ^2.31.3
|
||||
version: 2.31.4
|
||||
specifier: ^2.33.1
|
||||
version: 2.33.1
|
||||
'@amplitude/plugin-session-replay-browser':
|
||||
specifier: ^1.23.6
|
||||
version: 1.24.1(@amplitude/rrweb@2.0.0-alpha.33)(rollup@2.79.2)
|
||||
@ -578,8 +578,8 @@ packages:
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@amplitude/analytics-browser@2.31.4':
|
||||
resolution: {integrity: sha512-9O8a0SK55tQOgJJ0z9eE+q/C2xWo6a65wN4iSglxYwm1vvGJKG6Z/QV4XKQ6X0syGscRuG1XoMc0mt3xdVPtDg==}
|
||||
'@amplitude/analytics-browser@2.33.1':
|
||||
resolution: {integrity: sha512-93wZjuAFJ7QdyptF82i1pezm5jKuBWITHI++XshDgpks1RstJvJ9n11Ak8MnE4L2BGQ93XDN2aVEHfmQkt0/Pw==}
|
||||
|
||||
'@amplitude/analytics-client-common@2.4.16':
|
||||
resolution: {integrity: sha512-qF7NAl6Qr6QXcWKnldGJfO0Kp1TYoy1xsmzEDnOYzOS96qngtvsZ8MuKya1lWdVACoofwQo82V0VhNZJKk/2YA==}
|
||||
@ -590,29 +590,32 @@ packages:
|
||||
'@amplitude/analytics-core@2.33.0':
|
||||
resolution: {integrity: sha512-56m0R12TjZ41D2YIghb/XNHSdL4CurAVyRT3L2FD+9DCFfbgjfT8xhDBnsZtA+aBkb6Yak1EGUojGBunfAm2/A==}
|
||||
|
||||
'@amplitude/analytics-core@2.35.0':
|
||||
resolution: {integrity: sha512-7RmHYELXCGu8yuO9D6lEXiqkMtiC5sePNhCWmwuP30dneDYHtH06gaYvAFH/YqOFuE6enwEEJfFYtcaPhyiqtA==}
|
||||
|
||||
'@amplitude/analytics-types@2.11.0':
|
||||
resolution: {integrity: sha512-L1niBXYSWmbyHUE/GNuf6YBljbafaxWI3X5jjEIZDFCjQvdWO3DKalY1VPFUbhgYQgWw7+bC6I/AlUaporyfig==}
|
||||
|
||||
'@amplitude/experiment-core@0.7.2':
|
||||
resolution: {integrity: sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA==}
|
||||
|
||||
'@amplitude/plugin-autocapture-browser@1.18.0':
|
||||
resolution: {integrity: sha512-hBBZpghTEnl+XF8UZaGxe1xCbSjawdmOkJC0/tQF2k1FwlJS/rdWBGmPd8wH7iU4hd55pnSw28Kd2NL7q0zTcA==}
|
||||
'@amplitude/plugin-autocapture-browser@1.18.3':
|
||||
resolution: {integrity: sha512-njYque5t1QCEEe5V8Ls4yVVklTM6V7OXxBk6pqznN/hj/Pc4X8Wjy898pZ2VtbnvpagBKKzGb5B6Syl8OXiicw==}
|
||||
|
||||
'@amplitude/plugin-network-capture-browser@1.7.0':
|
||||
resolution: {integrity: sha512-tlwkBL0tlc1OUTT2XYTjWx4mm6O0DSggKzkkDq+8DhW+ZFl9OfHMFIh/hDLJzxs1LTtX7CvFUfAVSDifJOs+NA==}
|
||||
'@amplitude/plugin-network-capture-browser@1.7.3':
|
||||
resolution: {integrity: sha512-zfWgAN7g6AigJAsgrGmlgVwydOHH6XvweBoxhU+qEvRydboiIVCDLSxuXczUsBG7kYVLWRdBK1DYoE5J7lqTGA==}
|
||||
|
||||
'@amplitude/plugin-page-url-enrichment-browser@0.5.6':
|
||||
resolution: {integrity: sha512-H6+tf0zYhvM+8oJsdC/kAbIzuxOY/0p+3HBmX4K+G4doo5nCGAB0DYTr6dqMp1GcPOZ09pKT41+DJ6vwSy4ypQ==}
|
||||
'@amplitude/plugin-page-url-enrichment-browser@0.5.9':
|
||||
resolution: {integrity: sha512-TqdELx4WrdRutCjHUFUzum/f/UjhbdTZw0UKkYFAj5gwAKDjaPEjL4waRvINOTaVLsne1A6ck4KEMfC8AKByFw==}
|
||||
|
||||
'@amplitude/plugin-page-view-tracking-browser@2.6.3':
|
||||
resolution: {integrity: sha512-lLU4W2r5jXtfn/14cZKM9c9CQDxT7PVVlgm0susHJ3Kfsua9jJQuMHs4Zlg6rwByAtZi5nF4nYE5z0GF09gx0A==}
|
||||
'@amplitude/plugin-page-view-tracking-browser@2.6.6':
|
||||
resolution: {integrity: sha512-dBcJlrdKgPzSgS3exDRRrMLqhIaOjwlIy7o8sEMn1PpMawERlbumSSdtfII6L4L67HYUPo4PY4Kp4acqSzaLvQ==}
|
||||
|
||||
'@amplitude/plugin-session-replay-browser@1.24.1':
|
||||
resolution: {integrity: sha512-NHePIu2Yv9ba+fOt5N33b8FFQPzyKvjs1BnWBgBCM5RECos3w6n/+zUWTnTJ4at2ipO2lz111abKDteUwbuptg==}
|
||||
|
||||
'@amplitude/plugin-web-vitals-browser@1.1.0':
|
||||
resolution: {integrity: sha512-TA0X4Np4Wt5hkQ4+Ouhg6nm2xjDd9l03OV9N8Kbe1cqpr/sxvRwSpd+kp2eREbp6D7tHFFkKJA2iNtxbE5Y0cA==}
|
||||
'@amplitude/plugin-web-vitals-browser@1.1.4':
|
||||
resolution: {integrity: sha512-XQXI9OjTNSz2yi0lXw2VYMensDzzSkMCfvXNniTb1LgnHwBcQ1JWPcTqHLPFrvvNckeIdOT78vjs7yA+c1FyzA==}
|
||||
|
||||
'@amplitude/rrdom@2.0.0-alpha.33':
|
||||
resolution: {integrity: sha512-uu+1w1RGEJ7QcGPwCC898YBR47DpNYOZTnQMY9/IgMzTXQ0+Hh1/JLsQfMnBBtAePhvCS0BlHd/qGD5w0taIcg==}
|
||||
@ -8734,8 +8737,8 @@ packages:
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
web-vitals@5.0.1:
|
||||
resolution: {integrity: sha512-BsULPWaCKAAtNntUz0aJq1cu1wyuWmDzf4N6vYNMbYA6zzQAf2pzCYbyClf+Ui2MI54bt225AwugXIfL1W+Syg==}
|
||||
web-vitals@5.1.0:
|
||||
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
|
||||
|
||||
webidl-conversions@4.0.2:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
@ -9016,14 +9019,14 @@ snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@amplitude/analytics-browser@2.31.4':
|
||||
'@amplitude/analytics-browser@2.33.1':
|
||||
dependencies:
|
||||
'@amplitude/analytics-core': 2.33.0
|
||||
'@amplitude/plugin-autocapture-browser': 1.18.0
|
||||
'@amplitude/plugin-network-capture-browser': 1.7.0
|
||||
'@amplitude/plugin-page-url-enrichment-browser': 0.5.6
|
||||
'@amplitude/plugin-page-view-tracking-browser': 2.6.3
|
||||
'@amplitude/plugin-web-vitals-browser': 1.1.0
|
||||
'@amplitude/analytics-core': 2.35.0
|
||||
'@amplitude/plugin-autocapture-browser': 1.18.3
|
||||
'@amplitude/plugin-network-capture-browser': 1.7.3
|
||||
'@amplitude/plugin-page-url-enrichment-browser': 0.5.9
|
||||
'@amplitude/plugin-page-view-tracking-browser': 2.6.6
|
||||
'@amplitude/plugin-web-vitals-browser': 1.1.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@amplitude/analytics-client-common@2.4.16':
|
||||
@ -9041,31 +9044,37 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
zen-observable-ts: 1.1.0
|
||||
|
||||
'@amplitude/analytics-core@2.35.0':
|
||||
dependencies:
|
||||
'@amplitude/analytics-connector': 1.6.4
|
||||
tslib: 2.8.1
|
||||
zen-observable-ts: 1.1.0
|
||||
|
||||
'@amplitude/analytics-types@2.11.0': {}
|
||||
|
||||
'@amplitude/experiment-core@0.7.2':
|
||||
dependencies:
|
||||
js-base64: 3.7.8
|
||||
|
||||
'@amplitude/plugin-autocapture-browser@1.18.0':
|
||||
'@amplitude/plugin-autocapture-browser@1.18.3':
|
||||
dependencies:
|
||||
'@amplitude/analytics-core': 2.33.0
|
||||
'@amplitude/analytics-core': 2.35.0
|
||||
rxjs: 7.8.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@amplitude/plugin-network-capture-browser@1.7.0':
|
||||
'@amplitude/plugin-network-capture-browser@1.7.3':
|
||||
dependencies:
|
||||
'@amplitude/analytics-core': 2.33.0
|
||||
'@amplitude/analytics-core': 2.35.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@amplitude/plugin-page-url-enrichment-browser@0.5.6':
|
||||
'@amplitude/plugin-page-url-enrichment-browser@0.5.9':
|
||||
dependencies:
|
||||
'@amplitude/analytics-core': 2.33.0
|
||||
'@amplitude/analytics-core': 2.35.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@amplitude/plugin-page-view-tracking-browser@2.6.3':
|
||||
'@amplitude/plugin-page-view-tracking-browser@2.6.6':
|
||||
dependencies:
|
||||
'@amplitude/analytics-core': 2.33.0
|
||||
'@amplitude/analytics-core': 2.35.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@amplitude/plugin-session-replay-browser@1.24.1(@amplitude/rrweb@2.0.0-alpha.33)(rollup@2.79.2)':
|
||||
@ -9080,11 +9089,11 @@ snapshots:
|
||||
- '@amplitude/rrweb'
|
||||
- rollup
|
||||
|
||||
'@amplitude/plugin-web-vitals-browser@1.1.0':
|
||||
'@amplitude/plugin-web-vitals-browser@1.1.4':
|
||||
dependencies:
|
||||
'@amplitude/analytics-core': 2.33.0
|
||||
'@amplitude/analytics-core': 2.35.0
|
||||
tslib: 2.8.1
|
||||
web-vitals: 5.0.1
|
||||
web-vitals: 5.1.0
|
||||
|
||||
'@amplitude/rrdom@2.0.0-alpha.33':
|
||||
dependencies:
|
||||
@ -9148,7 +9157,7 @@ snapshots:
|
||||
'@amplitude/targeting@0.2.0':
|
||||
dependencies:
|
||||
'@amplitude/analytics-client-common': 2.4.16
|
||||
'@amplitude/analytics-core': 2.33.0
|
||||
'@amplitude/analytics-core': 2.35.0
|
||||
'@amplitude/analytics-types': 2.11.0
|
||||
'@amplitude/experiment-core': 0.7.2
|
||||
idb: 8.0.0
|
||||
@ -18336,7 +18345,7 @@ snapshots:
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
web-vitals@5.0.1: {}
|
||||
web-vitals@5.1.0: {}
|
||||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
||||
|
||||
@ -316,7 +316,7 @@ export type SiteConfig = {
|
||||
use_icon_as_answer_icon: boolean
|
||||
}
|
||||
|
||||
export type AppIconType = 'image' | 'emoji'
|
||||
export type AppIconType = 'image' | 'emoji' | 'link'
|
||||
|
||||
/**
|
||||
* App
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
export default <T>(defaultValue: T): [() => T, (v: T) => void] => {
|
||||
export function serverOnlyContext<T>(defaultValue: T): [() => T, (v: T) => void] {
|
||||
const getRef = cache(() => ({ current: defaultValue }))
|
||||
|
||||
const getValue = (): T => getRef().current
|
||||
|
||||
Reference in New Issue
Block a user