Merge 'refactor/assistant-debounce' into 'feat/sidebar-ui'

This commit is contained in:
suyao 2025-06-12 18:29:00 +08:00
parent d95a4e56f5
commit 6ed30fd78a
No known key found for this signature in database
30 changed files with 617 additions and 170 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -308,13 +308,16 @@ emoji-picker {
--border-size: 0;
}
.katex-display {
.katex,
mjx-container {
display: inline-block;
overflow-x: auto;
overflow-y: hidden;
}
mjx-container {
overflow-x: auto;
overflow-wrap: break-word;
vertical-align: middle;
max-width: 100%;
padding: 1px 2px;
margin-top: -2px;
}
/* CodeMirror 相关样式 */

View File

@ -429,7 +429,86 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'deepseek-ai'
}
],
'302ai': [
{
id: 'deepseek-chat',
name: 'deepseek-chat',
provider: '302ai',
group: 'DeepSeek'
},
{
id: 'deepseek-reasoner',
name: 'deepseek-reasoner',
provider: '302ai',
group: 'DeepSeek'
},
{
id: 'chatgpt-4o-latest',
name: 'chatgpt-4o-latest',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'gpt-4.1',
name: 'gpt-4.1',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'o3',
name: 'o3',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'o4-mini',
name: 'o4-mini',
provider: '302ai',
group: 'OpenAI'
},
{
id: 'qwen3-235b-a22b',
name: 'qwen3-235b-a22b',
provider: '302ai',
group: 'Qwen'
},
{
id: 'gemini-2.5-flash-preview-05-20',
name: 'gemini-2.5-flash-preview-05-20',
provider: '302ai',
group: 'Gemini'
},
{
id: 'gemini-2.5-pro-preview-06-05',
name: 'gemini-2.5-pro-preview-06-05',
provider: '302ai',
group: 'Gemini'
},
{
id: 'claude-sonnet-4-20250514',
provider: '302ai',
name: 'claude-sonnet-4-20250514',
group: 'Anthropic'
},
{
id: 'claude-opus-4-20250514',
provider: '302ai',
name: 'claude-opus-4-20250514',
group: 'Anthropic'
},
{
id: 'jina-clip-v2',
name: 'jina-clip-v2',
provider: '302ai',
group: 'Jina AI'
},
{
id: 'jina-reranker-m0',
name: 'jina-reranker-m0',
provider: '302ai',
group: 'Jina AI'
}
],
aihubmix: [
{
id: 'gpt-4o',
@ -2082,6 +2161,14 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'Qwen Plus',
group: 'Qwen'
}
],
cephalon: [
{
id: 'DeepSeek-R1',
provider: 'cephalon',
name: 'DeepSeek-R1满血版',
group: 'DeepSeek'
}
]
}

View File

