cherry-studio/src/renderer/src/hooks/useKnowledgeBaseForm.ts
2025-09-15 17:59:46 +08:00

157 lines
5.1 KiB
TypeScript

import { getEmbeddingMaxContext } from '@renderer/config/embedings'
import { usePreprocessProviders } from '@renderer/hooks/usePreprocess'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { KnowledgeBase } from '@renderer/types'
import { nanoid } from 'nanoid'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
const createInitialKnowledgeBase = (): KnowledgeBase => ({
id: nanoid(),
name: '',
model: null as any, // model is required, but will be set by user interaction
items: [],
created_at: Date.now(),
updated_at: Date.now(),
version: 1
})
/**
* A hook that manages the state and handlers for a knowledge base form.
*
* The hook provides:
* - A state object `newBase` that tracks the current form values.
* - A function `setNewBase` to update the form state.
* - A set of handlers for various form actions:
* - `handleEmbeddingModelChange`: Updates the embedding model.
* - `handleRerankModelChange`: Updates the rerank model.
* - `handleDimensionChange`: Updates the dimensions.
* - `handleDocPreprocessChange`: Updates the document preprocess provider.
* - `handleChunkSizeChange`: Updates the chunk size.
* - `handleChunkOverlapChange`: Updates the chunk overlap.
* - `handleThresholdChange`: Updates the threshold.
* @param base - The base knowledge base to use as the initial state. If not provided, an empty base will be used.
* @returns An object containing the new base state, a function to update the base, and handlers for various form actions.
* Also includes provider data for dropdown options and selected provider.
*/
export const useKnowledgeBaseForm = (base?: KnowledgeBase) => {
const { t } = useTranslation()
const [newBase, setNewBase] = useState<KnowledgeBase>(base || createInitialKnowledgeBase())
const { providers } = useProviders()
const { preprocessProviders } = usePreprocessProviders()
useEffect(() => {
if (base) {
setNewBase(base)
}
}, [base])
const selectedDocPreprocessProvider = useMemo(
() => newBase.preprocessProvider?.provider,
[newBase.preprocessProvider]
)
const docPreprocessSelectOptions = useMemo(() => {
const preprocessOptions = {
label: t('settings.tool.preprocess.provider'),
title: t('settings.tool.preprocess.provider'),
options: preprocessProviders
.filter((p) => p.apiKey !== '' || p.id === 'mineru')
.map((p) => ({ value: p.id, label: p.name }))
}
return [preprocessOptions]
}, [preprocessProviders, t])
const handleEmbeddingModelChange = useCallback(
(value: string) => {
const model = providers.flatMap((p) => p.models).find((m) => getModelUniqId(m) === value)
if (model) {
setNewBase((prev) => ({ ...prev, model }))
}
},
[providers]
)
const handleRerankModelChange = useCallback(
(value: string) => {
const rerankModel = value
? providers.flatMap((p) => p.models).find((m) => getModelUniqId(m) === value)
: undefined
setNewBase((prev) => ({ ...prev, rerankModel }))
},
[providers]
)
const handleDimensionChange = useCallback((value: number | null) => {
setNewBase((prev) => ({ ...prev, dimensions: value || undefined }))
}, [])
const handleDocPreprocessChange = useCallback(
(value: string) => {
const provider = preprocessProviders.find((p) => p.id === value)
if (!provider) {
setNewBase((prev) => ({ ...prev, preprocessProvider: undefined }))
return
}
setNewBase((prev) => ({
...prev,
preprocessProvider: {
type: 'preprocess',
provider
}
}))
},
[preprocessProviders]
)
const handleChunkSizeChange = useCallback(
(value: number | null) => {
const modelId = newBase.model?.id || base?.model?.id
if (!modelId) return
const maxContext = getEmbeddingMaxContext(modelId)
if (!value || !maxContext || value <= maxContext) {
setNewBase((prev) => ({ ...prev, chunkSize: value || undefined }))
}
},
[newBase.model, base?.model]
)
const handleChunkOverlapChange = useCallback(
(value: number | null) => {
if (!value || (newBase.chunkSize && newBase.chunkSize > value)) {
setNewBase((prev) => ({ ...prev, chunkOverlap: value || undefined }))
} else {
window.toast.error(t('message.error.chunk_overlap_too_large'))
}
},
[newBase.chunkSize, t]
)
const handleThresholdChange = useCallback(
(value: number | null) => {
setNewBase((prev) => ({ ...prev, threshold: value || undefined }))
},
[setNewBase]
)
const handlers = {
handleEmbeddingModelChange,
handleRerankModelChange,
handleDimensionChange,
handleDocPreprocessChange,
handleChunkSizeChange,
handleChunkOverlapChange,
handleThresholdChange
}
const providerData = {
providers,
preprocessProviders,
selectedDocPreprocessProvider,
docPreprocessSelectOptions
}
return { newBase, setNewBase, handlers, providerData }
}