cherry-studio/src/renderer/src/hooks/useAssistant.ts

207 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { loggerService } from '@logger'
import {
getThinkModelType,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel,
MODEL_SUPPORTED_OPTIONS,
MODEL_SUPPORTED_REASONING_EFFORT
} from '@renderer/config/models'
import { db } from '@renderer/databases'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addAssistant,
addTopic,
insertAssistant,
removeAllTopics,
removeAssistant,
removeTopic,
setModel,
updateAssistant,
updateAssistants,
updateAssistantSettings as _updateAssistantSettings,
updateDefaultAssistant,
updateTopic,
updateTopics
} from '@renderer/store/assistants'
import { setDefaultModel, setQuickModel, setTranslateModel } from '@renderer/store/llm'
import { Assistant, AssistantSettings, Model, ThinkingOption, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { TopicManager } from './useTopic'
export function useAssistants() {
const { t } = useTranslation()
const { assistants } = useAppSelector((state) => state.assistants)
const dispatch = useAppDispatch()
const logger = loggerService.withContext('useAssistants')
return {
assistants,
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
insertAssistant: (index: number, assistant: Assistant) => dispatch(insertAssistant({ index, assistant })),
copyAssistant: (assistant: Assistant): Assistant | undefined => {
if (!assistant) {
logger.error("assistant doesn't exists.")
return
}
const index = assistants.findIndex((_assistant) => _assistant.id === assistant.id)
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] }
if (index === -1) {
logger.warn("Origin assistant's id not found. Fallback to addAssistant.")
dispatch(addAssistant(_assistant))
} else {
// 插入到后面
try {
dispatch(insertAssistant({ index: index + 1, assistant: _assistant }))
} catch (e) {
logger.error('Failed to insert assistant', e as Error)
window.message.error(t('message.error.copy'))
}
}
return _assistant
},
removeAssistant: (id: string) => {
dispatch(removeAssistant({ id }))
const assistant = assistants.find((a) => a.id === id)
const topics = assistant?.topics || []
topics.forEach(({ id }) => TopicManager.removeTopic(id))
}
}
}
export function useAssistant(id: string) {
const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant)
const dispatch = useAppDispatch()
const { defaultModel } = useDefaultModel()
const model = useMemo(() => assistant?.model ?? assistant?.defaultModel ?? defaultModel, [assistant, defaultModel])
if (!model) {
throw new Error(`Assistant model is not set for assistant with name: ${assistant?.name ?? 'unknown'}`)
}
const assistantWithModel = useMemo(() => ({ ...assistant, model }), [assistant, model])
const settingsRef = useRef(assistant?.settings)
useEffect(() => {
settingsRef.current = assistant?.settings
}, [assistant?.settings])
const updateAssistantSettings = useCallback(
(settings: Partial<AssistantSettings>) => {
assistant?.id && dispatch(_updateAssistantSettings({ assistantId: assistant.id, settings }))
},
[assistant?.id, dispatch]
)
// 当model变化时同步reasoning effort为模型支持的合法值
useEffect(() => {
const settings = settingsRef.current
if (settings) {
const currentReasoningEffort = settings.reasoning_effort
if (isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)) {
const modelType = getThinkModelType(model)
const supportedOptions = MODEL_SUPPORTED_OPTIONS[modelType]
if (supportedOptions.every((option) => option !== currentReasoningEffort)) {
const cache = settings.reasoning_effort_cache
let fallbackOption: ThinkingOption
// 选项不支持时,首先尝试恢复到上次使用的值
if (cache && supportedOptions.includes(cache)) {
fallbackOption = cache
} else {
// 灵活回退到支持的值
// 注意这里假设可用的options不会为空
const enableThinking = currentReasoningEffort !== undefined
fallbackOption = enableThinking
? MODEL_SUPPORTED_REASONING_EFFORT[modelType][0]
: MODEL_SUPPORTED_OPTIONS[modelType][0]
}
updateAssistantSettings({
reasoning_effort: fallbackOption === 'off' ? undefined : fallbackOption,
reasoning_effort_cache: fallbackOption === 'off' ? undefined : fallbackOption,
qwenThinkMode: fallbackOption === 'off' ? undefined : true
})
} else {
// 对于支持的选项, 不再更新 cache.
}
} else {
// 切换到非思考模型时保留cache
updateAssistantSettings({
reasoning_effort: undefined,
reasoning_effort_cache: currentReasoningEffort,
qwenThinkMode: undefined
})
}
}
}, [model, updateAssistantSettings])
return {
assistant: assistantWithModel,
model,
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => {
TopicManager.removeTopic(topic.id)
dispatch(removeTopic({ assistantId: assistant.id, topic }))
},
moveTopic: (topic: Topic, toAssistant: Assistant) => {
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic, assistantId: toAssistant.id } }))
dispatch(removeTopic({ assistantId: assistant.id, topic }))
// update topic messages in database
db.topics
.where('id')
.equals(topic.id)
.modify((dbTopic) => {
if (dbTopic.messages) {
dbTopic.messages = dbTopic.messages.map((message) => ({
...message,
assistantId: toAssistant.id
}))
}
})
},
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
setModel: useCallback(
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
[assistant, dispatch]
),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
updateAssistantSettings
}
}
export function useDefaultAssistant() {
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
const dispatch = useAppDispatch()
const memoizedTopics = useMemo(() => [getDefaultTopic(defaultAssistant.id)], [defaultAssistant.id])
return {
defaultAssistant: {
...defaultAssistant,
topics: memoizedTopics
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
}
}
export function useDefaultModel() {
const { defaultModel, quickModel, translateModel } = useAppSelector((state) => state.llm)
const dispatch = useAppDispatch()
return {
defaultModel,
quickModel,
translateModel,
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
setQuickModel: (model: Model) => dispatch(setQuickModel({ model })),
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
}
}