cherry-studio/src/renderer/src/hooks/useAssistant.ts
one 1a5138c5b1
refactor: quick panel and inputbar tools (#10142)
* refactor: add QuickPanelReservedSymbol

* refactor: pass assistant id instead of assistant object

* refactor: simplify InputbarTools props

* refactor: add root symbol

* refactor: merge file panel to attachment button

* refactor: extract ActionButton for reuse

* chore: update CLAUDE.md

* fix: colors

* refactor: add more reserved symbols

* fix: keep updateAssistant stable

* refactor(components): replace styled-components with cn utility in ActionIconButton

- Replace styled-components with cn utility from @heroui/react for better maintainability
- Add new --icon and --color-icon CSS variables for consistent icon coloring
- Simplify button styling using Tailwind CSS classes

---------

Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-16 11:10:14 +08:00

207 lines
7.8 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.toast.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: useCallback((assistant: Partial<Assistant>) => dispatch(updateAssistant(assistant)), [dispatch]),
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 }))
}
}