mirror of
https://github.com/langgenius/dify.git
synced 2026-08-01 18:30:48 +08:00
312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
import type { UseMutateAsyncFunction } from '@tanstack/react-query'
|
|
import type { ChangeEvent } from 'react'
|
|
import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types'
|
|
import type {
|
|
Attachment,
|
|
ExternalKnowledgeBaseHitTestingRequest,
|
|
ExternalKnowledgeBaseHitTestingResponse,
|
|
HitTestingRequest,
|
|
HitTestingResponse,
|
|
Query,
|
|
} from '@/models/datasets'
|
|
import type { RetrievalConfig } from '@/types/app'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { RiEqualizer2Line, RiPlayCircleLine } from '@remixicon/react'
|
|
import * as React from 'react'
|
|
import { useCallback, useMemo, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { v4 as uuid4 } from 'uuid'
|
|
import ImageUploaderInRetrievalTesting from '@/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing'
|
|
import { getIcon } from '@/app/components/datasets/common/retrieval-method-info'
|
|
import ModifyExternalRetrievalModal from '@/app/components/datasets/hit-testing/modify-external-retrieval-modal'
|
|
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
|
import { RETRIEVE_METHOD } from '@/types/app'
|
|
import Textarea from './textarea'
|
|
|
|
type QueryInputProps = {
|
|
onUpdateList: () => void
|
|
setHitResult: (res: HitTestingResponse) => void
|
|
setExternalHitResult: (res: ExternalKnowledgeBaseHitTestingResponse) => void
|
|
loading: boolean
|
|
queries: Query[]
|
|
setQueries: (v: Query[]) => void
|
|
isExternal?: boolean
|
|
onClickRetrievalMethod: () => void
|
|
retrievalConfig: RetrievalConfig
|
|
isEconomy: boolean
|
|
canRunRetrievalRecall?: boolean
|
|
onSubmit?: () => void
|
|
hitTestingMutation: UseMutateAsyncFunction<HitTestingResponse, Error, HitTestingRequest, unknown>
|
|
externalKnowledgeBaseHitTestingMutation: UseMutateAsyncFunction<
|
|
ExternalKnowledgeBaseHitTestingResponse,
|
|
Error,
|
|
ExternalKnowledgeBaseHitTestingRequest,
|
|
unknown
|
|
>
|
|
}
|
|
|
|
const QueryInput = ({
|
|
onUpdateList,
|
|
setHitResult,
|
|
setExternalHitResult,
|
|
loading,
|
|
queries,
|
|
setQueries,
|
|
isExternal = false,
|
|
onClickRetrievalMethod,
|
|
retrievalConfig,
|
|
isEconomy,
|
|
canRunRetrievalRecall = true,
|
|
onSubmit: _onSubmit,
|
|
hitTestingMutation,
|
|
externalKnowledgeBaseHitTestingMutation,
|
|
}: QueryInputProps) => {
|
|
const { t } = useTranslation()
|
|
const isMultimodal = useDatasetDetailContextWithSelector((s) => !!s.dataset?.is_multimodal)
|
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
|
const [externalRetrievalSettings, setExternalRetrievalSettings] = useState({
|
|
top_k: 4,
|
|
score_threshold: 0.5,
|
|
score_threshold_enabled: false,
|
|
})
|
|
|
|
const text = useMemo(() => {
|
|
return queries.find((query) => query.content_type === 'text_query')?.content ?? ''
|
|
}, [queries])
|
|
|
|
const images = useMemo(() => {
|
|
const imageQueries = queries
|
|
.filter((query) => query.content_type === 'image_query')
|
|
.map((query) => query.file_info)
|
|
.filter(Boolean) as Attachment[]
|
|
return (
|
|
imageQueries.map((item) => ({
|
|
id: uuid4(),
|
|
name: item.name,
|
|
size: item.size,
|
|
mimeType: item.mime_type,
|
|
extension: item.extension,
|
|
sourceUrl: item.source_url,
|
|
uploadedId: item.id,
|
|
progress: 100,
|
|
})) || []
|
|
)
|
|
}, [queries])
|
|
|
|
const isAllUploaded = useMemo(() => {
|
|
return images.every((image) => !!image.uploadedId)
|
|
}, [images])
|
|
|
|
const handleSaveExternalRetrievalSettings = useCallback(
|
|
(data: { top_k: number; score_threshold: number; score_threshold_enabled: boolean }) => {
|
|
setExternalRetrievalSettings(data)
|
|
setIsSettingsOpen(false)
|
|
},
|
|
[],
|
|
)
|
|
|
|
const handleTextChange = useCallback(
|
|
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
const newQueries = [...queries]
|
|
const textQuery = newQueries.find((query) => query.content_type === 'text_query')
|
|
if (!textQuery) {
|
|
newQueries.push({
|
|
content: event.target.value,
|
|
content_type: 'text_query',
|
|
file_info: null,
|
|
})
|
|
} else {
|
|
textQuery.content = event.target.value
|
|
}
|
|
setQueries(newQueries)
|
|
},
|
|
[queries, setQueries],
|
|
)
|
|
|
|
const handleImageChange = useCallback(
|
|
(files: FileEntity[]) => {
|
|
let newQueries = [...queries]
|
|
newQueries = newQueries.filter((query) => query.content_type !== 'image_query')
|
|
files.forEach((file) => {
|
|
newQueries.push({
|
|
content: file.sourceUrl || '',
|
|
content_type: 'image_query',
|
|
file_info: {
|
|
id: file.uploadedId || '',
|
|
mime_type: file.mimeType,
|
|
source_url: file.sourceUrl || '',
|
|
name: file.name,
|
|
size: file.size,
|
|
extension: file.extension,
|
|
},
|
|
})
|
|
})
|
|
setQueries(newQueries)
|
|
},
|
|
[queries, setQueries],
|
|
)
|
|
|
|
const onSubmit = useCallback(async () => {
|
|
if (!canRunRetrievalRecall) return
|
|
|
|
await hitTestingMutation(
|
|
{
|
|
query: text,
|
|
attachment_ids: images.map((image) => image.uploadedId),
|
|
retrieval_model: {
|
|
...retrievalConfig,
|
|
search_method: isEconomy ? RETRIEVE_METHOD.keywordSearch : retrievalConfig.search_method,
|
|
},
|
|
},
|
|
{
|
|
onSuccess: (data) => {
|
|
setHitResult(data)
|
|
onUpdateList?.()
|
|
if (_onSubmit) _onSubmit()
|
|
},
|
|
},
|
|
)
|
|
}, [
|
|
canRunRetrievalRecall,
|
|
text,
|
|
retrievalConfig,
|
|
isEconomy,
|
|
hitTestingMutation,
|
|
onUpdateList,
|
|
_onSubmit,
|
|
images,
|
|
setHitResult,
|
|
])
|
|
|
|
const externalRetrievalTestingOnSubmit = useCallback(async () => {
|
|
if (!canRunRetrievalRecall) return
|
|
|
|
await externalKnowledgeBaseHitTestingMutation(
|
|
{
|
|
query: text,
|
|
external_retrieval_model: {
|
|
top_k: externalRetrievalSettings.top_k,
|
|
score_threshold: externalRetrievalSettings.score_threshold,
|
|
score_threshold_enabled: externalRetrievalSettings.score_threshold_enabled,
|
|
},
|
|
},
|
|
{
|
|
onSuccess: (data) => {
|
|
setExternalHitResult(data)
|
|
onUpdateList?.()
|
|
},
|
|
},
|
|
)
|
|
}, [
|
|
canRunRetrievalRecall,
|
|
text,
|
|
externalRetrievalSettings,
|
|
externalKnowledgeBaseHitTestingMutation,
|
|
onUpdateList,
|
|
setExternalHitResult,
|
|
])
|
|
|
|
const retrievalMethod = isEconomy ? RETRIEVE_METHOD.keywordSearch : retrievalConfig.search_method
|
|
const icon = (
|
|
<img
|
|
className="size-3.5 text-util-colors-purple-purple-600"
|
|
src={getIcon(retrievalMethod)}
|
|
alt=""
|
|
/>
|
|
)
|
|
const TextAreaComp = useMemo(() => {
|
|
return <Textarea text={text} handleTextChange={handleTextChange} />
|
|
}, [text, handleTextChange])
|
|
const ActionButtonComp = useMemo(() => {
|
|
return (
|
|
<Button
|
|
onClick={isExternal ? externalRetrievalTestingOnSubmit : onSubmit}
|
|
variant="primary"
|
|
loading={loading}
|
|
disabled={
|
|
!canRunRetrievalRecall ||
|
|
(text.length === 0 && images.length === 0) ||
|
|
text.length > 200 ||
|
|
(images.length > 0 && !isAllUploaded)
|
|
}
|
|
className="w-[88px]"
|
|
>
|
|
<RiPlayCircleLine className="mr-1 size-4" />
|
|
{t(($) => $['input.testing'], { ns: 'datasetHitTesting' })}
|
|
</Button>
|
|
)
|
|
}, [
|
|
isExternal,
|
|
externalRetrievalTestingOnSubmit,
|
|
onSubmit,
|
|
canRunRetrievalRecall,
|
|
text,
|
|
loading,
|
|
t,
|
|
images,
|
|
isAllUploaded,
|
|
])
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'relative flex h-80 shrink-0 flex-col overflow-hidden rounded-xl bg-linear-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 p-0.5 shadow-xs',
|
|
)}
|
|
>
|
|
<div className="flex h-full flex-col overflow-hidden rounded-[10px] bg-background-section-burn">
|
|
<div className="relative flex shrink-0 items-center justify-between p-1.5 pb-1 pl-3">
|
|
<span className="system-sm-semibold-uppercase text-text-secondary">
|
|
{t(($) => $['input.title'], { ns: 'datasetHitTesting' })}
|
|
</span>
|
|
{isExternal ? (
|
|
<Button
|
|
variant="secondary"
|
|
size="small"
|
|
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
|
>
|
|
<RiEqualizer2Line className="size-3.5 text-components-button-secondary-text" />
|
|
<div className="flex items-center justify-center gap-1 px-[3px]">
|
|
<span className="system-xs-medium text-components-button-secondary-text">
|
|
{t(($) => $.settingTitle, { ns: 'datasetHitTesting' })}
|
|
</span>
|
|
</div>
|
|
</Button>
|
|
) : (
|
|
<div
|
|
onClick={onClickRetrievalMethod}
|
|
className="flex h-7 cursor-pointer items-center space-x-0.5 rounded-lg border-[0.5px] border-components-button-secondary-bg bg-components-button-secondary-bg px-1.5 shadow-xs backdrop-blur-[5px] hover:bg-components-button-secondary-bg-hover"
|
|
>
|
|
{icon}
|
|
<div className="text-xs font-medium text-text-secondary uppercase">
|
|
{t(($) => $[`retrieval.${retrievalMethod}.title`], { ns: 'dataset' })}
|
|
</div>
|
|
<RiEqualizer2Line className="size-4 text-components-menu-item-text"></RiEqualizer2Line>
|
|
</div>
|
|
)}
|
|
{isSettingsOpen && (
|
|
<ModifyExternalRetrievalModal
|
|
onClose={() => setIsSettingsOpen(false)}
|
|
onSave={handleSaveExternalRetrievalSettings}
|
|
initialTopK={externalRetrievalSettings.top_k}
|
|
initialScoreThreshold={externalRetrievalSettings.score_threshold}
|
|
initialScoreThresholdEnabled={externalRetrievalSettings.score_threshold_enabled}
|
|
/>
|
|
)}
|
|
</div>
|
|
<ImageUploaderInRetrievalTesting
|
|
textArea={TextAreaComp}
|
|
actionButton={ActionButtonComp}
|
|
onChange={handleImageChange}
|
|
value={images}
|
|
showUploader={isMultimodal}
|
|
className="grow"
|
|
actionAreaClassName="px-4 py-2 shrink-0 bg-background-default"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default QueryInput
|