@ -1,6 +1,7 @@
import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png'
import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png'
import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png'
import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png'
@ -8,6 +9,7 @@ import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png
import BaiduCloudProviderLogo from '@renderer/assets/images/providers/baidu-cloud.svg'
import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png'
import BurnCloudProviderLogo from '@renderer/assets/images/providers/burncloud.png'
import CephalonProviderLogo from '@renderer/assets/images/providers/cephalon.jpeg'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import DmxapiProviderLogo from '@renderer/assets/images/providers/DMXAPI.png'
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
@ -48,6 +50,7 @@ import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import { TOKENFLUX_HOST } from './constant'
const PROVIDER_LOGO_MAP = {
'302ai': Ai302ProviderLogo,
openai: OpenAiProviderLogo,
silicon: SiliconFlowProviderLogo,
deepseek: DeepSeekProviderLogo,
@ -94,7 +97,8 @@ const PROVIDER_LOGO_MAP = {
alayanew: AlayaNewProviderLogo,
voyageai: VoyageAIProviderLogo,
qiniu: QiniuProviderLogo,
tokenflux: TokenFluxProviderLogo
tokenflux: TokenFluxProviderLogo,
cephalon: CephalonProviderLogo
} as const
export function getProviderLogo(providerId: string) {
@ -106,6 +110,17 @@ export const NOT_SUPPORTED_REANK_PROVIDERS = ['ollama']
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini']
export const PROVIDER_CONFIG = {
'302ai': {
api: {
url: 'https://api.302.ai'
},
websites: {
official: 'https://302.ai',
apiKey: 'https://dash.302.ai/apis/list',
docs: 'https://302ai.apifox.cn/api-147522039',
models: 'https://302.ai/pricing/'
}
},
openai: {
api: {
url: 'https://api.openai.com'
@ -612,5 +627,16 @@ export const PROVIDER_CONFIG = {
docs: `${TOKENFLUX_HOST}/docs`,
models: `${TOKENFLUX_HOST}/models`
}
},
cephalon: {
api: {
url: 'https://cephalon.cloud/user-center/v1/model'
},
websites: {
official: 'https://cephalon.cloud/share/register-landing?invite_id=jSdOYA',
apiKey: 'https://cephalon.cloud/api',
docs: 'https://cephalon.cloud/apitoken/1864244127731589124',
models: 'https://cephalon.cloud/model'
}
}
}

View File

@ -3,24 +3,18 @@ import { getDefaultTopic } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addAssistant,
addTopic,
removeAllTopics,
removeAssistant,
removeTopic,
setModel,
updateAssistant,
updateAssistants,
updateAssistantSettings,
updateDefaultAssistant,
updateTopic,
updateTopics
updateDefaultAssistant
} from '@renderer/store/assistants'
import { setDefaultModel, setQuickAssistantModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { selectTopicsForAssistant, topicsActions } from '@renderer/store/topics'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { useCallback, useMemo } from 'react'
import { TopicManager } from './useTopic'
export function useAssistants() {
const { assistants } = useAppSelector((state) => state.assistants)
const dispatch = useAppDispatch()
@ -31,15 +25,15 @@ export function useAssistants() {
addAssistant: (assistant: Assistant) => dispatch(addAssistant(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))
// Remove all topics for this assistant
dispatch(topicsActions.removeAllTopics({ assistantId: id }))
}
}
}
export function useAssistant(id: string) {
const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant)
const topics = useTopicsForAssistant(id)
const dispatch = useAppDispatch()
const { defaultModel } = useDefaultModel()
@ -48,19 +42,18 @@ export function useAssistant(id: string) {
throw new Error(`Assistant model is not set for assistant with name: ${assistant?.name ?? 'unknown'}`)
}
const assistantWithModel = useMemo(() => ({ ...assistant, model }), [assistant, model])
const assistantWithModel = useMemo(() => ({ ...assistant, model, topics }), [assistant, model, topics])
return {
assistant: assistantWithModel,
model,
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
topics,
addTopic: (topic: Topic) => dispatch(topicsActions.addTopic({ assistantId: id, topic })),
removeTopic: (topic: Topic) => {
TopicManager.removeTopic(topic.id)
dispatch(removeTopic({ assistantId: assistant.id, topic }))
dispatch(topicsActions.removeTopic({ assistantId: id, topicId: topic.id }))
},
moveTopic: (topic: Topic, toAssistant: Assistant) => {
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic, assistantId: toAssistant.id } }))
dispatch(removeTopic({ assistantId: assistant.id, topic }))
dispatch(topicsActions.moveTopic({ fromAssistantId: id, toAssistantId: toAssistant.id, topicId: topic.id }))
// update topic messages in database
db.topics
.where('id')
@ -74,9 +67,9 @@ export function useAssistant(id: string) {
}
})
},
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
updateTopic: (topic: Topic) => dispatch(topicsActions.updateTopic({ assistantId: id, topic })),
updateTopics: (topics: Topic[]) => dispatch(topicsActions.updateTopics({ assistantId: id, topics })),
removeAllTopics: () => dispatch(topicsActions.removeAllTopics({ assistantId: id })),
setModel: useCallback(
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
[assistant, dispatch]
@ -88,15 +81,27 @@ export function useAssistant(id: string) {
}
}
export function useTopicsForAssistant(assistantId: string) {
return useAppSelector((state) => selectTopicsForAssistant(state, assistantId))
}
export function useDefaultAssistant() {
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
const topics = useTopicsForAssistant(defaultAssistant.id)
const dispatch = useAppDispatch()
const memoizedTopics = useMemo(() => [getDefaultTopic(defaultAssistant.id)], [defaultAssistant.id])
// Ensure default assistant has at least one topic
const finalTopics = useMemo(() => {
if (topics.length > 0) {
return topics
}
return [getDefaultTopic(defaultAssistant.id)]
}, [topics, defaultAssistant.id])
return {
defaultAssistant: {
...defaultAssistant,
topics: memoizedTopics
topics: finalTopics
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
}

View File

@ -6,13 +6,14 @@ import { Assistant } from '@renderer/types'
import { Topic } from '@renderer/types'
import { useEffect } from 'react'
import { useAssistants } from './useAssistant'
import { useAssistants, useTopicsForAssistant } from './useAssistant'
import { useSettings } from './useSettings'
export const useChat = () => {
const { assistants } = useAssistants()
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || activeAssistant?.topics[0]!
const topics = useTopicsForAssistant(activeAssistant.id)
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || topics[0]
const { clickAssistantToShowTopic } = useSettings()
const dispatch = useAppDispatch()
@ -24,12 +25,12 @@ export const useChat = () => {
}, [activeTopic, dispatch])
useEffect(() => {
if (activeAssistant?.topics?.find((topic) => topic.id === activeTopic?.id)) {
if (topics.find((topic) => topic.id === activeTopic?.id)) {
return
}
const firstTopic = activeAssistant.topics[0]
const firstTopic = topics[0]
firstTopic && dispatch(setActiveTopic(firstTopic))
}, [activeAssistant, activeTopic?.id, dispatch])
}, [activeAssistant, activeTopic?.id, dispatch, topics])
useEffect(() => {
if (clickAssistantToShowTopic) {

View File

@ -2,7 +2,7 @@ import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants'
import { selectTopicById, topicsActions } from '@renderer/store/topics'
import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { isEmpty } from 'lodash'
@ -11,18 +11,17 @@ import { getStoreSetting } from './useSettings'
const renamingTopics = new Set<string>()
export function useTopic(assistant: Assistant, topicId?: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
export function useTopic(topicId?: string) {
if (!topicId) return undefined
return selectTopicById(store.getState(), topicId)
}
export function getTopic(assistant: Assistant, topicId: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
export function getTopic(topicId: string) {
return selectTopicById(store.getState(), topicId)
}
export async function getTopicById(topicId: string) {
const assistants = store.getState().assistants.assistants
const topics = assistants.map((assistant) => assistant.topics).flat()
const topic = topics.find((topic) => topic.id === topicId)
const topic = selectTopicById(store.getState(), topicId)
const messages = await TopicManager.getTopicMessages(topicId)
return { ...topic, messages } as Topic
}
@ -55,7 +54,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.substring(0, 50)
if (topicName) {
const data = { ...topic, name: topicName } as Topic
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
store.dispatch(topicsActions.updateTopic({ assistantId: assistant.id, topic: data }))
}
return
}
@ -65,7 +64,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) {
const data = { ...topic, name: summaryText }
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
store.dispatch(topicsActions.updateTopic({ assistantId: assistant.id, topic: data }))
}
}
} finally {

View File

@ -980,6 +980,7 @@
"azure-openai": "Azure OpenAI",
"baichuan": "Baichuan",
"baidu-cloud": "Baidu Cloud",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek",
@ -1020,7 +1021,8 @@
"zhipu": "ZHIPU AI",
"voyageai": "Voyage AI",
"qiniu": "Qiniu AI",
"tokenflux": "TokenFlux"
"tokenflux": "TokenFlux",
"302ai": "302.AI"
},
"restore": {
"confirm": "Are you sure you want to restore data?",

View File

@ -706,7 +706,7 @@
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません",
"download.success": "ダウンロードに成功しました",
"download.failed": "ダウンロードに失敗しました",
"error.fetchTopicName": "[to be translated]:Failed to name the topic"
"error.fetchTopicName": "トピック名の取得に失敗しました"
},
"minapp": {
"popup": {
@ -1020,7 +1020,9 @@
"zhipu": "智譜AI",
"voyageai": "Voyage AI",
"qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux"
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"cephalon": "Cephalon"
},
"restore": {
"confirm": "データを復元しますか?",
@ -2045,4 +2047,4 @@
}
}
}
}
}

