-
-
{languageShowName}
-
- {language === 'mermaid' && }
-
-
-
- {(language === 'mermaid' && isSVG)
- ? (
)
- : (language === 'echarts'
- ? (
)
- : (language === 'svg'
- ? (
)
- : (
- {String(children).replace(/\n$/, '')}
- )))}
+ const renderCodeContent = useMemo(() => {
+ const content = String(children).replace(/\n$/, '')
+ if (language === 'mermaid' && isSVG) {
+ return
+ }
+ else if (language === 'echarts') {
+ return (
+
+
+
+
)
- : (
{children}
)
- }, [chartData, children, className, inline, isSVG, language, languageShowName, match, props])
+ }
+ else if (language === 'svg' && isSVG) {
+ return (
+
+
+
+ )
+ }
+ else {
+ return (
+
+ {content}
+
+ )
+ }
+ }, [language, match, props, children, chartData, isSVG])
+
+ if (inline || !match)
+ return
{children}
+
+ return (
+
+
+
{languageShowName}
+
+ {(['mermaid', 'svg']).includes(language!) && }
+
+
+
+ {renderCodeContent}
+
+ )
})
CodeBlock.displayName = 'CodeBlock'
@@ -224,7 +245,7 @@ export function Markdown(props: { content: string; className?: string }) {
return (
= ({ className, id, name, noTooltip, tip, step = 0.1,
{name}
{!noTooltip && (
{tip}
}
/>
)}
diff --git a/web/app/components/base/popover/index.tsx b/web/app/components/base/popover/index.tsx
index 141ac8ff70d62..1e7ba76269d75 100644
--- a/web/app/components/base/popover/index.tsx
+++ b/web/app/components/base/popover/index.tsx
@@ -17,6 +17,7 @@ type IPopover = {
btnElement?: string | React.ReactNode
btnClassName?: string | ((open: boolean) => string)
manualClose?: boolean
+ disabled?: boolean
}
const timeoutDuration = 100
@@ -30,6 +31,7 @@ export default function CustomPopover({
className,
btnClassName,
manualClose,
+ disabled = false,
}: IPopover) {
const buttonRef = useRef
(null)
const timeOutRef = useRef(null)
@@ -60,6 +62,7 @@ export default function CustomPopover({
>
{
+ resetReg()
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
}, [createWorkflowVariableBlockNode, getMatch])
useEffect(() => {
- REGEX.lastIndex = 0
+ resetReg()
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, transformListener),
)
diff --git a/web/app/components/base/regenerate-btn/index.tsx b/web/app/components/base/regenerate-btn/index.tsx
new file mode 100644
index 0000000000000..aaf0206df609d
--- /dev/null
+++ b/web/app/components/base/regenerate-btn/index.tsx
@@ -0,0 +1,31 @@
+'use client'
+import { t } from 'i18next'
+import { Refresh } from '../icons/src/vender/line/general'
+import Tooltip from '@/app/components/base/tooltip'
+
+type Props = {
+ className?: string
+ onClick?: () => void
+}
+
+const RegenerateBtn = ({ className, onClick }: Props) => {
+ return (
+
+
+ onClick?.()}
+ style={{
+ boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
+ }}
+ >
+
+
+
+
+ )
+}
+
+export default RegenerateBtn
diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx
index dee983690b3c4..e821cb154516b 100644
--- a/web/app/components/base/select/index.tsx
+++ b/web/app/components/base/select/index.tsx
@@ -87,7 +87,7 @@ const Select: FC = ({
{allowSearch
?
{
if (!disabled)
setQuery(event.target.value)
diff --git a/web/app/components/base/svg-gallery/index.tsx b/web/app/components/base/svg-gallery/index.tsx
index 81e8e87655009..4368df00e9d38 100644
--- a/web/app/components/base/svg-gallery/index.tsx
+++ b/web/app/components/base/svg-gallery/index.tsx
@@ -29,7 +29,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
if (svgRef.current) {
try {
svgRef.current.innerHTML = ''
- const draw = SVG().addTo(svgRef.current).size('100%', '100%')
+ const draw = SVG().addTo(svgRef.current)
const parser = new DOMParser()
const svgDoc = parser.parseFromString(content, 'image/svg+xml')
@@ -40,13 +40,11 @@ export const SVGRenderer = ({ content }: { content: string }) => {
const originalWidth = parseInt(svgElement.getAttribute('width') || '400', 10)
const originalHeight = parseInt(svgElement.getAttribute('height') || '600', 10)
- const scale = Math.min(windowSize.width / originalWidth, windowSize.height / originalHeight, 1)
- const scaledWidth = originalWidth * scale
- const scaledHeight = originalHeight * scale
- draw.size(scaledWidth, scaledHeight)
+ draw.viewbox(0, 0, originalWidth, originalHeight)
+
+ svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
const rootElement = draw.svg(content)
- rootElement.scale(scale)
rootElement.click(() => {
setImagePreview(svgToDataURL(svgElement as Element))
@@ -54,7 +52,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
}
catch (error) {
if (svgRef.current)
- svgRef.current.innerHTML = 'Error rendering SVG. Wait for the image content to complete.'
+ svgRef.current.innerHTML = 'Error rendering SVG. Wait for the image content to complete.'
}
}
}, [content, windowSize])
@@ -62,14 +60,14 @@ export const SVGRenderer = ({ content }: { content: string }) => {
return (
<>
{imagePreview && ( setImagePreview('')} />)}
>
diff --git a/web/app/components/base/topbar/index.tsx b/web/app/components/base/topbar/index.tsx
deleted file mode 100644
index cf67456bd3423..0000000000000
--- a/web/app/components/base/topbar/index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-'use client'
-
-import { AppProgressBar as ProgressBar } from 'next-nprogress-bar'
-
-const Topbar = () => {
- return (
- <>
-
- >)
-}
-
-export default Topbar
diff --git a/web/app/components/datasets/common/retrieval-param-config/index.tsx b/web/app/components/datasets/common/retrieval-param-config/index.tsx
index 323e47f3b4ae2..9d48d56a8dc51 100644
--- a/web/app/components/datasets/common/retrieval-param-config/index.tsx
+++ b/web/app/components/datasets/common/retrieval-param-config/index.tsx
@@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
-import React from 'react'
+import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
@@ -11,7 +11,7 @@ import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import type { RetrievalConfig } from '@/types/app'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
-import { useModelListAndDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
+import { useCurrentProviderAndModel, useModelListAndDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
DEFAULT_WEIGHTED_SCORE,
@@ -19,6 +19,7 @@ import {
WeightedScoreEnum,
} from '@/models/datasets'
import WeightedScore from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
+import Toast from '@/app/components/base/toast'
type Props = {
type: RETRIEVE_METHOD
@@ -38,6 +39,24 @@ const RetrievalParamConfig: FC = ({
defaultModel: rerankDefaultModel,
modelList: rerankModelList,
} = useModelListAndDefaultModel(ModelTypeEnum.rerank)
+
+ const {
+ currentModel,
+ } = useCurrentProviderAndModel(
+ rerankModelList,
+ rerankDefaultModel
+ ? {
+ ...rerankDefaultModel,
+ provider: rerankDefaultModel.provider.provider,
+ }
+ : undefined,
+ )
+
+ const handleDisabledSwitchClick = useCallback(() => {
+ if (!currentModel)
+ Toast.notify({ type: 'error', message: t('workflow.errorMsg.rerankModelRequired') })
+ }, [currentModel, rerankDefaultModel, t])
+
const isHybridSearch = type === RETRIEVE_METHOD.hybrid
const rerankModel = (() => {
@@ -99,16 +118,22 @@ const RetrievalParamConfig: FC = ({
{canToggleRerankModalEnable && (
-
{
- onChange({
- ...value,
- reranking_enable: v,
- })
- }}
- />
+
+ {
+ onChange({
+ ...value,
+ reranking_enable: v,
+ })
+ }}
+ disabled={!currentModel}
+ />
+
)}
{t('common.modelProvider.rerankModel.key')}
diff --git a/web/app/components/datasets/create/assets/jina.png b/web/app/components/datasets/create/assets/jina.png
new file mode 100644
index 0000000000000..b4beeafdfb127
Binary files /dev/null and b/web/app/components/datasets/create/assets/jina.png differ
diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx
index 12c6284d882c5..98098445c7695 100644
--- a/web/app/components/datasets/create/index.tsx
+++ b/web/app/components/datasets/create/index.tsx
@@ -11,7 +11,7 @@ import { DataSourceType } from '@/models/datasets'
import type { CrawlOptions, CrawlResultItem, DataSet, FileItem, createDocumentResponse } from '@/models/datasets'
import { fetchDataSource } from '@/service/common'
import { fetchDatasetDetail } from '@/service/datasets'
-import type { NotionPage } from '@/models/common'
+import { DataSourceProvider, type NotionPage } from '@/models/common'
import { useModalContext } from '@/context/modal-context'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
@@ -26,6 +26,7 @@ const DEFAULT_CRAWL_OPTIONS: CrawlOptions = {
excludes: '',
limit: 10,
max_depth: '',
+ use_sitemap: true,
}
const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
@@ -51,7 +52,8 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
const updateFileList = (preparedFiles: FileItem[]) => {
setFiles(preparedFiles)
}
- const [fireCrawlJobId, setFireCrawlJobId] = useState('')
+ const [websiteCrawlProvider, setWebsiteCrawlProvider] = useState
(DataSourceProvider.fireCrawl)
+ const [websiteCrawlJobId, setWebsiteCrawlJobId] = useState('')
const updateFile = (fileItem: FileItem, progress: number, list: FileItem[]) => {
const targetIndex = list.findIndex(file => file.fileID === fileItem.fileID)
@@ -137,7 +139,8 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
onStepChange={nextStep}
websitePages={websitePages}
updateWebsitePages={setWebsitePages}
- onFireCrawlJobIdChange={setFireCrawlJobId}
+ onWebsiteCrawlProviderChange={setWebsiteCrawlProvider}
+ onWebsiteCrawlJobIdChange={setWebsiteCrawlJobId}
crawlOptions={crawlOptions}
onCrawlOptionsChange={setCrawlOptions}
/>
@@ -151,7 +154,8 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
files={fileList.map(file => file.file)}
notionPages={notionPages}
websitePages={websitePages}
- fireCrawlJobId={fireCrawlJobId}
+ websiteCrawlProvider={websiteCrawlProvider}
+ websiteCrawlJobId={websiteCrawlJobId}
onStepChange={changeStep}
updateIndexingTypeCache={updateIndexingTypeCache}
updateResultCache={updateResultCache}
diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx
index c2d77f4cecdcc..643932e9ae21d 100644
--- a/web/app/components/datasets/create/step-one/index.tsx
+++ b/web/app/components/datasets/create/step-one/index.tsx
@@ -10,7 +10,7 @@ import WebsitePreview from '../website/preview'
import s from './index.module.css'
import cn from '@/utils/classnames'
import type { CrawlOptions, CrawlResultItem, FileItem } from '@/models/datasets'
-import type { NotionPage } from '@/models/common'
+import type { DataSourceProvider, NotionPage } from '@/models/common'
import { DataSourceType } from '@/models/datasets'
import Button from '@/app/components/base/button'
import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
@@ -33,7 +33,8 @@ type IStepOneProps = {
changeType: (type: DataSourceType) => void
websitePages?: CrawlResultItem[]
updateWebsitePages: (value: CrawlResultItem[]) => void
- onFireCrawlJobIdChange: (jobId: string) => void
+ onWebsiteCrawlProviderChange: (provider: DataSourceProvider) => void
+ onWebsiteCrawlJobIdChange: (jobId: string) => void
crawlOptions: CrawlOptions
onCrawlOptionsChange: (payload: CrawlOptions) => void
}
@@ -69,7 +70,8 @@ const StepOne = ({
updateNotionPages,
websitePages = [],
updateWebsitePages,
- onFireCrawlJobIdChange,
+ onWebsiteCrawlProviderChange,
+ onWebsiteCrawlJobIdChange,
crawlOptions,
onCrawlOptionsChange,
}: IStepOneProps) => {
@@ -229,7 +231,8 @@ const StepOne = ({
onPreview={setCurrentWebsite}
checkedCrawlResult={websitePages}
onCheckedCrawlResultChange={updateWebsitePages}
- onJobIdChange={onFireCrawlJobIdChange}
+ onCrawlProviderChange={onWebsiteCrawlProviderChange}
+ onJobIdChange={onWebsiteCrawlJobIdChange}
crawlOptions={crawlOptions}
onCrawlOptionsChange={onCrawlOptionsChange}
/>
diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx
index 94614918dbe40..5d92e30deb8cc 100644
--- a/web/app/components/datasets/create/step-two/index.tsx
+++ b/web/app/components/datasets/create/step-two/index.tsx
@@ -33,6 +33,7 @@ import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/componen
import Toast from '@/app/components/base/toast'
import { formatNumber } from '@/utils/format'
import type { NotionPage } from '@/models/common'
+import { DataSourceProvider } from '@/models/common'
import { DataSourceType, DocForm } from '@/models/datasets'
import NotionIcon from '@/app/components/base/notion-icon'
import Switch from '@/app/components/base/switch'
@@ -63,7 +64,8 @@ type StepTwoProps = {
notionPages?: NotionPage[]
websitePages?: CrawlResultItem[]
crawlOptions?: CrawlOptions
- fireCrawlJobId?: string
+ websiteCrawlProvider?: DataSourceProvider
+ websiteCrawlJobId?: string
onStepChange?: (delta: number) => void
updateIndexingTypeCache?: (type: string) => void
updateResultCache?: (res: createDocumentResponse) => void
@@ -94,7 +96,8 @@ const StepTwo = ({
notionPages = [],
websitePages = [],
crawlOptions,
- fireCrawlJobId = '',
+ websiteCrawlProvider = DataSourceProvider.fireCrawl,
+ websiteCrawlJobId = '',
onStepChange,
updateIndexingTypeCache,
updateResultCache,
@@ -129,6 +132,7 @@ const StepTwo = ({
? IndexingType.QUALIFIED
: IndexingType.ECONOMICAL,
)
+ const [isLanguageSelectDisabled, setIsLanguageSelectDisabled] = useState(false)
const [docForm, setDocForm] = useState(
(datasetId && documentDetail) ? documentDetail.doc_form : DocForm.TEXT,
)
@@ -197,9 +201,9 @@ const StepTwo = ({
}
}
- const fetchFileIndexingEstimate = async (docForm = DocForm.TEXT) => {
+ const fetchFileIndexingEstimate = async (docForm = DocForm.TEXT, language?: string) => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
- const res = await didFetchFileIndexingEstimate(getFileIndexingEstimateParams(docForm)!)
+ const res = await didFetchFileIndexingEstimate(getFileIndexingEstimateParams(docForm, language)!)
if (segmentationType === SegmentType.CUSTOM)
setCustomFileIndexingEstimate(res)
else
@@ -260,14 +264,14 @@ const StepTwo = ({
const getWebsiteInfo = () => {
return {
- provider: 'firecrawl',
- job_id: fireCrawlJobId,
+ provider: websiteCrawlProvider,
+ job_id: websiteCrawlJobId,
urls: websitePages.map(page => page.source_url),
only_main_content: crawlOptions?.only_main_content,
}
}
- const getFileIndexingEstimateParams = (docForm: DocForm): IndexingEstimateParams | undefined => {
+ const getFileIndexingEstimateParams = (docForm: DocForm, language?: string): IndexingEstimateParams | undefined => {
if (dataSourceType === DataSourceType.FILE) {
return {
info_list: {
@@ -279,7 +283,7 @@ const StepTwo = ({
indexing_technique: getIndexing_technique() as string,
process_rule: getProcessRule(),
doc_form: docForm,
- doc_language: docLanguage,
+ doc_language: language || docLanguage,
dataset_id: datasetId as string,
}
}
@@ -292,7 +296,7 @@ const StepTwo = ({
indexing_technique: getIndexing_technique() as string,
process_rule: getProcessRule(),
doc_form: docForm,
- doc_language: docLanguage,
+ doc_language: language || docLanguage,
dataset_id: datasetId as string,
}
}
@@ -305,7 +309,7 @@ const StepTwo = ({
indexing_technique: getIndexing_technique() as string,
process_rule: getProcessRule(),
doc_form: docForm,
- doc_language: docLanguage,
+ doc_language: language || docLanguage,
dataset_id: datasetId as string,
}
}
@@ -480,8 +484,26 @@ const StepTwo = ({
setDocForm(DocForm.TEXT)
}
+ const previewSwitch = async (language?: string) => {
+ setPreviewSwitched(true)
+ setIsLanguageSelectDisabled(true)
+ if (segmentationType === SegmentType.AUTO)
+ setAutomaticFileIndexingEstimate(null)
+ else
+ setCustomFileIndexingEstimate(null)
+ try {
+ await fetchFileIndexingEstimate(DocForm.QA, language)
+ }
+ finally {
+ setIsLanguageSelectDisabled(false)
+ }
+ }
+
const handleSelect = (language: string) => {
setDocLanguage(language)
+ // Switch language, re-cutter
+ if (docForm === DocForm.QA && previewSwitched)
+ previewSwitch(language)
}
const changeToEconomicalType = () => {
@@ -491,15 +513,6 @@ const StepTwo = ({
}
}
- const previewSwitch = async () => {
- setPreviewSwitched(true)
- if (segmentationType === SegmentType.AUTO)
- setAutomaticFileIndexingEstimate(null)
- else
- setCustomFileIndexingEstimate(null)
- await fetchFileIndexingEstimate(DocForm.QA)
- }
-
useEffect(() => {
// fetch rules
if (!isSetting) {
@@ -572,7 +585,7 @@ const StepTwo = ({
{t('datasetCreation.steps.two')}
- {isMobile && (
+ {(isMobile || !showPreview) && (
{t('datasetCreation.stepTwo.QALanguage')}
-
+
@@ -820,7 +833,7 @@ const StepTwo = ({
{t('datasetSettings.form.retrievalSetting.title')}
@@ -945,7 +958,7 @@ const StepTwo = ({
{t('datasetCreation.stepTwo.previewTitle')}
{docForm === DocForm.QA && !previewSwitched && (
-
+
)}
diff --git a/web/app/components/datasets/create/step-two/language-select/index.tsx b/web/app/components/datasets/create/step-two/language-select/index.tsx
index f8709c89f3a6b..fab2bb1c71389 100644
--- a/web/app/components/datasets/create/step-two/language-select/index.tsx
+++ b/web/app/components/datasets/create/step-two/language-select/index.tsx
@@ -9,16 +9,19 @@ import { languages } from '@/i18n/language'
export type ILanguageSelectProps = {
currentLanguage: string
onSelect: (language: string) => void
+ disabled?: boolean
}
const LanguageSelect: FC
= ({
currentLanguage,
onSelect,
+ disabled,
}) => {
return (
{languages.filter(language => language.supported).map(({ prompt_name, name }) => (
diff --git a/web/app/components/datasets/create/website/firecrawl/base/checkbox-with-label.tsx b/web/app/components/datasets/create/website/base/checkbox-with-label.tsx
similarity index 72%
rename from web/app/components/datasets/create/website/firecrawl/base/checkbox-with-label.tsx
rename to web/app/components/datasets/create/website/base/checkbox-with-label.tsx
index 5c574ebe3e619..25d40fe0763da 100644
--- a/web/app/components/datasets/create/website/firecrawl/base/checkbox-with-label.tsx
+++ b/web/app/components/datasets/create/website/base/checkbox-with-label.tsx
@@ -3,6 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import Checkbox from '@/app/components/base/checkbox'
+import Tooltip from '@/app/components/base/tooltip'
type Props = {
className?: string
@@ -10,6 +11,7 @@ type Props = {
onChange: (isChecked: boolean) => void
label: string
labelClassName?: string
+ tooltip?: string
}
const CheckboxWithLabel: FC = ({
@@ -18,11 +20,20 @@ const CheckboxWithLabel: FC = ({
onChange,
label,
labelClassName,
+ tooltip,
}) => {
return (
+ }
+ triggerClassName='ml-0.5 w-4 h-4'
+ />
+ )}
)
}
diff --git a/web/app/components/datasets/create/website/firecrawl/crawled-result-item.tsx b/web/app/components/datasets/create/website/base/crawled-result-item.tsx
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/crawled-result-item.tsx
rename to web/app/components/datasets/create/website/base/crawled-result-item.tsx
diff --git a/web/app/components/datasets/create/website/firecrawl/crawled-result.tsx b/web/app/components/datasets/create/website/base/crawled-result.tsx
similarity index 97%
rename from web/app/components/datasets/create/website/firecrawl/crawled-result.tsx
rename to web/app/components/datasets/create/website/base/crawled-result.tsx
index 2bd51e4d731a9..d5c8d1b80a5a1 100644
--- a/web/app/components/datasets/create/website/firecrawl/crawled-result.tsx
+++ b/web/app/components/datasets/create/website/base/crawled-result.tsx
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
-import CheckboxWithLabel from './base/checkbox-with-label'
+import CheckboxWithLabel from './checkbox-with-label'
import CrawledResultItem from './crawled-result-item'
import cn from '@/utils/classnames'
import type { CrawlResultItem } from '@/models/datasets'
diff --git a/web/app/components/datasets/create/website/firecrawl/crawling.tsx b/web/app/components/datasets/create/website/base/crawling.tsx
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/crawling.tsx
rename to web/app/components/datasets/create/website/base/crawling.tsx
diff --git a/web/app/components/datasets/create/website/firecrawl/base/error-message.tsx b/web/app/components/datasets/create/website/base/error-message.tsx
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/base/error-message.tsx
rename to web/app/components/datasets/create/website/base/error-message.tsx
diff --git a/web/app/components/datasets/create/website/firecrawl/base/field.tsx b/web/app/components/datasets/create/website/base/field.tsx
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/base/field.tsx
rename to web/app/components/datasets/create/website/base/field.tsx
diff --git a/web/app/components/datasets/create/website/firecrawl/base/input.tsx b/web/app/components/datasets/create/website/base/input.tsx
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/base/input.tsx
rename to web/app/components/datasets/create/website/base/input.tsx
diff --git a/web/app/components/datasets/create/website/firecrawl/mock-crawl-result.ts b/web/app/components/datasets/create/website/base/mock-crawl-result.ts
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/mock-crawl-result.ts
rename to web/app/components/datasets/create/website/base/mock-crawl-result.ts
diff --git a/web/app/components/datasets/create/website/firecrawl/base/options-wrap.tsx b/web/app/components/datasets/create/website/base/options-wrap.tsx
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/base/options-wrap.tsx
rename to web/app/components/datasets/create/website/base/options-wrap.tsx
diff --git a/web/app/components/datasets/create/website/firecrawl/base/url-input.tsx b/web/app/components/datasets/create/website/base/url-input.tsx
similarity index 100%
rename from web/app/components/datasets/create/website/firecrawl/base/url-input.tsx
rename to web/app/components/datasets/create/website/base/url-input.tsx
diff --git a/web/app/components/datasets/create/website/firecrawl/index.tsx b/web/app/components/datasets/create/website/firecrawl/index.tsx
index de4f8bb129344..aa4dffc174315 100644
--- a/web/app/components/datasets/create/website/firecrawl/index.tsx
+++ b/web/app/components/datasets/create/website/firecrawl/index.tsx
@@ -2,13 +2,13 @@
import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import UrlInput from '../base/url-input'
+import OptionsWrap from '../base/options-wrap'
+import CrawledResult from '../base/crawled-result'
+import Crawling from '../base/crawling'
+import ErrorMessage from '../base/error-message'
import Header from './header'
-import UrlInput from './base/url-input'
-import OptionsWrap from './base/options-wrap'
import Options from './options'
-import CrawledResult from './crawled-result'
-import Crawling from './crawling'
-import ErrorMessage from './base/error-message'
import cn from '@/utils/classnames'
import { useModalContext } from '@/context/modal-context'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
diff --git a/web/app/components/datasets/create/website/firecrawl/options.tsx b/web/app/components/datasets/create/website/firecrawl/options.tsx
index 20cc4f073fe43..8cc2c6757c961 100644
--- a/web/app/components/datasets/create/website/firecrawl/options.tsx
+++ b/web/app/components/datasets/create/website/firecrawl/options.tsx
@@ -2,8 +2,8 @@
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
-import CheckboxWithLabel from './base/checkbox-with-label'
-import Field from './base/field'
+import CheckboxWithLabel from '../base/checkbox-with-label'
+import Field from '../base/field'
import cn from '@/utils/classnames'
import type { CrawlOptions } from '@/models/datasets'
diff --git a/web/app/components/datasets/create/website/index.module.css b/web/app/components/datasets/create/website/index.module.css
new file mode 100644
index 0000000000000..abaab4bea4b7a
--- /dev/null
+++ b/web/app/components/datasets/create/website/index.module.css
@@ -0,0 +1,6 @@
+.jinaLogo {
+ @apply w-4 h-4 bg-center bg-no-repeat inline-block;
+ background-color: #F5FAFF;
+ background-image: url(../assets/jina.png);
+ background-size: 16px;
+}
diff --git a/web/app/components/datasets/create/website/index.tsx b/web/app/components/datasets/create/website/index.tsx
index e06fbb4a1210b..58b7f5f2fd77b 100644
--- a/web/app/components/datasets/create/website/index.tsx
+++ b/web/app/components/datasets/create/website/index.tsx
@@ -1,8 +1,12 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import s from './index.module.css'
import NoData from './no-data'
import Firecrawl from './firecrawl'
+import JinaReader from './jina-reader'
+import cn from '@/utils/classnames'
import { useModalContext } from '@/context/modal-context'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fetchDataSources } from '@/service/datasets'
@@ -12,6 +16,7 @@ type Props = {
onPreview: (payload: CrawlResultItem) => void
checkedCrawlResult: CrawlResultItem[]
onCheckedCrawlResultChange: (payload: CrawlResultItem[]) => void
+ onCrawlProviderChange: (provider: DataSourceProvider) => void
onJobIdChange: (jobId: string) => void
crawlOptions: CrawlOptions
onCrawlOptionsChange: (payload: CrawlOptions) => void
@@ -21,17 +26,32 @@ const Website: FC
= ({
onPreview,
checkedCrawlResult,
onCheckedCrawlResultChange,
+ onCrawlProviderChange,
onJobIdChange,
crawlOptions,
onCrawlOptionsChange,
}) => {
+ const { t } = useTranslation()
const { setShowAccountSettingModal } = useModalContext()
const [isLoaded, setIsLoaded] = useState(false)
- const [isSetFirecrawlApiKey, setIsSetFirecrawlApiKey] = useState(false)
+ const [selectedProvider, setSelectedProvider] = useState(DataSourceProvider.jinaReader)
+ const [sources, setSources] = useState([])
+
+ useEffect(() => {
+ onCrawlProviderChange(selectedProvider)
+ }, [selectedProvider, onCrawlProviderChange])
+
const checkSetApiKey = useCallback(async () => {
const res = await fetchDataSources() as any
- const isFirecrawlSet = res.sources.some((item: DataSourceItem) => item.provider === DataSourceProvider.fireCrawl)
- setIsSetFirecrawlApiKey(isFirecrawlSet)
+ setSources(res.sources)
+
+ // If users have configured one of the providers, select it.
+ const availableProviders = res.sources.filter((item: DataSourceItem) =>
+ [DataSourceProvider.jinaReader, DataSourceProvider.fireCrawl].includes(item.provider),
+ )
+
+ if (availableProviders.length > 0)
+ setSelectedProvider(availableProviders[0].provider)
}, [])
useEffect(() => {
@@ -52,20 +72,66 @@ const Website: FC = ({
return (
- {isSetFirecrawlApiKey
- ? (
-
- )
- : (
-
- )}
+
+
+ {t('datasetCreation.stepOne.website.chooseProvider')}
+
+
+
+
+
+
+
+ {
+ selectedProvider === DataSourceProvider.fireCrawl
+ ? sources.find(source => source.provider === DataSourceProvider.fireCrawl)
+ ? (
+
+ )
+ : (
+
+ )
+ : sources.find(source => source.provider === DataSourceProvider.jinaReader)
+ ? (
+
+ )
+ : (
+
+ )
+ }
)
}
diff --git a/web/app/components/datasets/create/website/jina-reader/header.tsx b/web/app/components/datasets/create/website/jina-reader/header.tsx
new file mode 100644
index 0000000000000..85014a30ee2b1
--- /dev/null
+++ b/web/app/components/datasets/create/website/jina-reader/header.tsx
@@ -0,0 +1,42 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
+import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
+
+const I18N_PREFIX = 'datasetCreation.stepOne.website'
+
+type Props = {
+ onSetting: () => void
+}
+
+const Header: FC = ({
+ onSetting,
+}) => {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
+export default React.memo(Header)
diff --git a/web/app/components/datasets/create/website/jina-reader/index.tsx b/web/app/components/datasets/create/website/jina-reader/index.tsx
new file mode 100644
index 0000000000000..51d77d712140b
--- /dev/null
+++ b/web/app/components/datasets/create/website/jina-reader/index.tsx
@@ -0,0 +1,232 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import UrlInput from '../base/url-input'
+import OptionsWrap from '../base/options-wrap'
+import CrawledResult from '../base/crawled-result'
+import Crawling from '../base/crawling'
+import ErrorMessage from '../base/error-message'
+import Header from './header'
+import Options from './options'
+import cn from '@/utils/classnames'
+import { useModalContext } from '@/context/modal-context'
+import Toast from '@/app/components/base/toast'
+import { checkJinaReaderTaskStatus, createJinaReaderTask } from '@/service/datasets'
+import { sleep } from '@/utils'
+import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
+
+const ERROR_I18N_PREFIX = 'common.errorMsg'
+const I18N_PREFIX = 'datasetCreation.stepOne.website'
+
+type Props = {
+ onPreview: (payload: CrawlResultItem) => void
+ checkedCrawlResult: CrawlResultItem[]
+ onCheckedCrawlResultChange: (payload: CrawlResultItem[]) => void
+ onJobIdChange: (jobId: string) => void
+ crawlOptions: CrawlOptions
+ onCrawlOptionsChange: (payload: CrawlOptions) => void
+}
+
+enum Step {
+ init = 'init',
+ running = 'running',
+ finished = 'finished',
+}
+
+const JinaReader: FC = ({
+ onPreview,
+ checkedCrawlResult,
+ onCheckedCrawlResultChange,
+ onJobIdChange,
+ crawlOptions,
+ onCrawlOptionsChange,
+}) => {
+ const { t } = useTranslation()
+ const [step, setStep] = useState(Step.init)
+ const [controlFoldOptions, setControlFoldOptions] = useState(0)
+ useEffect(() => {
+ if (step !== Step.init)
+ setControlFoldOptions(Date.now())
+ }, [step])
+ const { setShowAccountSettingModal } = useModalContext()
+ const handleSetting = useCallback(() => {
+ setShowAccountSettingModal({
+ payload: 'data-source',
+ })
+ }, [setShowAccountSettingModal])
+
+ const checkValid = useCallback((url: string) => {
+ let errorMsg = ''
+ if (!url) {
+ errorMsg = t(`${ERROR_I18N_PREFIX}.fieldRequired`, {
+ field: 'url',
+ })
+ }
+
+ if (!errorMsg && !((url.startsWith('http://') || url.startsWith('https://'))))
+ errorMsg = t(`${ERROR_I18N_PREFIX}.urlError`)
+
+ if (!errorMsg && (crawlOptions.limit === null || crawlOptions.limit === undefined || crawlOptions.limit === '')) {
+ errorMsg = t(`${ERROR_I18N_PREFIX}.fieldRequired`, {
+ field: t(`${I18N_PREFIX}.limit`),
+ })
+ }
+
+ return {
+ isValid: !errorMsg,
+ errorMsg,
+ }
+ }, [crawlOptions, t])
+
+ const isInit = step === Step.init
+ const isCrawlFinished = step === Step.finished
+ const isRunning = step === Step.running
+ const [crawlResult, setCrawlResult] = useState<{
+ current: number
+ total: number
+ data: CrawlResultItem[]
+ time_consuming: number | string
+ } | undefined>(undefined)
+ const [crawlErrorMessage, setCrawlErrorMessage] = useState('')
+ const showError = isCrawlFinished && crawlErrorMessage
+
+ const waitForCrawlFinished = useCallback(async (jobId: string) => {
+ try {
+ const res = await checkJinaReaderTaskStatus(jobId) as any
+ console.log('res', res)
+ if (res.status === 'completed') {
+ return {
+ isError: false,
+ data: {
+ ...res,
+ total: Math.min(res.total, parseFloat(crawlOptions.limit as string)),
+ },
+ }
+ }
+ if (res.status === 'failed' || !res.status) {
+ return {
+ isError: true,
+ errorMessage: res.message,
+ data: {
+ data: [],
+ },
+ }
+ }
+ // update the progress
+ setCrawlResult({
+ ...res,
+ total: Math.min(res.total, parseFloat(crawlOptions.limit as string)),
+ })
+ onCheckedCrawlResultChange(res.data || []) // default select the crawl result
+ await sleep(2500)
+ return await waitForCrawlFinished(jobId)
+ }
+ catch (e: any) {
+ const errorBody = await e.json()
+ return {
+ isError: true,
+ errorMessage: errorBody.message,
+ data: {
+ data: [],
+ },
+ }
+ }
+ }, [crawlOptions.limit])
+
+ const handleRun = useCallback(async (url: string) => {
+ const { isValid, errorMsg } = checkValid(url)
+ if (!isValid) {
+ Toast.notify({
+ message: errorMsg!,
+ type: 'error',
+ })
+ return
+ }
+ setStep(Step.running)
+ try {
+ const startTime = Date.now()
+ const res = await createJinaReaderTask({
+ url,
+ options: crawlOptions,
+ }) as any
+
+ if (res.data) {
+ const data = {
+ current: 1,
+ total: 1,
+ data: [{
+ title: res.data.title,
+ markdown: res.data.content,
+ description: res.data.description,
+ source_url: res.data.url,
+ }],
+ time_consuming: (Date.now() - startTime) / 1000,
+ }
+ setCrawlResult(data)
+ onCheckedCrawlResultChange(data.data || [])
+ setCrawlErrorMessage('')
+ }
+ else if (res.job_id) {
+ const jobId = res.job_id
+ onJobIdChange(jobId)
+ const { isError, data, errorMessage } = await waitForCrawlFinished(jobId)
+ if (isError) {
+ setCrawlErrorMessage(errorMessage || t(`${I18N_PREFIX}.unknownError`))
+ }
+ else {
+ setCrawlResult(data)
+ onCheckedCrawlResultChange(data.data || []) // default select the crawl result
+ setCrawlErrorMessage('')
+ }
+ }
+ }
+ catch (e) {
+ setCrawlErrorMessage(t(`${I18N_PREFIX}.unknownError`)!)
+ console.log(e)
+ }
+ finally {
+ setStep(Step.finished)
+ }
+ }, [checkValid, crawlOptions, onJobIdChange, t, waitForCrawlFinished])
+
+ return (
+
+
+
+
+
+
+
+
+ {!isInit && (
+
+ {isRunning
+ && }
+ {showError && (
+
+ )}
+ {isCrawlFinished && !showError
+ &&
+ }
+
+ )}
+
+
+ )
+}
+export default React.memo(JinaReader)
diff --git a/web/app/components/datasets/create/website/jina-reader/options.tsx b/web/app/components/datasets/create/website/jina-reader/options.tsx
new file mode 100644
index 0000000000000..52cfaa8b3b40f
--- /dev/null
+++ b/web/app/components/datasets/create/website/jina-reader/options.tsx
@@ -0,0 +1,59 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import CheckboxWithLabel from '../base/checkbox-with-label'
+import Field from '../base/field'
+import cn from '@/utils/classnames'
+import type { CrawlOptions } from '@/models/datasets'
+
+const I18N_PREFIX = 'datasetCreation.stepOne.website'
+
+type Props = {
+ className?: string
+ payload: CrawlOptions
+ onChange: (payload: CrawlOptions) => void
+}
+
+const Options: FC = ({
+ className = '',
+ payload,
+ onChange,
+}) => {
+ const { t } = useTranslation()
+
+ const handleChange = useCallback((key: keyof CrawlOptions) => {
+ return (value: any) => {
+ onChange({
+ ...payload,
+ [key]: value,
+ })
+ }
+ }, [payload, onChange])
+ return (
+
+ )
+}
+export default React.memo(Options)
diff --git a/web/app/components/datasets/create/website/no-data.tsx b/web/app/components/datasets/create/website/no-data.tsx
index 13e5ee7dfbd50..8a508a48c6bb8 100644
--- a/web/app/components/datasets/create/website/no-data.tsx
+++ b/web/app/components/datasets/create/website/no-data.tsx
@@ -2,35 +2,56 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
+import s from './index.module.css'
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
import Button from '@/app/components/base/button'
+import { DataSourceProvider } from '@/models/common'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
type Props = {
onConfig: () => void
+ provider: DataSourceProvider
}
const NoData: FC = ({
onConfig,
+ provider,
}) => {
const { t } = useTranslation()
+ const providerConfig = {
+ [DataSourceProvider.jinaReader]: {
+ emoji: ,
+ title: t(`${I18N_PREFIX}.jinaReaderNotConfigured`),
+ description: t(`${I18N_PREFIX}.jinaReaderNotConfiguredDescription`),
+ },
+ [DataSourceProvider.fireCrawl]: {
+ emoji: '🔥',
+ title: t(`${I18N_PREFIX}.fireCrawlNotConfigured`),
+ description: t(`${I18N_PREFIX}.fireCrawlNotConfiguredDescription`),
+ },
+ }
+
+ const currentProvider = providerConfig[provider]
+
return (
-
-
- 🔥
-
-
-
{t(`${I18N_PREFIX}.fireCrawlNotConfigured`)}
-
- {t(`${I18N_PREFIX}.fireCrawlNotConfiguredDescription`)}
+ <>
+
+
+ {currentProvider.emoji}
+
+
+
{currentProvider.title}
+
+ {currentProvider.description}
+
+
-
-
+ >
)
}
export default React.memo(NoData)
diff --git a/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx b/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx
index c65b244f6d315..5b76acc9360c6 100644
--- a/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx
+++ b/web/app/components/datasets/documents/detail/completed/SegmentCard.tsx
@@ -36,6 +36,12 @@ export type UsageScene = 'doc' | 'hitTesting'
type ISegmentCardProps = {
loading: boolean
detail?: SegmentDetailModel & { document: { name: string } }
+ contentExternal?: string
+ refSource?: {
+ title: string
+ uri: string
+ }
+ isExternal?: boolean
score?: number
onClick?: () => void
onChangeSwitch?: (segId: string, enabled: boolean) => Promise
@@ -48,6 +54,9 @@ type ISegmentCardProps = {
const SegmentCard: FC = ({
detail = {},
+ contentExternal,
+ isExternal,
+ refSource,
score,
onClick,
onChangeSwitch,
@@ -88,6 +97,9 @@ const SegmentCard: FC = ({
)
}
+ if (contentExternal)
+ return contentExternal
+
return content
}
@@ -199,16 +211,16 @@ const SegmentCard: FC = ({
-
+
- {t('datasetHitTesting.viewChart')}
+ {isExternal ? t('datasetHitTesting.viewDetail') : t('datasetHitTesting.viewChart')}
diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx
index 540474e7a5739..0e0eebb034df5 100644
--- a/web/app/components/datasets/documents/list.tsx
+++ b/web/app/components/datasets/documents/list.tsx
@@ -122,6 +122,7 @@ export const OperationAction: FC<{
}> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '' }) => {
const { id, enabled = false, archived = false, data_source_type } = detail || {}
const [showModal, setShowModal] = useState(false)
+ const [deleting, setDeleting] = useState(false)
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
const router = useRouter()
@@ -153,6 +154,7 @@ export const OperationAction: FC<{
break
default:
opApi = deleteDocument
+ setDeleting(true)
break
}
const [e] = await asyncRunSafe
(opApi({ datasetId, documentId: id }) as Promise)
@@ -160,6 +162,8 @@ export const OperationAction: FC<{
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
else
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
+ if (operationName === 'delete')
+ setDeleting(false)
onUpdate(operationName)
}
@@ -295,6 +299,8 @@ export const OperationAction: FC<{
{showModal
&& void
+ formSchemas: FormSchema[]
+ inputClassName?: string
+}
+
+const Form: FC = React.memo(({
+ className,
+ itemClassName,
+ fieldLabelClassName,
+ value,
+ onChange,
+ formSchemas,
+ inputClassName,
+}) => {
+ const { t, i18n } = useTranslation()
+ const [changeKey, setChangeKey] = useState('')
+
+ const handleFormChange = (key: string, val: string) => {
+ setChangeKey(key)
+ if (key === 'name') {
+ onChange({ ...value, [key]: val })
+ }
+ else {
+ onChange({
+ ...value,
+ settings: {
+ ...value.settings,
+ [key]: val,
+ },
+ })
+ }
+ }
+
+ const renderField = (formSchema: FormSchema) => {
+ const { variable, type, label, required } = formSchema
+ const fieldValue = variable === 'name' ? value[variable] : (value.settings[variable as keyof typeof value.settings] || '')
+
+ return (
+
+
+
handleFormChange(variable, val.target.value)}
+ required={required}
+ className={cn(inputClassName)}
+ />
+
+ )
+ }
+
+ return (
+
+ )
+})
+
+export default Form
diff --git a/web/app/components/datasets/external-api/external-api-modal/index.tsx b/web/app/components/datasets/external-api/external-api-modal/index.tsx
new file mode 100644
index 0000000000000..340d147a505bd
--- /dev/null
+++ b/web/app/components/datasets/external-api/external-api-modal/index.tsx
@@ -0,0 +1,218 @@
+import type { FC } from 'react'
+import {
+ memo,
+ useEffect,
+ useState,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ RiBook2Line,
+ RiCloseLine,
+ RiInformation2Line,
+ RiLock2Fill,
+} from '@remixicon/react'
+import type { CreateExternalAPIReq, FormSchema } from '../declarations'
+import Form from './Form'
+import ActionButton from '@/app/components/base/action-button'
+import Confirm from '@/app/components/base/confirm'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+} from '@/app/components/base/portal-to-follow-elem'
+import { createExternalAPI } from '@/service/datasets'
+import { useToastContext } from '@/app/components/base/toast'
+import Button from '@/app/components/base/button'
+import Tooltip from '@/app/components/base/tooltip'
+
+type AddExternalAPIModalProps = {
+ data?: CreateExternalAPIReq
+ onSave: (formValue: CreateExternalAPIReq) => void
+ onCancel: () => void
+ onEdit?: (formValue: CreateExternalAPIReq) => Promise
+ datasetBindings?: { id: string; name: string }[]
+ isEditMode: boolean
+}
+
+const formSchemas: FormSchema[] = [
+ {
+ variable: 'name',
+ type: 'text',
+ label: {
+ en_US: 'Name',
+ },
+ required: true,
+ },
+ {
+ variable: 'endpoint',
+ type: 'text',
+ label: {
+ en_US: 'API Endpoint',
+ },
+ required: true,
+ },
+ {
+ variable: 'api_key',
+ type: 'secret',
+ label: {
+ en_US: 'API Key',
+ },
+ required: true,
+ },
+]
+
+const AddExternalAPIModal: FC = ({ data, onSave, onCancel, datasetBindings, isEditMode, onEdit }) => {
+ const { t } = useTranslation()
+ const { notify } = useToastContext()
+ const [loading, setLoading] = useState(false)
+ const [showConfirm, setShowConfirm] = useState(false)
+ const [formData, setFormData] = useState({ name: '', settings: { endpoint: '', api_key: '' } })
+
+ useEffect(() => {
+ if (isEditMode && data)
+ setFormData(data)
+ }, [isEditMode, data])
+
+ const hasEmptyInputs = Object.values(formData).some(value =>
+ typeof value === 'string' ? value.trim() === '' : Object.values(value).some(v => v.trim() === ''),
+ )
+ const handleDataChange = (val: CreateExternalAPIReq) => {
+ setFormData(val)
+ }
+
+ const handleSave = async () => {
+ if (formData && formData.settings.api_key && formData.settings.api_key?.length < 5) {
+ notify({ type: 'error', message: t('common.apiBasedExtension.modal.apiKey.lengthError') })
+ setLoading(false)
+ return
+ }
+ try {
+ setLoading(true)
+ if (isEditMode && onEdit) {
+ await onEdit(
+ {
+ ...formData,
+ settings: { ...formData.settings, api_key: formData.settings.api_key ? '[__HIDDEN__]' : formData.settings.api_key },
+ },
+ )
+ notify({ type: 'success', message: 'External API updated successfully' })
+ }
+ else {
+ const res = await createExternalAPI({ body: formData })
+ if (res && res.id) {
+ notify({ type: 'success', message: 'External API saved successfully' })
+ onSave(res)
+ }
+ }
+ onCancel()
+ }
+ catch (error) {
+ console.error('Error saving/updating external API:', error)
+ notify({ type: 'error', message: 'Failed to save/update External API' })
+ }
+ finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ {
+ isEditMode ? t('dataset.editExternalAPIFormTitle') : t('dataset.createExternalAPI')
+ }
+
+ {isEditMode && (datasetBindings?.length ?? 0) > 0 && (
+
+ {t('dataset.editExternalAPIFormWarning.front')}
+
+ {datasetBindings?.length} {t('dataset.editExternalAPIFormWarning.end')}
+
+
+
{`${datasetBindings?.length} ${t('dataset.editExternalAPITooltipTitle')}`}
+
+ {datasetBindings?.map(binding => (
+
+ ))}
+
+ }
+ asChild={false}
+ position='bottom'
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {t('dataset.externalAPIForm.encrypted.front')}
+
+ PKCS1_OAEP
+
+ {t('dataset.externalAPIForm.encrypted.end')}
+
+
+ {showConfirm && (datasetBindings?.length ?? 0) > 0 && (
+ setShowConfirm(false)}
+ onConfirm={handleSave}
+ />
+ )}
+
+
+
+ )
+}
+
+export default memo(AddExternalAPIModal)
diff --git a/web/app/components/datasets/external-api/external-api-panel/index.tsx b/web/app/components/datasets/external-api/external-api-panel/index.tsx
new file mode 100644
index 0000000000000..044c008b12a60
--- /dev/null
+++ b/web/app/components/datasets/external-api/external-api-panel/index.tsx
@@ -0,0 +1,90 @@
+import React from 'react'
+import {
+ RiAddLine,
+ RiBookOpenLine,
+ RiCloseLine,
+} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import ExternalKnowledgeAPICard from '../external-knowledge-api-card'
+import cn from '@/utils/classnames'
+import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
+import ActionButton from '@/app/components/base/action-button'
+import Button from '@/app/components/base/button'
+import Loading from '@/app/components/base/loading'
+import { useModalContext } from '@/context/modal-context'
+
+type ExternalAPIPanelProps = {
+ onClose: () => void
+}
+
+const ExternalAPIPanel: React.FC
= ({ onClose }) => {
+ const { t } = useTranslation()
+ const { setShowExternalKnowledgeAPIModal } = useModalContext()
+ const { externalKnowledgeApiList, mutateExternalKnowledgeApis, isLoading } = useExternalKnowledgeApi()
+
+ const handleOpenExternalAPIModal = () => {
+ setShowExternalKnowledgeAPIModal({
+ payload: { name: '', settings: { endpoint: '', api_key: '' } },
+ datasetBindings: [],
+ onSaveCallback: () => {
+ mutateExternalKnowledgeApis()
+ },
+ onCancelCallback: () => {
+ mutateExternalKnowledgeApis()
+ },
+ isEditMode: false,
+ })
+ }
+
+ return (
+
+
+
+
+
+
+
+ {isLoading
+ ? (
+
+ )
+ : (
+ externalKnowledgeApiList.map(api => (
+
+ ))
+ )}
+
+
+
+ )
+}
+
+export default ExternalAPIPanel
diff --git a/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx
new file mode 100644
index 0000000000000..603b4fe7cb228
--- /dev/null
+++ b/web/app/components/datasets/external-api/external-knowledge-api-card/index.tsx
@@ -0,0 +1,151 @@
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ RiDeleteBinLine,
+ RiEditLine,
+} from '@remixicon/react'
+import type { CreateExternalAPIReq } from '../declarations'
+import type { ExternalAPIItem } from '@/models/datasets'
+import { checkUsageExternalAPI, deleteExternalAPI, fetchExternalAPI, updateExternalAPI } from '@/service/datasets'
+import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
+import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
+import { useModalContext } from '@/context/modal-context'
+import ActionButton from '@/app/components/base/action-button'
+import Confirm from '@/app/components/base/confirm'
+
+type ExternalKnowledgeAPICardProps = {
+ api: ExternalAPIItem
+}
+
+const ExternalKnowledgeAPICard: React.FC = ({ api }) => {
+ const { setShowExternalKnowledgeAPIModal } = useModalContext()
+ const [showConfirm, setShowConfirm] = useState(false)
+ const [isHovered, setIsHovered] = useState(false)
+ const [usageCount, setUsageCount] = useState(0)
+ const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
+
+ const { t } = useTranslation()
+
+ const handleEditClick = async () => {
+ try {
+ const response = await fetchExternalAPI({ apiTemplateId: api.id })
+ const formValue: CreateExternalAPIReq = {
+ name: response.name,
+ settings: {
+ endpoint: response.settings.endpoint,
+ api_key: response.settings.api_key,
+ },
+ }
+
+ setShowExternalKnowledgeAPIModal({
+ payload: formValue,
+ onSaveCallback: () => {
+ mutateExternalKnowledgeApis()
+ },
+ onCancelCallback: () => {
+ mutateExternalKnowledgeApis()
+ },
+ isEditMode: true,
+ datasetBindings: response.dataset_bindings,
+ onEditCallback: async (updatedData: CreateExternalAPIReq) => {
+ try {
+ await updateExternalAPI({
+ apiTemplateId: api.id,
+ body: {
+ ...response,
+ name: updatedData.name,
+ settings: {
+ ...response.settings,
+ endpoint: updatedData.settings.endpoint,
+ api_key: updatedData.settings.api_key,
+ },
+ },
+ })
+ mutateExternalKnowledgeApis()
+ }
+ catch (error) {
+ console.error('Error updating external knowledge API:', error)
+ }
+ },
+ })
+ }
+ catch (error) {
+ console.error('Error fetching external knowledge API data:', error)
+ }
+ }
+
+ const handleDeleteClick = async () => {
+ try {
+ const usage = await checkUsageExternalAPI({ apiTemplateId: api.id })
+ if (usage.is_using)
+ setUsageCount(usage.count)
+
+ setShowConfirm(true)
+ }
+ catch (error) {
+ console.error('Error checking external API usage:', error)
+ }
+ }
+
+ const handleConfirmDelete = async () => {
+ try {
+ const response = await deleteExternalAPI({ apiTemplateId: api.id })
+ if (response && response.result === 'success') {
+ setShowConfirm(false)
+ mutateExternalKnowledgeApis()
+ }
+ else {
+ console.error('Failed to delete external API')
+ }
+ }
+ catch (error) {
+ console.error('Error deleting external knowledge API:', error)
+ }
+ }
+
+ return (
+ <>
+
+
+
+
{api.settings.endpoint}
+
+
+
+
+
+
setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
+ {showConfirm && (
+ 0
+ ? `${t('dataset.deleteExternalAPIConfirmWarningContent.content.front')} ${usageCount} ${t('dataset.deleteExternalAPIConfirmWarningContent.content.end')}`
+ : t('dataset.deleteExternalAPIConfirmWarningContent.noConnectionContent')
+ }
+ type='warning'
+ onConfirm={handleConfirmDelete}
+ onCancel={() => setShowConfirm(false)}
+ />
+ )}
+ >
+ )
+}
+
+export default ExternalKnowledgeAPICard
diff --git a/web/app/components/datasets/external-knowledge-base/connector/index.tsx b/web/app/components/datasets/external-knowledge-base/connector/index.tsx
new file mode 100644
index 0000000000000..33f57d0b47dbf
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/connector/index.tsx
@@ -0,0 +1,36 @@
+'use client'
+
+import React, { useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { useToastContext } from '@/app/components/base/toast'
+import ExternalKnowledgeBaseCreate from '@/app/components/datasets/external-knowledge-base/create'
+import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
+import { createExternalKnowledgeBase } from '@/service/datasets'
+
+const ExternalKnowledgeBaseConnector = () => {
+ const { notify } = useToastContext()
+ const [loading, setLoading] = useState(false)
+ const router = useRouter()
+
+ const handleConnect = async (formValue: CreateKnowledgeBaseReq) => {
+ try {
+ setLoading(true)
+ const result = await createExternalKnowledgeBase({ body: formValue })
+ if (result && result.id) {
+ notify({ type: 'success', message: 'External Knowledge Base Connected Successfully' })
+ router.back()
+ }
+ else { throw new Error('Failed to create external knowledge base') }
+ }
+ catch (error) {
+ console.error('Error creating external knowledge base:', error)
+ notify({ type: 'error', message: 'Failed to connect External Knowledge Base' })
+ }
+ setLoading(false)
+ }
+ return (
+
+ )
+}
+
+export default ExternalKnowledgeBaseConnector
diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx
new file mode 100644
index 0000000000000..a6a46479a4eb4
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx
@@ -0,0 +1,110 @@
+import React, { useEffect, useState } from 'react'
+import {
+ RiAddLine,
+ RiArrowDownSLine,
+} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useRouter } from 'next/navigation'
+import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
+import { useModalContext } from '@/context/modal-context'
+import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
+
+type ApiItem = {
+ value: string
+ name: string
+ url: string
+}
+
+type ExternalApiSelectProps = {
+ items: ApiItem[]
+ value?: string
+ onSelect: (item: ApiItem) => void
+}
+
+const ExternalApiSelect: React.FC = ({ items, value, onSelect }) => {
+ const { t } = useTranslation()
+ const [isOpen, setIsOpen] = useState(false)
+ const [selectedItem, setSelectedItem] = useState(
+ items.find(item => item.value === value) || null,
+ )
+ const { setShowExternalKnowledgeAPIModal } = useModalContext()
+ const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
+ const router = useRouter()
+
+ useEffect(() => {
+ const newSelectedItem = items.find(item => item.value === value) || null
+ setSelectedItem(newSelectedItem)
+ }, [value, items])
+
+ const handleAddNewAPI = () => {
+ setShowExternalKnowledgeAPIModal({
+ payload: { name: '', settings: { endpoint: '', api_key: '' } },
+ onSaveCallback: async () => {
+ mutateExternalKnowledgeApis()
+ router.refresh()
+ },
+ onCancelCallback: () => {
+ mutateExternalKnowledgeApis()
+ },
+ isEditMode: false,
+ })
+ }
+
+ const handleSelect = (item: ApiItem) => {
+ setSelectedItem(item)
+ onSelect(item)
+ setIsOpen(false)
+ }
+
+ return (
+
+
setIsOpen(!isOpen)}
+ >
+ {selectedItem
+ ? (
+
+
+
+ {selectedItem.name}
+
+
+ )
+ : (
+
{t('dataset.selectExternalKnowledgeAPI.placeholder')}
+ )}
+
+
+ {isOpen && (
+
+ {items.map(item => (
+
handleSelect(item)}
+ >
+
+
+
{item.name}
+
{item.url}
+
+
+ ))}
+
+
+
+ {t('dataset.createNewExternalAPI')}
+
+
+
+ )}
+
+ )
+}
+
+export default ExternalApiSelect
diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx
new file mode 100644
index 0000000000000..c910d9b2a7e7e
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx
@@ -0,0 +1,96 @@
+'use client'
+
+import React, { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { RiAddLine } from '@remixicon/react'
+import { useRouter } from 'next/navigation'
+import ExternalApiSelect from './ExternalApiSelect'
+import Input from '@/app/components/base/input'
+import Button from '@/app/components/base/button'
+import { useModalContext } from '@/context/modal-context'
+import { useExternalKnowledgeApi } from '@/context/external-knowledge-api-context'
+
+type ExternalApiSelectionProps = {
+ external_knowledge_api_id: string
+ external_knowledge_id: string
+ onChange: (data: { external_knowledge_api_id?: string; external_knowledge_id?: string }) => void
+}
+
+const ExternalApiSelection: React.FC = ({ external_knowledge_api_id, external_knowledge_id, onChange }) => {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const { externalKnowledgeApiList } = useExternalKnowledgeApi()
+ const [selectedApiId, setSelectedApiId] = useState(external_knowledge_api_id)
+ const { setShowExternalKnowledgeAPIModal } = useModalContext()
+ const { mutateExternalKnowledgeApis } = useExternalKnowledgeApi()
+
+ const apiItems = externalKnowledgeApiList.map(api => ({
+ value: api.id,
+ name: api.name,
+ url: api.settings.endpoint,
+ }))
+
+ useEffect(() => {
+ if (apiItems.length > 0) {
+ const newSelectedId = external_knowledge_api_id || apiItems[0].value
+ setSelectedApiId(newSelectedId)
+ if (newSelectedId !== external_knowledge_api_id)
+ onChange({ external_knowledge_api_id: newSelectedId, external_knowledge_id })
+ }
+ }, [apiItems, external_knowledge_api_id, external_knowledge_id, onChange])
+
+ const handleAddNewAPI = () => {
+ setShowExternalKnowledgeAPIModal({
+ payload: { name: '', settings: { endpoint: '', api_key: '' } },
+ onSaveCallback: async () => {
+ mutateExternalKnowledgeApis()
+ router.refresh()
+ },
+ onCancelCallback: () => {
+ mutateExternalKnowledgeApis()
+ },
+ isEditMode: false,
+ })
+ }
+
+ useEffect(() => {
+ if (!external_knowledge_api_id && apiItems.length > 0)
+ onChange({ external_knowledge_api_id: apiItems[0].value, external_knowledge_id })
+ }, [])
+
+ return (
+
+ )
+}
+
+export default ExternalApiSelection
diff --git a/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx
new file mode 100644
index 0000000000000..bd32683c8579c
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx
@@ -0,0 +1,33 @@
+import { RiBookOpenLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+
+const InfoPanel = () => {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
+
+export default InfoPanel
diff --git a/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx
new file mode 100644
index 0000000000000..fec526b8811f7
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/create/KnowledgeBaseInfo.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import Input from '@/app/components/base/input'
+
+type KnowledgeBaseInfoProps = {
+ name: string
+ description?: string
+ onChange: (data: { name?: string; description?: string }) => void
+}
+
+const KnowledgeBaseInfo: React.FC = ({ name, description, onChange }) => {
+ const { t } = useTranslation()
+
+ const handleNameChange = (e: React.ChangeEvent) => {
+ onChange({ name: e.target.value })
+ }
+
+ const handleDescriptionChange = (e: React.ChangeEvent) => {
+ onChange({ description: e.target.value })
+ }
+
+ return (
+
+ )
+}
+
+export default KnowledgeBaseInfo
diff --git a/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx b/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx
new file mode 100644
index 0000000000000..d501dde271e54
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/create/RetrievalSettings.tsx
@@ -0,0 +1,67 @@
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import TopKItem from '@/app/components/base/param-item/top-k-item'
+import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
+import cn from '@/utils/classnames'
+
+type RetrievalSettingsProps = {
+ topK: number
+ scoreThreshold: number
+ scoreThresholdEnabled: boolean
+ isInHitTesting?: boolean
+ isInRetrievalSetting?: boolean
+ onChange: (data: { top_k?: number; score_threshold?: number; score_threshold_enabled?: boolean }) => void
+}
+
+const RetrievalSettings: FC = ({
+ topK,
+ scoreThreshold,
+ scoreThresholdEnabled,
+ onChange,
+ isInHitTesting = false,
+ isInRetrievalSetting = false,
+}) => {
+ const { t } = useTranslation()
+
+ const handleScoreThresholdChange = (enabled: boolean) => {
+ onChange({ score_threshold_enabled: enabled })
+ }
+
+ return (
+
+ {!isInHitTesting && !isInRetrievalSetting &&
+
+
}
+
+
+ onChange({ top_k: v })}
+ enable={true}
+ />
+
+
+ onChange({ score_threshold: v })}
+ enable={scoreThresholdEnabled}
+ hasSwitch={true}
+ onSwitchChange={(_key, v) => handleScoreThresholdChange(v)}
+ />
+
+
+
+ )
+}
+
+export default RetrievalSettings
diff --git a/web/app/components/datasets/external-knowledge-base/create/declarations.ts b/web/app/components/datasets/external-knowledge-base/create/declarations.ts
new file mode 100644
index 0000000000000..271caf33dfbbe
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/create/declarations.ts
@@ -0,0 +1,12 @@
+export type CreateKnowledgeBaseReq = {
+ name: string
+ description?: string
+ external_knowledge_api_id: string
+ provider: 'external'
+ external_knowledge_id: string
+ external_retrieval_model: {
+ top_k: number
+ score_threshold: number
+ score_threshold_enabled: boolean
+ }
+}
diff --git a/web/app/components/datasets/external-knowledge-base/create/index.tsx b/web/app/components/datasets/external-knowledge-base/create/index.tsx
new file mode 100644
index 0000000000000..921050e218d20
--- /dev/null
+++ b/web/app/components/datasets/external-knowledge-base/create/index.tsx
@@ -0,0 +1,128 @@
+'use client'
+
+import { useCallback, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import { RiArrowLeftLine, RiArrowRightLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import KnowledgeBaseInfo from './KnowledgeBaseInfo'
+import ExternalApiSelection from './ExternalApiSelection'
+import RetrievalSettings from './RetrievalSettings'
+import InfoPanel from './InfoPanel'
+import type { CreateKnowledgeBaseReq } from './declarations'
+import Divider from '@/app/components/base/divider'
+import Button from '@/app/components/base/button'
+
+type ExternalKnowledgeBaseCreateProps = {
+ onConnect: (formValue: CreateKnowledgeBaseReq) => void
+ loading: boolean
+}
+
+const ExternalKnowledgeBaseCreate: React.FC = ({ onConnect, loading }) => {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ external_knowledge_api_id: '',
+ external_knowledge_id: '',
+ external_retrieval_model: {
+ top_k: 2,
+ score_threshold: 0.5,
+ score_threshold_enabled: false,
+ },
+ provider: 'external',
+
+ })
+
+ const navBackHandle = useCallback(() => {
+ router.replace('/datasets')
+ }, [router])
+
+ const handleFormChange = (newData: CreateKnowledgeBaseReq) => {
+ setFormData(newData)
+ }
+
+ const isFormValid = formData.name.trim() !== ''
+ && formData.external_knowledge_api_id !== ''
+ && formData.external_knowledge_id !== ''
+ && formData.external_retrieval_model.top_k !== undefined
+ && formData.external_retrieval_model.score_threshold !== undefined
+
+ return (
+
+
+
+
+
+
{t('dataset.connectDataset')}
+
+ {t('dataset.connectHelper.helper1')}
+ {t('dataset.connectHelper.helper2')}
+ {t('dataset.connectHelper.helper3')}
+
+ {t('dataset.connectHelper.helper4')}
+
+ {t('dataset.connectHelper.helper5')}
+
+
+
+
handleFormChange({
+ ...formData,
+ ...data,
+ })}
+ />
+
+ handleFormChange({
+ ...formData,
+ ...data,
+ })}
+ />
+ handleFormChange({
+ ...formData,
+ external_retrieval_model: {
+ ...formData.external_retrieval_model,
+ ...data,
+ },
+ })}
+ />
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default ExternalKnowledgeBaseCreate
diff --git a/web/app/components/datasets/hit-testing/hit-detail.tsx b/web/app/components/datasets/hit-testing/hit-detail.tsx
index 70e43176d9da2..066e2238c8a0a 100644
--- a/web/app/components/datasets/hit-testing/hit-detail.tsx
+++ b/web/app/components/datasets/hit-testing/hit-detail.tsx
@@ -26,12 +26,15 @@ const HitDetail: FC = ({ segInfo }) => {
)
}
- return segInfo?.content
+ return {segInfo?.content}
}
return (
-
-
+ segInfo?.id === 'external'
+ ?
+ :
-
)
}
diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx
index 505cd98fa7de5..ce47f2bfa6c8c 100644
--- a/web/app/components/datasets/hit-testing/index.tsx
+++ b/web/app/components/datasets/hit-testing/index.tsx
@@ -13,7 +13,7 @@ import s from './style.module.css'
import HitDetail from './hit-detail'
import ModifyRetrievalModal from './modify-retrieval-modal'
import cn from '@/utils/classnames'
-import type { HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
+import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import Drawer from '@/app/components/base/drawer'
@@ -49,8 +49,10 @@ const HitTesting: FC
= ({ datasetId }: Props) => {
const isMobile = media === MediaType.mobile
const [hitResult, setHitResult] = useState() // 初始化记录为空数组
+ const [externalHitResult, setExternalHitResult] = useState()
const [submitLoading, setSubmitLoading] = useState(false)
const [currParagraph, setCurrParagraph] = useState<{ paraInfo?: HitTestingType; showModal: boolean }>({ showModal: false })
+ const [externalCurrParagraph, setExternalCurrParagraph] = useState<{ paraInfo?: ExternalKnowledgeBaseHitTestingType; showModal: boolean }>({ showModal: false })
const [text, setText] = useState('')
const [currPage, setCurrPage] = React.useState(0)
@@ -66,12 +68,52 @@ const HitTesting: FC = ({ datasetId }: Props) => {
setCurrParagraph({ paraInfo: detail, showModal: true })
}
+ const onClickExternalCard = (detail: ExternalKnowledgeBaseHitTestingType) => {
+ setExternalCurrParagraph({ paraInfo: detail, showModal: true })
+ }
const { dataset: currentDataset } = useContext(DatasetDetailContext)
+ const isExternal = currentDataset?.provider === 'external'
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false)
const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile)
+ const renderHitResults = (results: any[], onClickCard: (record: any) => void) => (
+ <>
+ {t('datasetHitTesting.hit.title')}
+
+
+ {results.map((record, idx) => (
+ onClickCard(record)}
+ />
+ ))}
+
+
+ >
+ )
+
+ const renderEmptyState = () => (
+
+
+
+ {t('datasetHitTesting.hit.emptyTip')}
+
+
+ )
+
useEffect(() => {
setShowRightPanel(!isMobile)
}, [isMobile, setShowRightPanel])
@@ -86,12 +128,14 @@ const HitTesting: FC = ({ datasetId }: Props) => {
- : !hitResult?.records.length
- ? (
-
-
-
- {t('datasetHitTesting.hit.emptyTip')}
-
-
- )
- : (
- <>
- {t('datasetHitTesting.hit.title')}
-
-
- {hitResult?.records.map((record, idx) => {
- return onClickCard(record as any)}
- />
- })}
-
-
- >
- )
+ : (
+ (() => {
+ if (!hitResult?.records.length && !externalHitResult?.records.length)
+ return renderEmptyState()
+
+ if (hitResult?.records.length)
+ return renderHitResults(hitResult.records, onClickCard)
+
+ return renderHitResults(externalHitResult?.records || [], onClickExternalCard)
+ })()
+ )
}
setCurrParagraph({ showModal: false })}
- isShow={currParagraph.showModal}
+ onClose={() => {
+ setCurrParagraph({ showModal: false })
+ setExternalCurrParagraph({ showModal: false })
+ }}
+ isShow={currParagraph.showModal || externalCurrParagraph.showModal}
>
- {currParagraph.showModal && }
+ {currParagraph.showModal && (
+
+ )}
+ {externalCurrParagraph.showModal && (
+
+ )}
setIsShowModifyRetrievalModal(false)} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
void
+ onSave: (data: { top_k: number; score_threshold: number; score_threshold_enabled: boolean }) => void
+ initialTopK: number
+ initialScoreThreshold: number
+ initialScoreThresholdEnabled: boolean
+}
+
+const ModifyExternalRetrievalModal: React.FC = ({
+ onClose,
+ onSave,
+ initialTopK,
+ initialScoreThreshold,
+ initialScoreThresholdEnabled,
+}) => {
+ const { t } = useTranslation()
+ const [topK, setTopK] = useState(initialTopK)
+ const [scoreThreshold, setScoreThreshold] = useState(initialScoreThreshold)
+ const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(initialScoreThresholdEnabled)
+
+ const handleSettingsChange = (data: { top_k?: number; score_threshold?: number; score_threshold_enabled?: boolean }) => {
+ if (data.top_k !== undefined)
+ setTopK(data.top_k)
+ if (data.score_threshold !== undefined)
+ setScoreThreshold(data.score_threshold)
+ if (data.score_threshold_enabled !== undefined)
+ setScoreThresholdEnabled(data.score_threshold_enabled)
+ }
+
+ const handleSave = () => {
+ onSave({ top_k: topK, score_threshold: scoreThreshold, score_threshold_enabled: scoreThresholdEnabled })
+ onClose()
+ }
+
+ return (
+
+
+
{t('datasetHitTesting.settingTitle')}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default ModifyExternalRetrievalModal
diff --git a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx
index 999f1cdf0d2b0..1fc5b68d671eb 100644
--- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx
+++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx
@@ -77,7 +77,7 @@ const ModifyRetrievalModal: FC = ({
{t('datasetSettings.form.retrievalSetting.title')}
diff --git a/web/app/components/datasets/hit-testing/textarea.tsx b/web/app/components/datasets/hit-testing/textarea.tsx
index 59f346db4a79f..9f8b55f6c6af7 100644
--- a/web/app/components/datasets/hit-testing/textarea.tsx
+++ b/web/app/components/datasets/hit-testing/textarea.tsx
@@ -1,12 +1,17 @@
+import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
+import {
+ RiEqualizer2Line,
+} from '@remixicon/react'
import Button from '../../base/button'
import Tag from '../../base/tag'
import { getIcon } from '../common/retrieval-method-info'
import s from './style.module.css'
+import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
-import type { HitTestingResponse } from '@/models/datasets'
-import { hitTesting } from '@/service/datasets'
+import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from '@/models/datasets'
+import { externalKnowledgeBaseHitTesting, hitTesting } from '@/service/datasets'
import { asyncRunSafe } from '@/utils'
import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
@@ -14,10 +19,12 @@ type TextAreaWithButtonIProps = {
datasetId: string
onUpdateList: () => void
setHitResult: (res: HitTestingResponse) => void
+ setExternalHitResult: (res: ExternalKnowledgeBaseHitTestingResponse) => void
loading: boolean
setLoading: (v: boolean) => void
text: string
setText: (v: string) => void
+ isExternal?: boolean
onClickRetrievalMethod: () => void
retrievalConfig: RetrievalConfig
isEconomy: boolean
@@ -28,16 +35,29 @@ const TextAreaWithButton = ({
datasetId,
onUpdateList,
setHitResult,
+ setExternalHitResult,
setLoading,
loading,
text,
setText,
+ isExternal = false,
onClickRetrievalMethod,
retrievalConfig,
isEconomy,
onSubmit: _onSubmit,
}: TextAreaWithButtonIProps) => {
const { t } = useTranslation()
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false)
+ const [externalRetrievalSettings, setExternalRetrievalSettings] = useState({
+ top_k: 2,
+ score_threshold: 0.5,
+ score_threshold_enabled: false,
+ })
+
+ const handleSaveExternalRetrievalSettings = (data: { top_k: number; score_threshold: number; score_threshold_enabled: boolean }) => {
+ setExternalRetrievalSettings(data)
+ setIsSettingsOpen(false)
+ }
function handleTextChange(event: any) {
setText(event.target.value)
@@ -63,28 +83,70 @@ const TextAreaWithButton = ({
_onSubmit && _onSubmit()
}
+ const externalRetrievalTestingOnSubmit = async () => {
+ const [e, res] = await asyncRunSafe(
+ externalKnowledgeBaseHitTesting({
+ datasetId,
+ query: text,
+ external_retrieval_model: {
+ top_k: externalRetrievalSettings.top_k,
+ score_threshold: externalRetrievalSettings.score_threshold,
+ score_threshold_enabled: externalRetrievalSettings.score_threshold_enabled,
+ },
+ }) as Promise,
+ )
+ if (!e) {
+ setExternalHitResult(res)
+ onUpdateList?.()
+ }
+ setLoading(false)
+ }
+
const retrievalMethod = isEconomy ? RETRIEVE_METHOD.invertedIndex : retrievalConfig.search_method
const Icon = getIcon(retrievalMethod)
return (
<>
-
+
{t('datasetHitTesting.input.title')}
-
- setIsSettingsOpen(!isSettingsOpen)}
>
-
-
{t(`dataset.retrieval.${retrievalMethod}.title`)}
-
-
+
+
+ {t('datasetHitTesting.settingTitle')}
+
+
+ :
+
+
+
{t(`dataset.retrieval.${retrievalMethod}.title`)}
+
+
+ }
+ {
+ isSettingsOpen && (
+
setIsSettingsOpen(false)}
+ onSave={handleSaveExternalRetrievalSettings}
+ initialTopK={externalRetrievalSettings.top_k}
+ initialScoreThreshold={externalRetrievalSettings.score_threshold}
+ initialScoreThresholdEnabled={externalRetrievalSettings.score_threshold_enabled}
+ />
+ )
+ }
@@ -122,7 +184,7 @@ const TextAreaWithButton = ({
-
>
)
diff --git a/web/app/components/datasets/rename-modal/index.tsx b/web/app/components/datasets/rename-modal/index.tsx
index a7e9e6e335edd..7d6008c087a5e 100644
--- a/web/app/components/datasets/rename-modal/index.tsx
+++ b/web/app/components/datasets/rename-modal/index.tsx
@@ -3,7 +3,6 @@
import type { MouseEventHandler } from 'react'
import { useState } from 'react'
import { RiCloseLine } from '@remixicon/react'
-import { BookOpenIcon } from '@heroicons/react/24/outline'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
@@ -26,6 +25,8 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
const [loading, setLoading] = useState(false)
const [name, setName] = useState(dataset.name)
const [description, setDescription] = useState(dataset.description)
+ const [externalKnowledgeId, setExternalKnowledgeId] = useState(dataset.external_knowledge_info.external_knowledge_id)
+ const [externalKnowledgeApiId, setExternalKnowledgeApiId] = useState(dataset.external_knowledge_info.external_knowledge_api_id)
const onConfirm: MouseEventHandler = async () => {
if (!name.trim()) {
@@ -34,12 +35,17 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
}
try {
setLoading(true)
+ const body: Partial & { external_knowledge_id?: string; external_knowledge_api_id?: string } = {
+ name,
+ description,
+ }
+ if (externalKnowledgeId && externalKnowledgeApiId) {
+ body.external_knowledge_id = externalKnowledgeId
+ body.external_knowledge_api_id = externalKnowledgeApiId
+ }
await updateDatasetSetting({
datasetId: dataset.id,
- body: {
- name,
- description,
- },
+ body,
})
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
if (onSuccess)
@@ -87,10 +93,6 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
className='block px-3 py-2 w-full h-[88px] rounded-lg bg-gray-100 text-sm outline-none appearance-none resize-none'
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
/>
-
-
- {t('datasetSettings.form.descWrite')}
-
diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx
index 0f6bdd0a59e0d..fa8c8de62ec28 100644
--- a/web/app/components/datasets/settings/form/index.tsx
+++ b/web/app/components/datasets/settings/form/index.tsx
@@ -2,17 +2,19 @@
import { useState } from 'react'
import { useMount } from 'ahooks'
import { useContext } from 'use-context-selector'
-import { BookOpenIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import { useSWRConfig } from 'swr'
import { unstable_serialize } from 'swr/infinite'
import PermissionSelector from '../permission-selector'
import IndexMethodRadio from '../index-method-radio'
+import RetrievalSettings from '../../external-knowledge-base/create/RetrievalSettings'
import cn from '@/utils/classnames'
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
import { ToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
+import Divider from '@/app/components/base/divider'
+import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import { updateDatasetSetting } from '@/service/datasets'
import type { DataSetListResponse } from '@/models/datasets'
import DatasetDetailContext from '@/context/dataset-detail'
@@ -55,6 +57,9 @@ const Form = () => {
const [name, setName] = useState(currentDataset?.name ?? '')
const [description, setDescription] = useState(currentDataset?.description ?? '')
const [permission, setPermission] = useState(currentDataset?.permission)
+ const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2)
+ const [scoreThreshold, setScoreThreshold] = useState(currentDataset?.external_retrieval_model.score_threshold ?? 0.5)
+ const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(currentDataset?.external_retrieval_model.score_threshold_enabled ?? false)
const [selectedMemberIDs, setSelectedMemberIDs] = useState(currentDataset?.partial_member_list || [])
const [memberList, setMemberList] = useState([])
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
@@ -85,6 +90,15 @@ const Form = () => {
setMemberList(accounts)
}
+ const handleSettingsChange = (data: { top_k?: number; score_threshold?: number; score_threshold_enabled?: boolean }) => {
+ if (data.top_k !== undefined)
+ setTopK(data.top_k)
+ if (data.score_threshold !== undefined)
+ setScoreThreshold(data.score_threshold)
+ if (data.score_threshold_enabled !== undefined)
+ setScoreThresholdEnabled(data.score_threshold_enabled)
+ }
+
useMount(() => {
getMembers()
})
@@ -132,6 +146,15 @@ const Form = () => {
},
embedding_model: embeddingModel.model,
embedding_model_provider: embeddingModel.provider,
+ ...(currentDataset!.provider === 'external' && {
+ external_knowledge_id: currentDataset!.external_knowledge_info.external_knowledge_id,
+ external_knowledge_api_id: currentDataset!.external_knowledge_info.external_knowledge_api_id,
+ external_retrieval_model: {
+ top_k: topK,
+ score_threshold: scoreThreshold,
+ score_threshold_enabled: scoreThresholdEnabled,
+ },
+ }),
},
} as any
if (permission === 'partial_members') {
@@ -161,7 +184,7 @@ const Form = () => {
-
{t('datasetSettings.form.name')}
+
{t('datasetSettings.form.name')}
{
-
{t('datasetSettings.form.desc')}
+
{t('datasetSettings.form.desc')}
-
{t('datasetSettings.form.permissions')}
+
{t('datasetSettings.form.permissions')}
{
-
{t('datasetSettings.form.indexMethod')}
+
{t('datasetSettings.form.indexMethod')}
{
{indexMethod === 'high_quality' && (
-
{t('datasetSettings.form.embeddingModel')}
+
{t('datasetSettings.form.embeddingModel')}
{
)}
{/* Retrieval Method Config */}
-
-
-
-
{t('datasetSettings.form.retrievalSetting.title')}
-
-
{t('datasetSettings.form.retrievalSetting.learnMore')}
- {t('datasetSettings.form.retrievalSetting.description')}
+ {currentDataset?.provider === 'external'
+ ? <>
+
+
+
+
{t('datasetSettings.form.retrievalSetting.title')}
+
+
+
+
+
+
{t('datasetSettings.form.externalKnowledgeAPI')}
+
+
+
+
+
+ {currentDataset?.external_knowledge_info.external_knowledge_api_name}
+
+
·
+
{currentDataset?.external_knowledge_info.external_knowledge_api_endpoint}
+
+
+
+
+
+
{t('datasetSettings.form.externalKnowledgeID')}
+
+
+
+
{currentDataset?.external_knowledge_info.external_knowledge_id}
+
+
+
+
+ >
+ :
+
+
+
{t('datasetSettings.form.retrievalSetting.title')}
+
+
+
+
+ {indexMethod === 'high_quality'
+ ? (
+
+ )
+ : (
+
+ )}
-
- {indexMethod === 'high_quality'
- ? (
-
- )
- : (
-
- )}
-
-
+ }
- setShowAccountSettingModal({ payload: 'account' })}>
+
+
{t('common.account.account')}
+
+
+
+
+ setShowAccountSettingModal({ payload: 'members' })}>
{t('common.userProfile.settings')}
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx
index d68fc79b0d5a7..a4a8b9b63722b 100644
--- a/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx
+++ b/web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx
@@ -9,7 +9,7 @@ import {
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Button from '@/app/components/base/button'
import type { FirecrawlConfig } from '@/models/common'
-import Field from '@/app/components/datasets/create/website/firecrawl/base/field'
+import Field from '@/app/components/datasets/create/website/base/field'
import Toast from '@/app/components/base/toast'
import { createDataSourceApiKeyBinding } from '@/service/datasets'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/config-jina-reader-modal.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/config-jina-reader-modal.tsx
new file mode 100644
index 0000000000000..c6d6ad02565cb
--- /dev/null
+++ b/web/app/components/header/account-setting/data-source-page/data-source-website/config-jina-reader-modal.tsx
@@ -0,0 +1,140 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+} from '@/app/components/base/portal-to-follow-elem'
+import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
+import Button from '@/app/components/base/button'
+import { DataSourceProvider } from '@/models/common'
+import Field from '@/app/components/datasets/create/website/base/field'
+import Toast from '@/app/components/base/toast'
+import { createDataSourceApiKeyBinding } from '@/service/datasets'
+import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
+type Props = {
+ onCancel: () => void
+ onSaved: () => void
+}
+
+const I18N_PREFIX = 'datasetCreation.jinaReader'
+
+const ConfigJinaReaderModal: FC
= ({
+ onCancel,
+ onSaved,
+}) => {
+ const { t } = useTranslation()
+ const [isSaving, setIsSaving] = useState(false)
+ const [apiKey, setApiKey] = useState('')
+
+ const handleSave = useCallback(async () => {
+ if (isSaving)
+ return
+ let errorMsg = ''
+ if (!errorMsg) {
+ if (!apiKey) {
+ errorMsg = t('common.errorMsg.fieldRequired', {
+ field: 'API Key',
+ })
+ }
+ }
+
+ if (errorMsg) {
+ Toast.notify({
+ type: 'error',
+ message: errorMsg,
+ })
+ return
+ }
+ const postData = {
+ category: 'website',
+ provider: DataSourceProvider.jinaReader,
+ credentials: {
+ auth_type: 'bearer',
+ config: {
+ api_key: apiKey,
+ },
+ },
+ }
+ try {
+ setIsSaving(true)
+ await createDataSourceApiKeyBinding(postData)
+ Toast.notify({
+ type: 'success',
+ message: t('common.api.success'),
+ })
+ }
+ finally {
+ setIsSaving(false)
+ }
+
+ onSaved()
+ }, [apiKey, onSaved, t, isSaving])
+
+ return (
+
+
+
+
+
+
+
{t(`${I18N_PREFIX}.configJinaReader`)}
+
+
+
+ setApiKey(value as string)}
+ placeholder={t(`${I18N_PREFIX}.apiKeyPlaceholder`)!}
+ />
+
+
+
+
+
+
+ {t('common.modelProvider.encrypted.front')}
+
+ PKCS1_OAEP
+
+ {t('common.modelProvider.encrypted.back')}
+
+
+
+
+
+
+ )
+}
+export default React.memo(ConfigJinaReaderModal)
diff --git a/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx
index 21f7660ef1dd1..628510c5dd387 100644
--- a/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx
+++ b/web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx
@@ -2,11 +2,12 @@
import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { useBoolean } from 'ahooks'
import Panel from '../panel'
import { DataSourceType } from '../panel/types'
import ConfigFirecrawlModal from './config-firecrawl-modal'
+import ConfigJinaReaderModal from './config-jina-reader-modal'
import cn from '@/utils/classnames'
+import s from '@/app/components/datasets/create/website/index.module.css'
import { fetchDataSources, removeDataSourceApiKeyBinding } from '@/service/datasets'
import type {
@@ -19,9 +20,11 @@ import {
} from '@/models/common'
import Toast from '@/app/components/base/toast'
-type Props = {}
+type Props = {
+ provider: DataSourceProvider
+}
-const DataSourceWebsite: FC = () => {
+const DataSourceWebsite: FC = ({ provider }) => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const [sources, setSources] = useState([])
@@ -36,22 +39,26 @@ const DataSourceWebsite: FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
- const [isShowConfig, {
- setTrue: showConfig,
- setFalse: hideConfig,
- }] = useBoolean(false)
+ const [configTarget, setConfigTarget] = useState(null)
+ const showConfig = useCallback((provider: DataSourceProvider) => {
+ setConfigTarget(provider)
+ }, [setConfigTarget])
+
+ const hideConfig = useCallback(() => {
+ setConfigTarget(null)
+ }, [setConfigTarget])
const handleAdded = useCallback(() => {
checkSetApiKey()
hideConfig()
}, [checkSetApiKey, hideConfig])
- const getIdByProvider = (provider: string): string | undefined => {
+ const getIdByProvider = (provider: DataSourceProvider): string | undefined => {
const source = sources.find(item => item.provider === provider)
return source?.id
}
- const handleRemove = useCallback((provider: string) => {
+ const handleRemove = useCallback((provider: DataSourceProvider) => {
return async () => {
const dataSourceId = getIdByProvider(provider)
if (dataSourceId) {
@@ -69,22 +76,34 @@ const DataSourceWebsite: FC = () => {
<>
0}
- onConfigure={showConfig}
+ provider={provider}
+ isConfigured={sources.find(item => item.provider === provider) !== undefined}
+ onConfigure={() => showConfig(provider)}
readOnly={!isCurrentWorkspaceManager}
- configuredList={sources.map(item => ({
+ configuredList={sources.filter(item => item.provider === provider).map(item => ({
id: item.id,
logo: ({ className }: { className: string }) => (
- 🔥
+ item.provider === DataSourceProvider.fireCrawl
+ ? (
+ 🔥
+ )
+ : (
+
+
+
+ )
),
- name: 'Firecrawl',
+ name: item.provider === DataSourceProvider.fireCrawl ? 'Firecrawl' : 'Jina Reader',
isActive: true,
}))}
- onRemove={handleRemove(DataSourceProvider.fireCrawl)}
+ onRemove={handleRemove(provider)}
/>
- {isShowConfig && (
+ {configTarget === DataSourceProvider.fireCrawl && (
)}
+ {configTarget === DataSourceProvider.jinaReader && (
+
+ )}
>
)
diff --git a/web/app/components/header/account-setting/data-source-page/index.tsx b/web/app/components/header/account-setting/data-source-page/index.tsx
index ede83152b223e..c3da977ca4e20 100644
--- a/web/app/components/header/account-setting/data-source-page/index.tsx
+++ b/web/app/components/header/account-setting/data-source-page/index.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import DataSourceNotion from './data-source-notion'
import DataSourceWebsite from './data-source-website'
import { fetchDataSource } from '@/service/common'
+import { DataSourceProvider } from '@/models/common'
export default function DataSourcePage() {
const { t } = useTranslation()
@@ -13,7 +14,8 @@ export default function DataSourcePage() {
{t('common.dataSource.add')}
-
+
+
)
}
diff --git a/web/app/components/header/account-setting/data-source-page/panel/index.tsx b/web/app/components/header/account-setting/data-source-page/panel/index.tsx
index 988aedcaf7476..4a810020b440e 100644
--- a/web/app/components/header/account-setting/data-source-page/panel/index.tsx
+++ b/web/app/components/header/account-setting/data-source-page/panel/index.tsx
@@ -8,10 +8,12 @@ import ConfigItem from './config-item'
import s from './style.module.css'
import { DataSourceType } from './types'
+import { DataSourceProvider } from '@/models/common'
import cn from '@/utils/classnames'
type Props = {
type: DataSourceType
+ provider: DataSourceProvider
isConfigured: boolean
onConfigure: () => void
readOnly: boolean
@@ -25,6 +27,7 @@ type Props = {
const Panel: FC = ({
type,
+ provider,
isConfigured,
onConfigure,
readOnly,
@@ -46,7 +49,7 @@ const Panel: FC = ({
{t(`common.dataSource.${type}.title`)}
{isWebsite && (
- {t('common.dataSource.website.with')} 🔥 Firecrawl
+ {t('common.dataSource.website.with')} { provider === DataSourceProvider.fireCrawl ? '🔥 Firecrawl' : 'Jina Reader'}
)}
diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx
index 253b9f1b4c2cd..d829f6b77b0cc 100644
--- a/web/app/components/header/account-setting/index.tsx
+++ b/web/app/components/header/account-setting/index.tsx
@@ -2,10 +2,6 @@
import { useTranslation } from 'react-i18next'
import { useEffect, useRef, useState } from 'react'
import {
- RiAccountCircleFill,
- RiAccountCircleLine,
- RiApps2AddFill,
- RiApps2AddLine,
RiBox3Fill,
RiBox3Line,
RiCloseLine,
@@ -21,9 +17,7 @@ import {
RiPuzzle2Line,
RiTranslate2,
} from '@remixicon/react'
-import AccountPage from './account-page'
import MembersPage from './members-page'
-import IntegrationsPage from './Integrations-page'
import LanguagePage from './language-page'
import ApiBasedExtensionPage from './api-based-extension-page'
import DataSourcePage from './data-source-page'
@@ -60,7 +54,7 @@ type GroupItem = {
export default function AccountSetting({
onCancel,
- activeTab = 'account',
+ activeTab = 'members',
}: IAccountSettingProps) {
const [activeMenu, setActiveMenu] = useState(activeTab)
const { t } = useTranslation()
@@ -125,18 +119,6 @@ export default function AccountSetting({
key: 'account-group',
name: t('common.settings.accountGroup'),
items: [
- {
- key: 'account',
- name: t('common.settings.account'),
- icon: ,
- activeIcon: ,
- },
- {
- key: 'integrations',
- name: t('common.settings.integrations'),
- icon: ,
- activeIcon: ,
- },
{
key: 'language',
name: t('common.settings.language'),
@@ -217,10 +199,8 @@ export default function AccountSetting({
- {activeMenu === 'account' &&
}
{activeMenu === 'members' &&
}
{activeMenu === 'billing' &&
}
- {activeMenu === 'integrations' &&
}
{activeMenu === 'language' &&
}
{activeMenu === 'provider' &&
}
{activeMenu === 'data-source' &&
}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx
index a22ec16c25228..a16b101e6a57c 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx
@@ -26,7 +26,7 @@ const ModelIcon: FC
= ({
return (
)
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx
index c618b6f1a970d..768f2c2766d6f 100644
--- a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx
@@ -16,7 +16,7 @@ const ProviderIcon: FC = ({
return (
)
diff --git a/web/app/components/header/dataset-nav/index.tsx b/web/app/components/header/dataset-nav/index.tsx
index abf76608a8066..6205468ce458e 100644
--- a/web/app/components/header/dataset-nav/index.tsx
+++ b/web/app/components/header/dataset-nav/index.tsx
@@ -51,7 +51,7 @@ const DatasetNav = () => {
navs={datasetItems.map(dataset => ({
id: dataset.id,
name: dataset.name,
- link: `/datasets/${dataset.id}/documents`,
+ link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`,
icon: dataset.icon,
icon_background: dataset.icon_background,
})) as NavItem[]}
diff --git a/web/app/components/header/header-wrapper.tsx b/web/app/components/header/header-wrapper.tsx
index 205a379a90301..360cf8e5607ba 100644
--- a/web/app/components/header/header-wrapper.tsx
+++ b/web/app/components/header/header-wrapper.tsx
@@ -11,7 +11,7 @@ const HeaderWrapper = ({
children,
}: HeaderWrapperProps) => {
const pathname = usePathname()
- const isBordered = ['/apps', '/datasets', '/datasets/create', '/tools'].includes(pathname)
+ const isBordered = ['/apps', '/datasets', '/datasets/create', '/tools', '/account'].includes(pathname)
return (
{
const router = useRouter()
const searchParams = useSearchParams()
- const consoleToken = searchParams.get('console_token')
+ const { getNewAccessToken } = useRefreshToken()
+ const consoleToken = searchParams.get('access_token')
+ const refreshToken = searchParams.get('refresh_token')
const consoleTokenFromLocalStorage = localStorage?.getItem('console_token')
+ const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token')
const [init, setInit] = useState(false)
useEffect(() => {
- if (!(consoleToken || consoleTokenFromLocalStorage))
+ if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage)) {
router.replace('/signin')
+ return
+ }
+ if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage)
+ getNewAccessToken()
- if (consoleToken) {
- localStorage?.setItem('console_token', consoleToken!)
- router.replace('/apps', { forceOptimisticNavigation: false } as any)
+ if (consoleToken && refreshToken) {
+ localStorage.setItem('console_token', consoleToken)
+ localStorage.setItem('refresh_token', refreshToken)
+ getNewAccessToken().then(() => {
+ router.replace('/apps', { forceOptimisticNavigation: false } as any)
+ }).catch(() => {
+ router.replace('/signin')
+ })
}
+
setInit(true)
}, [])
diff --git a/web/app/components/tools/workflow-tool/configure-button.tsx b/web/app/components/tools/workflow-tool/configure-button.tsx
index d2c5142f53c10..6521410daea01 100644
--- a/web/app/components/tools/workflow-tool/configure-button.tsx
+++ b/web/app/components/tools/workflow-tool/configure-button.tsx
@@ -65,7 +65,7 @@ const WorkflowToolConfigureButton = ({
else {
if (item.type === 'paragraph' && param.type !== 'string')
return true
- if (param.type !== item.type && !(param.type === 'string' && item.type === 'paragraph'))
+ if (item.type === 'text-input' && param.type !== 'string')
return true
}
}
diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts
index e1da503f3825f..68c3ff0a4b458 100644
--- a/web/app/components/workflow/hooks/use-workflow-run.ts
+++ b/web/app/components/workflow/hooks/use-workflow-run.ts
@@ -185,7 +185,7 @@ export const useWorkflowRun = () => {
draft.forEach((edge) => {
edge.data = {
...edge.data,
- _runned: false,
+ _run: false,
}
})
})
@@ -292,7 +292,7 @@ export const useWorkflowRun = () => {
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
if (edge.target === data.node_id && incomeNodesId.includes(edge.source))
- edge.data = { ...edge.data, _runned: true } as any
+ edge.data = { ...edge.data, _run: true } as any
})
})
setEdges(newEdges)
@@ -416,7 +416,7 @@ export const useWorkflowRun = () => {
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
if (edge)
- edge.data = { ...edge.data, _runned: true } as any
+ edge.data = { ...edge.data, _run: true } as any
})
setEdges(newEdges)
diff --git a/web/app/components/workflow/hooks/use-workflow-start-run.tsx b/web/app/components/workflow/hooks/use-workflow-start-run.tsx
index b2b1c69975658..77e959b573ba5 100644
--- a/web/app/components/workflow/hooks/use-workflow-start-run.tsx
+++ b/web/app/components/workflow/hooks/use-workflow-start-run.tsx
@@ -1,17 +1,25 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
+import { useTranslation } from 'react-i18next'
import { useWorkflowStore } from '../store'
import {
BlockEnum,
WorkflowRunningStatus,
} from '../types'
+import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
+import type { Node } from '../types'
+import { useWorkflow } from './use-workflow'
import {
useIsChatMode,
useNodesSyncDraft,
useWorkflowInteractions,
useWorkflowRun,
} from './index'
+import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
+import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
+import Toast from '@/app/components/base/toast'
export const useWorkflowStartRun = () => {
const store = useStoreApi()
@@ -20,7 +28,26 @@ export const useWorkflowStartRun = () => {
const isChatMode = useIsChatMode()
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleRun } = useWorkflowRun()
+ const { isFromStartNode } = useWorkflow()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
+ const { checkValid: checkKnowledgeRetrievalValid } = KnowledgeRetrievalDefault
+ const { t } = useTranslation()
+ const {
+ modelList: rerankModelList,
+ defaultModel: rerankDefaultModel,
+ } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
+
+ const {
+ currentModel,
+ } = useCurrentProviderAndModel(
+ rerankModelList,
+ rerankDefaultModel
+ ? {
+ ...rerankDefaultModel,
+ provider: rerankDefaultModel.provider.provider,
+ }
+ : undefined,
+ )
const handleWorkflowStartRunInWorkflow = useCallback(async () => {
const {
@@ -33,6 +60,9 @@ export const useWorkflowStartRun = () => {
const { getNodes } = store.getState()
const nodes = getNodes()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
+ const knowledgeRetrievalNodes = nodes.filter((node: Node
) =>
+ node.data.type === BlockEnum.KnowledgeRetrieval,
+ )
const startVariables = startNode?.data.variables || []
const fileSettings = featuresStore!.getState().features.file
const {
@@ -42,6 +72,31 @@ export const useWorkflowStartRun = () => {
setShowEnvPanel,
} = workflowStore.getState()
+ if (knowledgeRetrievalNodes.length > 0) {
+ for (const node of knowledgeRetrievalNodes) {
+ if (isFromStartNode(node.id)) {
+ const res = checkKnowledgeRetrievalValid(node.data, t)
+ if (!res.isValid || !currentModel || !rerankDefaultModel) {
+ const errorMessage = res.errorMessage
+ if (errorMessage) {
+ Toast.notify({
+ type: 'error',
+ message: errorMessage,
+ })
+ return false
+ }
+ else {
+ Toast.notify({
+ type: 'error',
+ message: t('appDebug.datasetConfig.rerankModelRequired'),
+ })
+ return false
+ }
+ }
+ }
+ }
+ }
+
setShowEnvPanel(false)
if (showDebugAndPreviewPanel) {
diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts
index b201b28b88d14..ec7ce66e5fdce 100644
--- a/web/app/components/workflow/hooks/use-workflow.ts
+++ b/web/app/components/workflow/hooks/use-workflow.ts
@@ -235,6 +235,33 @@ export const useWorkflow = () => {
return nodes.filter(node => node.parentId === nodeId)
}, [store])
+ const isFromStartNode = useCallback((nodeId: string) => {
+ const { getNodes } = store.getState()
+ const nodes = getNodes()
+ const currentNode = nodes.find(node => node.id === nodeId)
+
+ if (!currentNode)
+ return false
+
+ if (currentNode.data.type === BlockEnum.Start)
+ return true
+
+ const checkPreviousNodes = (node: Node) => {
+ const previousNodes = getBeforeNodeById(node.id)
+
+ for (const prevNode of previousNodes) {
+ if (prevNode.data.type === BlockEnum.Start)
+ return true
+ if (checkPreviousNodes(prevNode))
+ return true
+ }
+
+ return false
+ }
+
+ return checkPreviousNodes(currentNode)
+ }, [store, getBeforeNodeById])
+
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
const { getNodes, setNodes } = store.getState()
const afterNodes = getAfterNodesInSameBranch(nodeId)
@@ -389,6 +416,7 @@ export const useWorkflow = () => {
checkParallelLimit,
checkNestedParallelLimit,
isValidConnection,
+ isFromStartNode,
formatTimeFromNow,
getNode,
getBeforeNodeById,
diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx
index cdccd60a3b5a1..938ae679c3d26 100644
--- a/web/app/components/workflow/index.tsx
+++ b/web/app/components/workflow/index.tsx
@@ -405,9 +405,9 @@ const WorkflowWrap = memo(() => {
const initialFeatures: FeaturesData = {
file: {
image: {
- enabled: !!features.file_upload?.image.enabled,
- number_limits: features.file_upload?.image.number_limits || 3,
- transfer_methods: features.file_upload?.image.transfer_methods || ['local_file', 'remote_url'],
+ enabled: !!features.file_upload?.image?.enabled,
+ number_limits: features.file_upload?.image?.number_limits || 3,
+ transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
},
},
opening: {
diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts
index 3deec09dc2c9a..89ba4e5cf9342 100644
--- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts
+++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts
@@ -116,6 +116,19 @@ const formatItem = (
variable: 'sys.files',
type: VarType.arrayFile,
})
+ res.vars.push({
+ variable: 'sys.app_id',
+ type: VarType.string,
+ })
+ res.vars.push({
+ variable: 'sys.workflow_id',
+ type: VarType.string,
+ })
+ res.vars.push({
+ variable: 'sys.workflow_run_id',
+ type: VarType.string,
+ })
+
break
}
diff --git a/web/app/components/workflow/nodes/http/node.tsx b/web/app/components/workflow/nodes/http/node.tsx
index 5bbb10fc3a596..4b7dbea2571ae 100644
--- a/web/app/components/workflow/nodes/http/node.tsx
+++ b/web/app/components/workflow/nodes/http/node.tsx
@@ -15,7 +15,7 @@ const Node: FC> = ({
{method}
-
+
{
const { t } = useTranslation()
return (
-
+
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx
index 0369322783d9c..caf44d10bcc22 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx
@@ -1,13 +1,15 @@
'use client'
import type { FC } from 'react'
-import React, { useCallback } from 'react'
+import React, { useCallback, useState } from 'react'
import { useBoolean } from 'ahooks'
import {
RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
import type { DataSet } from '@/models/datasets'
import { DataSourceType } from '@/models/datasets'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import FileIcon from '@/app/components/base/file-icon'
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
import SettingsModal from '@/app/components/app/configuration/dataset-config/settings-modal'
@@ -30,8 +32,10 @@ const DatasetItem: FC
= ({
readonly,
}) => {
const media = useBreakpoints()
+ const { t } = useTranslation()
const isMobile = media === MediaType.mobile
const { formatIndexingTechniqueAndMethod } = useKnowledge()
+ const [isDeleteHovered, setIsDeleteHovered] = useState(false)
const [isShowSettingsModal, {
setTrue: showSettingsModal,
@@ -43,8 +47,18 @@ const DatasetItem: FC = ({
hideSettingsModal()
}, [hideSettingsModal, onChange])
+ const handleRemove = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation()
+ onRemove()
+ }, [onRemove])
+
return (
-
+
{
payload.data_source_type === DataSourceType.NOTION
@@ -61,24 +75,36 @@ const DatasetItem: FC
= ({
{!readonly && (
-
{
+ e.stopPropagation()
+ showSettingsModal()
+ }}
>
-
-
-
+
+
setIsDeleteHovered(true)}
+ onMouseLeave={() => setIsDeleteHovered(false)}
>
-
-
+
+
)}
-
+ {
+ payload.indexing_technique &&
+ }
+ {
+ payload.provider === 'external' &&
+ }
{isShowSettingsModal && (
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
index 994bf4f2052df..e83a5d97b5afd 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
@@ -136,6 +136,8 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
top_k: multipleRetrievalConfig?.top_k || DATASET_DEFAULT.top_k,
score_threshold: multipleRetrievalConfig?.score_threshold,
reranking_model: multipleRetrievalConfig?.reranking_model,
+ reranking_mode: multipleRetrievalConfig?.reranking_mode,
+ weights: multipleRetrievalConfig?.weights,
}
})
setInputs(newInput)
@@ -205,9 +207,11 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
const handleOnDatasetsChange = useCallback((newDatasets: DataSet[]) => {
const {
- allEconomic,
mixtureHighQualityAndEconomic,
+ mixtureInternalAndExternal,
inconsistentEmbeddingModel,
+ allInternal,
+ allExternal,
} = getSelectedDatasetsMode(newDatasets)
const newInputs = produce(inputs, (draft) => {
draft.dataset_ids = newDatasets.map(d => d.id)
@@ -220,7 +224,11 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
setInputs(newInputs)
setSelectedDatasets(newDatasets)
- if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
+ if (
+ (allInternal && (mixtureHighQualityAndEconomic || inconsistentEmbeddingModel))
+ || mixtureInternalAndExternal
+ || (allExternal && newDatasets.length > 1)
+ )
setRerankModelOpen(true)
}, [inputs, setInputs, payload.retrieval_mode])
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts
index 89ae9b47640c5..85ae6c4c96bcf 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts
@@ -21,6 +21,9 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
let allHighQualityFullTextSearch = true
let allEconomic = true
let mixtureHighQualityAndEconomic = true
+ let allExternal = true
+ let allInternal = true
+ let mixtureInternalAndExternal = true
let inconsistentEmbeddingModel = false
if (!datasets.length) {
allHighQuality = false
@@ -29,6 +32,9 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
allEconomic = false
mixtureHighQualityAndEconomic = false
inconsistentEmbeddingModel = false
+ allExternal = false
+ allInternal = false
+ mixtureInternalAndExternal = false
}
datasets.forEach((dataset) => {
if (dataset.indexing_technique === 'economy') {
@@ -45,8 +51,21 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
if (dataset.retrieval_model_dict.search_method !== RETRIEVE_METHOD.fullText)
allHighQualityFullTextSearch = false
}
+ if (dataset.provider !== 'external') {
+ allExternal = false
+ }
+ else {
+ allInternal = false
+ allHighQuality = false
+ allHighQualityVectorSearch = false
+ allHighQualityFullTextSearch = false
+ mixtureHighQualityAndEconomic = false
+ }
})
+ if (allExternal || allInternal)
+ mixtureInternalAndExternal = false
+
if (allHighQuality || allEconomic)
mixtureHighQualityAndEconomic = false
@@ -59,6 +78,9 @@ export const getSelectedDatasetsMode = (datasets: DataSet[]) => {
allHighQualityFullTextSearch,
allEconomic,
mixtureHighQualityAndEconomic,
+ allInternal,
+ allExternal,
+ mixtureInternalAndExternal,
inconsistentEmbeddingModel,
} as SelectedDatasetsMode
}
@@ -70,6 +92,9 @@ export const getMultipleRetrievalConfig = (multipleRetrievalConfig: MultipleRetr
allHighQualityFullTextSearch,
allEconomic,
mixtureHighQualityAndEconomic,
+ allInternal,
+ allExternal,
+ mixtureInternalAndExternal,
inconsistentEmbeddingModel,
} = getSelectedDatasetsMode(selectedDatasets)
@@ -91,13 +116,13 @@ export const getMultipleRetrievalConfig = (multipleRetrievalConfig: MultipleRetr
reranking_enable: allEconomic ? reranking_enable : true,
}
- if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
+ if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel || allExternal || mixtureInternalAndExternal)
result.reranking_mode = RerankingModeEnum.RerankingModel
- if (allHighQuality && !inconsistentEmbeddingModel && reranking_mode === undefined)
+ if (allHighQuality && !inconsistentEmbeddingModel && reranking_mode === undefined && allInternal)
result.reranking_mode = RerankingModeEnum.WeightedScore
- if (allHighQuality && !inconsistentEmbeddingModel && (reranking_mode === RerankingModeEnum.WeightedScore || reranking_mode === undefined) && !weights) {
+ if (allHighQuality && !inconsistentEmbeddingModel && (reranking_mode === RerankingModeEnum.WeightedScore || reranking_mode === undefined) && allInternal && !weights) {
result.weights = {
vector_setting: {
vector_weight: allHighQualityVectorSearch
diff --git a/web/app/components/workflow/nodes/start/panel.tsx b/web/app/components/workflow/nodes/start/panel.tsx
index ce86a34265b61..3a1eed5ff4622 100644
--- a/web/app/components/workflow/nodes/start/panel.tsx
+++ b/web/app/components/workflow/nodes/start/panel.tsx
@@ -121,6 +121,39 @@ const Panel: FC> = ({
}
/>
+
+ String
+
+ }
+ />
+
+ String
+
+ }
+ />
+
+ String
+
+ }
+ />
>
diff --git a/web/app/components/workflow/panel/chat-record/index.tsx b/web/app/components/workflow/panel/chat-record/index.tsx
index afd20b7358670..16d2c304a73c5 100644
--- a/web/app/components/workflow/panel/chat-record/index.tsx
+++ b/web/app/components/workflow/panel/chat-record/index.tsx
@@ -2,7 +2,6 @@ import {
memo,
useCallback,
useEffect,
- useMemo,
useState,
} from 'react'
import { RiCloseLine } from '@remixicon/react'
@@ -17,50 +16,70 @@ import type { ChatItem } from '@/app/components/base/chat/types'
import { fetchConversationMessages } from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading'
+import { UUID_NIL } from '@/app/components/base/chat/constants'
+
+function appendQAToChatList(newChatList: ChatItem[], item: any) {
+ newChatList.push({
+ id: item.id,
+ content: item.answer,
+ feedback: item.feedback,
+ isAnswer: true,
+ citation: item.metadata?.retriever_resources,
+ message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
+ workflow_run_id: item.workflow_run_id,
+ })
+ newChatList.push({
+ id: `question-${item.id}`,
+ content: item.query,
+ isAnswer: false,
+ message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
+ })
+}
+
+function getFormattedChatList(messages: any[]) {
+ const newChatList: ChatItem[] = []
+ let nextMessageId = null
+ for (const item of messages) {
+ if (!item.parent_message_id) {
+ appendQAToChatList(newChatList, item)
+ break
+ }
+
+ if (!nextMessageId) {
+ appendQAToChatList(newChatList, item)
+ nextMessageId = item.parent_message_id
+ }
+ else {
+ if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
+ appendQAToChatList(newChatList, item)
+ nextMessageId = item.parent_message_id
+ }
+ }
+ }
+ return newChatList.reverse()
+}
const ChatRecord = () => {
const [fetched, setFetched] = useState(false)
- const [chatList, setChatList] = useState([])
+ const [chatList, setChatList] = useState
([])
const appDetail = useAppStore(s => s.appDetail)
const workflowStore = useWorkflowStore()
const { handleLoadBackupDraft } = useWorkflowRun()
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const currentConversationID = historyWorkflowData?.conversation_id
- const chatMessageList = useMemo(() => {
- const res: ChatItem[] = []
- if (chatList.length) {
- chatList.forEach((item: any) => {
- res.push({
- id: `question-${item.id}`,
- content: item.query,
- isAnswer: false,
- message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
- })
- res.push({
- id: item.id,
- content: item.answer,
- feedback: item.feedback,
- isAnswer: true,
- citation: item.metadata?.retriever_resources,
- message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
- workflow_run_id: item.workflow_run_id,
- })
- })
- }
- return res
- }, [chatList])
-
const handleFetchConversationMessages = useCallback(async () => {
if (appDetail && currentConversationID) {
try {
setFetched(false)
const res = await fetchConversationMessages(appDetail.id, currentConversationID)
- setFetched(true)
- setChatList((res as any).data)
+ setChatList(getFormattedChatList((res as any).data))
}
catch (e) {
-
+ console.error(e)
+ }
+ finally {
+ setFetched(true)
}
}
}, [appDetail, currentConversationID])
@@ -71,7 +90,7 @@ const ChatRecord = () => {
return (
{
config={{
supportCitationHitInfo: true,
} as any}
- chatList={chatMessageList}
- chatContainerClassName='px-4'
+ chatList={chatList}
+ chatContainerClassName='px-3'
chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto'
chatFooterClassName='px-4 rounded-b-2xl'
chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
@@ -110,6 +129,8 @@ const ChatRecord = () => {
noChatInput
allToolIcons={{}}
showPromptLog
+ noSpacing
+ chatAnswerContainerInner='!pr-2'
/>
>
diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
index a7dd607e221ea..230b2d7fa0c65 100644
--- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
+++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
@@ -18,13 +18,14 @@ import ConversationVariableModal from './conversation-variable-modal'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
-import type { OnSend } from '@/app/components/base/chat/types'
+import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import {
fetchSuggestedQuestions,
stopChatMessageResponding,
} from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
+import { getLastAnswer } from '@/app/components/base/chat/utils'
type ChatWrapperProps = {
showConversationVariableModal: boolean
@@ -58,6 +59,8 @@ const ChatWrapper = forwardRef(({ showConv
const {
conversationId,
chatList,
+ chatListRef,
+ handleUpdateChatList,
handleStop,
isResponding,
suggestedQuestions,
@@ -73,19 +76,36 @@ const ChatWrapper = forwardRef(({ showConv
taskId => stopChatMessageResponding(appDetail!.id, taskId),
)
- const doSend = useCallback((query, files) => {
+ const doSend = useCallback((query, files, last_answer) => {
handleSend(
{
query,
files,
inputs: workflowStore.getState().inputs,
conversation_id: conversationId,
+ parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
},
{
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
},
)
- }, [conversationId, handleSend, workflowStore, appDetail])
+ }, [chatListRef, conversationId, handleSend, workflowStore, appDetail])
+
+ const doRegenerate = useCallback((chatItem: ChatItem) => {
+ const index = chatList.findIndex(item => item.id === chatItem.id)
+ if (index === -1)
+ return
+
+ const prevMessages = chatList.slice(0, index)
+ const question = prevMessages.pop()
+ const lastAnswer = getLastAnswer(prevMessages)
+
+ if (!question)
+ return
+
+ handleUpdateChatList(prevMessages)
+ doSend(question.content, question.message_files, lastAnswer)
+ }, [chatList, handleUpdateChatList, doSend])
useImperativeHandle(ref, () => {
return {
@@ -107,6 +127,7 @@ const ChatWrapper = forwardRef(({ showConv
chatFooterClassName='px-4 rounded-bl-2xl'
chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
onSend={doSend}
+ onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={(
<>
diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts
index 51a018bcb15b4..cad76a4490c85 100644
--- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts
+++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts
@@ -387,6 +387,8 @@ export const useChat = (
return {
conversationId: conversationId.current,
chatList,
+ chatListRef,
+ handleUpdateChatList,
handleSend,
handleStop,
handleRestart,
diff --git a/web/app/forgot-password/page.tsx b/web/app/forgot-password/page.tsx
index fa44d1a20c689..bb46011c06302 100644
--- a/web/app/forgot-password/page.tsx
+++ b/web/app/forgot-password/page.tsx
@@ -28,7 +28,7 @@ const ForgotPassword = () => {
{token ? : }
- © {new Date().getFullYear()} Dify, Inc. All rights reserved.
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
diff --git a/web/app/install/page.tsx b/web/app/install/page.tsx
index 9fa38dd15e2a8..395fae34ec169 100644
--- a/web/app/install/page.tsx
+++ b/web/app/install/page.tsx
@@ -22,7 +22,7 @@ const Install = () => {
- © {new Date().getFullYear()} Dify, Inc. All rights reserved.
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index e9242edfade52..48e35c50e065d 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -2,7 +2,6 @@ import type { Viewport } from 'next'
import I18nServer from './components/i18n-server'
import BrowserInitor from './components/browser-initor'
import SentryInitor from './components/sentry-initor'
-import Topbar from './components/base/topbar'
import { getLocaleOnServer } from '@/i18n/server'
import './styles/globals.css'
import './styles/markdown.scss'
@@ -45,7 +44,6 @@ const LocaleLayout = ({
data-public-site-about={process.env.NEXT_PUBLIC_SITE_ABOUT}
data-public-text-generation-timeout-ms={process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS}
>
-
{children}
diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx
index 7f23c7d22e5ff..113ed64b57b9e 100644
--- a/web/app/signin/normalForm.tsx
+++ b/web/app/signin/normalForm.tsx
@@ -11,6 +11,7 @@ import { IS_CE_EDITION, SUPPORT_MAIL_LOGIN, apiPrefix, emailRegex } from '@/conf
import Button from '@/app/components/base/button'
import { login, oauth } from '@/service/common'
import { getPurifyHref } from '@/utils'
+import useRefreshToken from '@/hooks/use-refresh-token'
type IState = {
formValid: boolean
@@ -61,6 +62,7 @@ function reducer(state: IState, action: IAction) {
const NormalForm = () => {
const { t } = useTranslation()
+ const { getNewAccessToken } = useRefreshToken()
const useEmailLogin = IS_CE_EDITION || SUPPORT_MAIL_LOGIN
const router = useRouter()
@@ -95,7 +97,9 @@ const NormalForm = () => {
},
})
if (res.result === 'success') {
- localStorage.setItem('console_token', res.data)
+ localStorage.setItem('console_token', res.data.access_token)
+ localStorage.setItem('refresh_token', res.data.refresh_token)
+ getNewAccessToken()
router.replace('/apps')
}
else {
@@ -217,6 +221,7 @@ const NormalForm = () => {
autoComplete="email"
placeholder={t('login.emailPlaceholder') || ''}
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'}
+ tabIndex={1}
/>
@@ -241,6 +246,7 @@ const NormalForm = () => {
autoComplete="current-password"
placeholder={t('login.passwordPlaceholder') || ''}
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
+ tabIndex={2}
/>