View File

@ -706,7 +706,7 @@
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!",
"download.success": "Скачано успешно",
"download.failed": "Скачивание не удалось",
"error.fetchTopicName": "[to be translated]:Failed to name the topic"
"error.fetchTopicName": "Не удалось назвать топик"
},
"minapp": {
"popup": {
@ -980,6 +980,7 @@
"azure-openai": "Azure OpenAI",
"baichuan": "Baichuan",
"baidu-cloud": "Baidu Cloud",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot",
"dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek",
@ -1020,7 +1021,8 @@
"zhipu": "ZHIPU AI",
"voyageai": "Voyage AI",
"qiniu": "Qiniu AI",
"tokenflux": "TokenFlux"
"tokenflux": "TokenFlux",
"302ai": "302.AI"
},
"restore": {
"confirm": "Вы уверены, что хотите восстановить данные?",
@ -2045,4 +2047,4 @@
}
}
}
}
}

View File

@ -980,6 +980,7 @@
"azure-openai": "Azure OpenAI",
"baichuan": "百川",
"baidu-cloud": "百度云千帆",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot",
"dashscope": "阿里云百炼",
"deepseek": "深度求索",
@ -1020,7 +1021,8 @@
"zhipu": "智谱AI",
"voyageai": "Voyage AI",
"qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux"
"tokenflux": "TokenFlux",
"302ai": "302.AI"
},
"restore": {
"confirm": "确定要恢复数据吗?",

View File

@ -980,6 +980,7 @@
"azure-openai": "Azure OpenAI",
"baichuan": "百川",
"baidu-cloud": "百度雲千帆",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot",
"dashscope": "阿里雲百鍊",
"deepseek": "深度求索",
@ -1020,7 +1021,8 @@
"zhipu": "智譜 AI",
"voyageai": "Voyage AI",
"qiniu": "七牛雲 AI 推理",
"tokenflux": "TokenFlux"
"tokenflux": "TokenFlux",
"302ai": "302.AI"
},
"restore": {
"confirm": "確定要復原資料嗎?",

View File

@ -840,6 +840,7 @@
"azure-openai": "Azure OpenAI",
"baichuan": "Παράκειμαι",
"baidu-cloud": "Baidu Cloud Qianfan",
"cephalon": "Cephalon",
"copilot": "GitHub Copilot",
"dashscope": "AliCloud Bailian",
"deepseek": "Βαθιά Αναζήτηση",

View File

@ -841,6 +841,7 @@
"azure-openai": "Azure OpenAI",
"baichuan": "BaiChuan",
"baidu-cloud": "Baidu Nube Qiánfān",
"cephalon": "Cephalon",
"copilot": "GitHub Copiloto",
"dashscope": "Álibaba Nube BaiLiàn",
"deepseek": "Profundo Buscar",

View File

@ -840,6 +840,7 @@
"azure-openai": "Azure OpenAI",
"baichuan": "BaiChuan",
"baidu-cloud": "Baidu Cloud Qianfan",
"cephalon": "Cephalon",
"copilot": "GitHub Copilote",
"dashscope": "AliCloud BaiLian",
"deepseek": "DeepSeek",

View File

@ -1,8 +1,9 @@
import { SearchOutlined } from '@ant-design/icons'
import { VStack } from '@renderer/components/Layout'
import { useAssistants } from '@renderer/hooks/useAssistant'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { getTopicById } from '@renderer/hooks/useTopic'
import { useAppSelector } from '@renderer/store'
import { selectAllTopics } from '@renderer/store/topics'
import { Topic } from '@renderer/types'
import { Button, Divider, Empty } from 'antd'
import dayjs from 'dayjs'
@ -17,13 +18,13 @@ type Props = {
} & React.HTMLAttributes<HTMLDivElement>
const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props }) => {
const { assistants } = useAssistants()
const topics = useAppSelector(selectAllTopics)
const { t } = useTranslation()
const { handleScroll, containerRef } = useScrollPosition('TopicsHistory')
const topics = orderBy(assistants.map((assistant) => assistant.topics).flat(), 'createdAt', 'desc')
const orderedTopics = orderBy(topics, 'createdAt', 'desc')
const filteredTopics = topics.filter((topic) => {
const filteredTopics = orderedTopics.filter((topic) => {
return topic.name.toLowerCase().includes(keywords.toLowerCase())
})

View File

@ -15,6 +15,7 @@ import type { Model } from '@renderer/types'
import type { Assistant, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
import {
exportMarkdownToJoplin,
exportMarkdownToSiyuan,
@ -23,7 +24,6 @@ import {
exportMessageToNotion,
messageToMarkdown
} from '@renderer/utils/export'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
// import { withMessageThought } from '@renderer/utils/formats'
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'

View File

@ -111,7 +111,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
setDisplayMessages([])
const _topic = getTopic(assistant, topic.id)
const _topic = getTopic(topic.id)
_topic && updateTopic({ ..._topic, name: defaultTopic.name } as Topic)
},
[assistant, clearTopicMessages, topic.id, updateTopic]

View File

@ -14,7 +14,7 @@ import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import Scrollbar from '@renderer/components/Scrollbar'
import { isMac } from '@renderer/config/constant'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistant, useAssistants, useTopicsForAssistant } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { TopicManager } from '@renderer/hooks/useTopic'
@ -56,6 +56,8 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { t } = useTranslation()
const { showTopicTime, pinTopicsToTop } = useSettings()
const topics = useTopicsForAssistant(_assistant.id)
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
@ -104,16 +106,16 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const handleConfirmDelete = useCallback(
async (topic: Topic, e: React.MouseEvent) => {
e.stopPropagation()
if (assistant.topics.length === 1) {
if (topics.length === 1) {
return onClearMessages(topic)
}
await modelGenerating()
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1])
const index = findIndex(topics, (t) => t.id === topic.id)
setActiveTopic(topics[index + 1 === topics.length ? index - 1 : index + 1])
removeTopic(topic)
setDeletingTopicId(null)
},
[assistant.topics, onClearMessages, removeTopic, setActiveTopic]
[topics, onClearMessages, removeTopic, setActiveTopic]
)
const onPinTopic = useCallback(
@ -128,22 +130,22 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
async (topic: Topic) => {
await modelGenerating()
if (topic.id === activeTopic?.id) {
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1])
const index = findIndex(topics, (t) => t.id === topic.id)
setActiveTopic(topics[index + 1 === topics.length ? index - 1 : index + 1])
}
removeTopic(topic)
},
[assistant.topics, removeTopic, setActiveTopic, activeTopic]
[topics, removeTopic, setActiveTopic, activeTopic]
)
const onMoveTopic = useCallback(
async (topic: Topic, toAssistant: Assistant) => {
await modelGenerating()
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
const index = findIndex(topics, (t) => t.id === topic.id)
setActiveTopic(topics[index + 1 === topics.length ? 0 : index + 1])
moveTopic(topic, toAssistant)
},
[assistant.topics, moveTopic, setActiveTopic]
[topics, moveTopic, setActiveTopic]
)
const onSwitchTopic = useCallback(
@ -340,7 +342,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
}
]
if (assistants.length > 1 && assistant.topics.length > 1) {
if (assistants.length > 1 && topics.length > 1) {
menus.push({
label: t('chat.topics.move_to'),
key: 'move',
@ -355,7 +357,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
})
}
if (assistant.topics.length > 1 && !topic.pinned) {
if (topics.length > 1 && !topic.pinned) {
menus.push({ type: 'divider' })
menus.push({
label: t('common.delete'),
@ -380,6 +382,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
exportMenuOptions.joplin,
exportMenuOptions.siyuan,
assistants,
topics.length,
assistant,
updateTopic,
activeTopic.id,
@ -393,14 +396,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
// Sort topics based on pinned status if pinTopicsToTop is enabled
const sortedTopics = useMemo(() => {
if (pinTopicsToTop) {
return [...assistant.topics].sort((a, b) => {
return [...topics].sort((a, b) => {
if (a.pinned && !b.pinned) return -1
if (!a.pinned && b.pinned) return 1
return 0
})
}
return assistant.topics
}, [assistant.topics, pinTopicsToTop])
return topics
}, [topics, pinTopicsToTop])
return (
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>

View File

@ -13,7 +13,7 @@ import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import EmojiIcon from '@renderer/components/EmojiIcon'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistant, useAssistants, useTopicsForAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTags } from '@renderer/hooks/useTags'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
@ -64,6 +64,8 @@ const AssistantItem: FC<AssistantItemProps> = ({
const defaultModel = getDefaultModel()
const { assistants, updateAssistants } = useAssistants()
const topics = useTopicsForAssistant(assistant.id)
const [isPending, setIsPending] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false)
@ -73,9 +75,9 @@ const AssistantItem: FC<AssistantItemProps> = ({
return
}
const hasPending = assistant.topics.some((topic) => hasTopicPendingRequests(topic.id))
const hasPending = topics.some((topic) => hasTopicPendingRequests(topic.id))
setIsPending(hasPending)
}, [isActive, assistant.topics])
}, [isActive, topics])
const sortByPinyinAsc = useCallback(() => {
updateAssistants(sortAssistantsByPinyin(assistants, true))

View File

@ -6,6 +6,7 @@ import { fetchMessagesSummary } from '@renderer/services/ApiService'
import store from '@renderer/store'
import { messageBlocksSelectors, removeManyBlocks } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { selectTopicsForAssistant } from '@renderer/store/topics'
import type { Assistant, FileType, MCPServer, Model, Topic, Usage } from '@renderer/types'
import { FileTypes } from '@renderer/types'
import type { Message, MessageBlock } from '@renderer/types/newMessage'
@ -265,7 +266,14 @@ export function checkRateLimit(assistant: Assistant): boolean {
return false
}
const topicId = assistant.topics[0].id
const topics = selectTopicsForAssistant(store.getState(), assistant.id)
const firstTopic = topics[0]
if (!firstTopic) {
return false
}
const topicId = firstTopic.id
const messages = selectMessagesForTopic(store.getState(), topicId)
if (!messages || messages.length <= 1) {

View File

@ -1,9 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { TopicManager } from '@renderer/hooks/useTopic'
import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/AssistantService'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { isEmpty, uniqBy } from 'lodash'
import { getDefaultAssistant } from '@renderer/services/AssistantService'
import { Assistant, AssistantSettings, Model } from '@renderer/types'
export interface AssistantsState {
defaultAssistant: Assistant
@ -11,9 +9,14 @@ export interface AssistantsState {
tagsOrder: string[]
}
// 之前的两个实例会导致两个助手不一致的问题
// FIXME: 更彻底的办法在这次重构就直接把二者合并了
// Create a single default assistant instance to ensure consistency
const defaultAssistant = getDefaultAssistant()
const initialState: AssistantsState = {
defaultAssistant: getDefaultAssistant(),
assistants: [getDefaultAssistant()],
defaultAssistant: defaultAssistant,
assistants: [defaultAssistant], // Share the same reference
tagsOrder: []
}
@ -22,10 +25,23 @@ const assistantsSlice = createSlice({
initialState,
reducers: {
updateDefaultAssistant: (state, action: PayloadAction<{ assistant: Assistant }>) => {
state.defaultAssistant = action.payload.assistant
const assistant = action.payload.assistant
state.defaultAssistant = assistant
// Also update the corresponding assistant in the array
const index = state.assistants.findIndex((a) => a.id === assistant.id)
if (index !== -1) {
state.assistants[index] = assistant
}
},
updateAssistants: (state, action: PayloadAction<Assistant[]>) => {
state.assistants = action.payload
// Update defaultAssistant if it exists in the new array
const defaultInArray = action.payload.find((a) => a.id === state.defaultAssistant.id)
if (defaultInArray) {
state.defaultAssistant = defaultInArray
}
},
addAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants.push(action.payload)
@ -34,7 +50,13 @@ const assistantsSlice = createSlice({
state.assistants = state.assistants.filter((c) => c.id !== action.payload.id)
},
updateAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
const assistant = action.payload
state.assistants = state.assistants.map((c) => (c.id === assistant.id ? assistant : c))
// Also update defaultAssistant if it's the same assistant
if (state.defaultAssistant.id === assistant.id) {
state.defaultAssistant = assistant
}
},
updateAssistantSettings: (
state,
@ -58,78 +80,25 @@ const assistantsSlice = createSlice({
}
}
},
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
const topic = action.payload.topic
topic.createdAt = topic.createdAt || new Date().toISOString()
topic.updatedAt = topic.updatedAt || new Date().toISOString()
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
topics: uniqBy([topic, ...assistant.topics], 'id')
}
: assistant
)
},
removeTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
topics: assistant.topics.filter(({ id }) => id !== action.payload.topic.id)
}
: assistant
)
},
updateTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
const newTopic = action.payload.topic
newTopic.updatedAt = new Date().toISOString()
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
topics: assistant.topics.map((topic) => {
const _topic = topic.id === newTopic.id ? newTopic : topic
_topic.messages = []
return _topic
})
}
: assistant
)
},
updateTopics: (state, action: PayloadAction<{ assistantId: string; topics: Topic[] }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
topics: action.payload.topics.map((topic) =>
isEmpty(topic.messages) ? topic : { ...topic, messages: [] }
)
}
: assistant
)
},
removeAllTopics: (state, action: PayloadAction<{ assistantId: string }>) => {
state.assistants = state.assistants.map((assistant) => {
if (assistant.id === action.payload.assistantId) {
assistant.topics.forEach((topic) => TopicManager.removeTopic(topic.id))
return {
...assistant,
topics: [getDefaultTopic(assistant.id)]
}
}
return assistant
})
},
setModel: (state, action: PayloadAction<{ assistantId: string; model: Model }>) => {
const { assistantId, model } = action.payload
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
assistant.id === assistantId
? {
...assistant,
model: action.payload.model
model: model
}
: assistant
)
// Also update defaultAssistant if it's the same assistant
if (state.defaultAssistant.id === assistantId) {
state.defaultAssistant = {
...state.defaultAssistant,
model: model
}
}
},
setTagsOrder: (state, action: PayloadAction<string[]>) => {
state.tagsOrder = action.payload
@ -143,11 +112,6 @@ export const {
addAssistant,
removeAssistant,
updateAssistant,
addTopic,
removeTopic,
updateTopic,
updateTopics,
removeAllTopics,
setModel,
setTagsOrder,
updateAssistantSettings

View File

@ -22,6 +22,7 @@ import runtime from './runtime'
import selectionStore from './selectionStore'
import settings from './settings'
import shortcuts from './shortcuts'
import topics from './topics'
import websearch from './websearch'
const rootReducer = combineReducers({
@ -40,6 +41,7 @@ const rootReducer = combineReducers({
mcp,
copilot,
selectionStore,
topics,
// messages: messagesReducer,
messages: newMessagesReducer,
messageBlocks: messageBlocksReducer,
@ -50,7 +52,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 111,
version: 113,
blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate
},
@ -69,7 +71,7 @@ const persistedReducer = persistReducer(
* Call storeSyncService.subscribe() in the window's entryPoint.tsx
*/
storeSyncService.setOptions({
syncList: ['assistants/', 'settings/', 'llm/', 'selectionStore/']
syncList: ['assistants/', 'settings/', 'llm/', 'selectionStore/', 'topics/']
})
const store = configureStore({

View File

@ -127,12 +127,22 @@ export const INITIAL_PROVIDERS: Provider[] = [
enabled: false
},
{
id: 'o3',
name: 'O3',
id: '302ai',
name: '302.AI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.o3.fan',
models: SYSTEM_MODELS.o3,
apiHost: 'https://api.302.ai',
models: SYSTEM_MODELS['302ai'],
isSystem: true,
enabled: false
},
{
id: 'cephalon',
name: 'Cephalon',
type: 'openai',
apiKey: '',
apiHost: 'https://cephalon.cloud/user-center/v1/model',
models: SYSTEM_MODELS.cephalon,
isSystem: true,
enabled: false
},

View File

@ -5,7 +5,8 @@ import { SYSTEM_MODELS } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { Assistant, WebSearchProvider } from '@renderer/types'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { Assistant, Topic, WebSearchProvider } from '@renderer/types'
import { getDefaultGroupName, getLeadingEmoji, runAsyncFunction, uuid } from '@renderer/utils'
import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist'
@ -1555,6 +1556,132 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'112': (state: RootState) => {
try {
addProvider(state, 'cephalon')
addProvider(state, '302ai')
state.llm.providers = moveProvider(state.llm.providers, 'cephalon', 13)
state.llm.providers = moveProvider(state.llm.providers, '302ai', 14)
return state
} catch (error) {
return state
}
},
'113': (state: RootState) => {
try {
// Step 1: Merge defaultAssistant and assistants[0] topics to ensure consistency
// This fixes any inconsistencies from backup restores or previous versions
if (state.assistants?.defaultAssistant && state.assistants?.assistants?.length > 0) {
const defaultAssistantId = state.assistants.defaultAssistant.id
const defaultAssistantInArray = state.assistants.assistants.find((a) => a.id === defaultAssistantId)
if (defaultAssistantInArray) {
// Merge topics from both defaultAssistant and assistants[0]
const defaultTopics = state.assistants.defaultAssistant.topics || []
const arrayTopics = defaultAssistantInArray.topics || []
// Create a map to avoid duplicates (by topic id)
const topicsMap = new Map<string, Topic>()
// Add topics from both sources
const allTopics = [...defaultTopics, ...arrayTopics]
allTopics.forEach((topic) => {
if (topic && topic.id) {
// Keep the one with more recent updatedAt, or prefer the one from assistants array
const existing = topicsMap.get(topic.id)
if (
!existing ||
(topic.updatedAt && existing.updatedAt && topic.updatedAt > existing.updatedAt) ||
arrayTopics.includes(topic)
) {
topicsMap.set(topic.id, topic)
}
}
})
const mergedTopics = Array.from(topicsMap.values())
// Update both defaultAssistant and the assistant in array
state.assistants.defaultAssistant.topics = mergedTopics
defaultAssistantInArray.topics = mergedTopics
} else {
// defaultAssistant not found in array, add it
state.assistants.assistants.unshift(state.assistants.defaultAssistant)
}
}
// Step 2: Migrate from nested topic structure to flattened topic structure
// This should run after v112 which ensures defaultAssistant and assistants[0] consistency
// Initialize the new topics slice if it doesn't exist
if (!state.topics) {
state.topics = {
ids: [],
entities: {},
topicIdsByAssistant: {}
}
}
// Type for legacy assistant with topics
type LegacyAssistant = Assistant
// Extract all topics from assistants and flatten them
const allTopics: Topic[] = []
const topicIdsByAssistant: Record<string, string[]> = {}
// Process regular assistants
if (state.assistants?.assistants) {
state.assistants.assistants.forEach((assistant) => {
const legacyAssistant = assistant as LegacyAssistant
if (legacyAssistant.topics && Array.isArray(legacyAssistant.topics) && legacyAssistant.topics.length > 0) {
allTopics.push(...legacyAssistant.topics)
topicIdsByAssistant[assistant.id] = legacyAssistant.topics.map((t: Topic) => t.id)
// Clear deprecated field
legacyAssistant.topics = []
} else {
// Create default topic for assistant with no topics
const defaultTopic = getDefaultTopic(assistant.id)
allTopics.push(defaultTopic)
topicIdsByAssistant[assistant.id] = [defaultTopic.id]
// Set deprecated field
legacyAssistant.topics = []
}
})
}
// Process default assistant - should already be consistent after v112
if (state.assistants?.defaultAssistant) {
const legacyDefaultAssistant = state.assistants.defaultAssistant as LegacyAssistant
// Since v112 already ensured consistency, just clear the deprecated field
legacyDefaultAssistant.topics = []
}
// Populate the new topics slice
const topicEntities: Record<string, Topic> = {}
const topicIds: string[] = []
allTopics.forEach((topic) => {
topicEntities[topic.id] = topic
topicIds.push(topic.id)
})
// Update topics slice
state.topics = {
ids: topicIds,
entities: topicEntities,
topicIdsByAssistant
}
return state
} catch (error) {
console.error('Migration 112 failed:', error)
return state
}
}
}

View File

@ -0,0 +1,185 @@
import { createEntityAdapter, createSlice, EntityState, PayloadAction } from '@reduxjs/toolkit'
// --- Selectors ---
import { createSelector } from '@reduxjs/toolkit'
import { TopicManager } from '@renderer/hooks/useTopic'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { Topic } from '@renderer/types'
import type { RootState } from './index'
// 1. Create the Adapter
const topicsAdapter = createEntityAdapter<Topic>()
// 2. Define the State Interface
export interface TopicsState extends EntityState<Topic, string> {
topicIdsByAssistant: Record<string, string[]> // Map: assistantId -> ordered topic IDs
}
// Create default topic for default assistant
const defaultTopic = getDefaultTopic('default')
// 3. Define the Initial State with default topic
const initialState: TopicsState = topicsAdapter.getInitialState(
{
topicIdsByAssistant: {
default: [defaultTopic.id] // Default assistant has default topic
}
},
{
[defaultTopic.id]: defaultTopic // Add default topic to entities
}
)
// Payload types
export interface TopicsReceivedPayload {
assistantId: string
topics: Topic[]
}
export interface AddTopicPayload {
assistantId: string
topic: Topic
}
export interface RemoveTopicPayload {
assistantId: string
topicId: string
}
export interface UpdateTopicPayload {
assistantId: string
topic: Topic
}
export interface MoveTopicPayload {
fromAssistantId: string
toAssistantId: string
topicId: string
}
// 4. Create the Slice
const topicsSlice = createSlice({
name: 'topics',
initialState,
reducers: {
topicsReceived(state, action: PayloadAction<TopicsReceivedPayload>) {
const { assistantId, topics } = action.payload
topicsAdapter.upsertMany(state, topics)
state.topicIdsByAssistant[assistantId] = topics.map((t) => t.id)
},
addTopic(state, action: PayloadAction<AddTopicPayload>) {
const { assistantId, topic } = action.payload
const topicWithTimestamp = {
...topic,
createdAt: topic.createdAt || new Date().toISOString(),
updatedAt: topic.updatedAt || new Date().toISOString()
}
topicsAdapter.addOne(state, topicWithTimestamp)
if (!state.topicIdsByAssistant[assistantId]) {
state.topicIdsByAssistant[assistantId] = []
}
// Add to the beginning to match original behavior
state.topicIdsByAssistant[assistantId].unshift(topic.id)
},
removeTopic(state, action: PayloadAction<RemoveTopicPayload>) {
const { assistantId, topicId } = action.payload
topicsAdapter.removeOne(state, topicId)
const currentTopicIds = state.topicIdsByAssistant[assistantId]
if (currentTopicIds) {
state.topicIdsByAssistant[assistantId] = currentTopicIds.filter((id) => id !== topicId)
}
// Remove topic from database
TopicManager.removeTopic(topicId)
},
updateTopic(state, action: PayloadAction<UpdateTopicPayload>) {
const { topic } = action.payload
const updatedTopic = {
...topic,
updatedAt: new Date().toISOString()
}
topicsAdapter.updateOne(state, {
id: topic.id,
changes: { ...updatedTopic, messages: [] } // Clear messages in redux to match original behavior
})
},
updateTopics(state, action: PayloadAction<TopicsReceivedPayload>) {
const { assistantId, topics } = action.payload
const topicsWithoutMessages = topics.map((topic) => ({
...topic,
messages: [] // Clear messages in redux
}))
topicsAdapter.upsertMany(state, topicsWithoutMessages)
state.topicIdsByAssistant[assistantId] = topics.map((t) => t.id)
},
removeAllTopics(state, action: PayloadAction<{ assistantId: string }>) {
const { assistantId } = action.payload
const topicIds = state.topicIdsByAssistant[assistantId] || []
// Remove topics from database
topicIds.forEach((topicId) => TopicManager.removeTopic(topicId))
// Remove topics from redux
topicsAdapter.removeMany(state, topicIds)
// Create default topic
const defaultTopic = getDefaultTopic(assistantId)
topicsAdapter.addOne(state, defaultTopic)
state.topicIdsByAssistant[assistantId] = [defaultTopic.id]
},
moveTopic(state, action: PayloadAction<MoveTopicPayload>) {
const { fromAssistantId, toAssistantId, topicId } = action.payload
// Update topic's assistantId
topicsAdapter.updateOne(state, {
id: topicId,
changes: { assistantId: toAssistantId }
})
// Remove from source assistant's topic list
const fromTopicIds = state.topicIdsByAssistant[fromAssistantId]
if (fromTopicIds) {
state.topicIdsByAssistant[fromAssistantId] = fromTopicIds.filter((id) => id !== topicId)
}
// Add to target assistant's topic list
if (!state.topicIdsByAssistant[toAssistantId]) {
state.topicIdsByAssistant[toAssistantId] = []
}
state.topicIdsByAssistant[toAssistantId].unshift(topicId)
}
}
})
// 5. Export Actions and Reducer
export const topicsActions = topicsSlice.actions
export default topicsSlice.reducer
// Base selector for the topics slice state
export const selectTopicsState = (state: RootState) => state.topics
// Selectors generated by createEntityAdapter
export const {
selectAll: selectAllTopics,
selectById: selectTopicById,
selectIds: selectAllTopicIds,
selectEntities: selectTopicEntities
} = topicsAdapter.getSelectors(selectTopicsState)
// Custom Selector: Select topics for a specific assistant in order
export const selectTopicsForAssistant = createSelector(
[selectTopicEntities, (state: RootState, assistantId: string) => state.topics.topicIdsByAssistant[assistantId]],
(topicEntities, assistantTopicIds) => {
if (!assistantTopicIds) {
return []
}
return assistantTopicIds.map((id) => topicEntities[id]).filter((t): t is Topic => !!t)
}
)

View File

@ -10,6 +10,7 @@ export type Assistant = {
name: string
prompt: string
knowledge_bases?: KnowledgeBase[]
/** @deprecated 话题现在通过独立的 topics slice 管理,请使用 selectTopicsForAssistant selector */
topics: Topic[]
type: string
emoji?: string
@ -69,6 +70,9 @@ export type Agent = Omit<Assistant, 'model'> & {
group?: string[]
}
/**
* @deprecated
*/
export type LegacyMessage = {
id: string
assistantId: string

View File

@ -1,9 +1,10 @@
import Scrollbar from '@renderer/components/Scrollbar'
import { useTopicsForAssistant } from '@renderer/hooks/useAssistant'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import { Assistant } from '@renderer/types'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { last } from 'lodash'
import { FC, useRef } from 'react'
import { FC, useMemo, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -21,7 +22,10 @@ interface ContainerProps {
const Messages: FC<Props> = ({ assistant, route }) => {
// const [messages, setMessages] = useState<Message[]>([])
const messages = useTopicMessages(assistant.topics[0].id)
const topics = useTopicsForAssistant(assistant.id)
const firstTopic = useMemo(() => topics[0], [topics])
const messages = useTopicMessages(firstTopic?.id || '')
const containerRef = useRef<HTMLDivElement>(null)
const messagesRef = useRef(messages)

View File

@ -1,6 +1,6 @@
import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useDefaultAssistant, useDefaultModel, useTopicsForAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { fetchChatCompletion } from '@renderer/services/ApiService'
@ -20,7 +20,7 @@ import { IpcChannel } from '@shared/IpcChannel'
import { Divider } from 'antd'
import dayjs from 'dayjs'
import { isEmpty } from 'lodash'
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -41,7 +41,10 @@ const HomeWindow: FC = () => {
const [lastClipboardText, setLastClipboardText] = useState<string | null>(null)
const textChange = useState(() => {})[1]
const { defaultAssistant } = useDefaultAssistant()
const topic = defaultAssistant.topics[0]
const topics = useTopicsForAssistant(defaultAssistant.id)
const topic = useMemo(() => topics[0], [topics])
const { defaultModel, quickAssistantModel } = useDefaultModel()
// 如果 quickAssistantModel 未設定,則使用 defaultModel
const model = quickAssistantModel || defaultModel
@ -182,7 +185,7 @@ const HomeWindow: FC = () => {
let blockId: string | null = null
let blockContent: string = ''
const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
const assistantMessage = getAssistantMessage({ assistant, topic })
store.dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
fetchChatCompletion({