From 6ed30fd78a80ca3612e0ce0318581922be7570da Mon Sep 17 00:00:00 2001 From: suyao Date: Thu, 12 Jun 2025 18:29:00 +0800 Subject: [PATCH] Merge 'refactor/assistant-debounce' into 'feat/sidebar-ui' --- .../src/assets/images/providers/302ai.webp | Bin 0 -> 7050 bytes .../src/assets/images/providers/cephalon.jpeg | Bin 0 -> 5997 bytes src/renderer/src/assets/styles/markdown.scss | 13 +- src/renderer/src/config/models.ts | 89 ++++++++- src/renderer/src/config/providers.ts | 28 ++- src/renderer/src/hooks/useAssistant.ts | 49 ++--- src/renderer/src/hooks/useChat.tsx | 11 +- src/renderer/src/hooks/useTopic.ts | 19 +- src/renderer/src/i18n/locales/en-us.json | 4 +- src/renderer/src/i18n/locales/ja-jp.json | 8 +- src/renderer/src/i18n/locales/ru-ru.json | 8 +- src/renderer/src/i18n/locales/zh-cn.json | 4 +- src/renderer/src/i18n/locales/zh-tw.json | 4 +- src/renderer/src/i18n/translate/el-gr.json | 1 + src/renderer/src/i18n/translate/es-es.json | 1 + src/renderer/src/i18n/translate/fr-fr.json | 1 + .../history/components/TopicsHistory.tsx | 9 +- .../pages/home/Messages/MessageMenubar.tsx | 2 +- .../src/pages/home/Messages/Messages.tsx | 2 +- .../src/pages/home/Tabs/TopicsTab.tsx | 35 ++-- .../home/Tabs/components/AssistantItem.tsx | 8 +- src/renderer/src/services/MessagesService.ts | 10 +- src/renderer/src/store/assistants.ts | 120 ++++-------- src/renderer/src/store/index.ts | 6 +- src/renderer/src/store/llm.ts | 18 +- src/renderer/src/store/migrate.ts | 129 +++++++++++- src/renderer/src/store/topics.ts | 185 ++++++++++++++++++ src/renderer/src/types/index.ts | 4 + .../windows/mini/chat/components/Messages.tsx | 8 +- .../src/windows/mini/home/HomeWindow.tsx | 11 +- 30 files changed, 617 insertions(+), 170 deletions(-) create mode 100644 src/renderer/src/assets/images/providers/302ai.webp create mode 100644 src/renderer/src/assets/images/providers/cephalon.jpeg create mode 100644 src/renderer/src/store/topics.ts diff --git a/src/renderer/src/assets/images/providers/302ai.webp b/src/renderer/src/assets/images/providers/302ai.webp new file mode 100644 index 0000000000000000000000000000000000000000..ba7bcc80aeaed40643c244f7963100278d0c9677 GIT binary patch literal 7050 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 literal 0 HcmV?d00001 diff --git a/src/renderer/src/assets/images/providers/cephalon.jpeg b/src/renderer/src/assets/images/providers/cephalon.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..8615e1c80d51510dd9ec87cb4f37de69248debe5 GIT binary patch literal 5997 zcmeHL*IQG~w}u$LUjUWfrAY^AQY^HHbft*_3|)E?L3$4&y-Al)A}DAQAatZx5s;>a zE+9pElPdKL7w0+uz&TgvyO?M1J$u&7tTk)B?^+YBr=xy_f|Y`Vgyf2*hRP!n64IoL z27`bes-JJs;6eK0k-9QTN&mHF@S$RBtZApMO(G25VI-uHjwH~FA)v8>MnZB4LrQW9 zJV`J5Fp&TLOPYka^gr(x1KA|qzLJnIj%cdfd+bZP{?id}W<5b-N%R+zd5qK2cM>FG zuNPuIr&CC(=`+kx-4{e!U%q$w?cu%3aoER>)CiT!d=a0Mq68h;SnpqUjYm99Fl;{l zeo)-bvOl$$-8kT%>y@MAJ#siX(m3pTWdP}1MLeP5f`^ijLZC3}e_#H88sS3$Jdr~o z;r47)B!~Y}oXc4qG)fnJbw_M2ftk!BE zSK&1^*I;@J(z)1cC z=8gXA&F5!ErtYc|!9YXycdL_Qa}8(VbUF?ni4im!+A}SesNw=1<xH~StOVP=?(1 z7HwVkG{5V<1aMMMd8kyt{+e%iy<E zpR3oHPZKw4=MrvB`4+rnKfqDlNfXNRU->BZFdjj77=o&OzXvw#t!1>=KfZlc@o*+$ z!47!$6mY=VPFfIfE?Z>v{j*N<9{I6q?T@Tgp=aFGZYnFgB`Z1QJ2y;g2^P)4=MK(A zXVz%f>SzwE&vekigj@5^Dymjb3L_pS+D)&eACHZhQNqxC+!6js1?#7`BXuPi=Kewn zL0H&(tmdraly6WE!pMw4KxUiv1Cq`9s1PEzM!1hy;=ew454D8lC>2h?Nom(Kx1A-3 z?X4^J$7n=eX7TvUV&5Uh`@28KOYOKsAsVWVz!c1F^i&0@Yb`I$*WW^XW zT*^8b0S@`e*{xxp(Bn&zORXAL@fAAq2)agML6`NF3I?xlzJi+-1B$Vc;rZ&$*~0sq z(;)x|Hak{LRpaZU)?=7-I&v1SSL4%U|BkAONOUq^v+#~_BbxgDdKGlEE2I{C_btP= z-gbJ(saIS1jeKj!5JRCgrNizdg<(M_&eYt(H!=@=5!8li^xtL`$EInEAv{gmhhd5m#?5dWi+eiJDi$&52nB$%bPy zSQDGg{1(zU*08K!!*gyV4WIL{TX}gL;VdRIC9U%xcQQF+^Ey+ZT4ESW8UIUB=%X%n zAJafur^BsZ@#ZyqG#aMxRwq+P0nT4O*8>Ees_w?;VqzA|h8GB2e=`(+{8efouAc(9 z-4mW+dv@qe20g+GdbHNHp&aKQnDD;Ix!e=NM-4jD=6$uq>aglnS{wcDjVd=X;E`nX z_^WiXCUALDPAcK9JC=ZQ(DBltB&dB#LF4rrXT`;t)|X&y(tl7bY{2c^f`@yc8s1^Ae6fN*bWHL~8(*5TqKCBvPL z-Q69$4<@6>?~Au(Lt#IzV=RgJdn1+Z-TZXV(~Y0nHd2U4 zWRak-Kt02cvsPJ@Vzo03JN*wfisOnSY@nzP@Pm<-IZ;sEXb)gxZ%q+$P%;WWLM~{0 zPDo_BSoF~RW68?-^KB|fCz$>W9qF56?uocSNH2u==k$*ZP$cL#jLo1|FT%D{DK(IB zyo6B;Cc{5c8U`M5YZ1S^=7~+Eapi_%5U87s_``=^hSWLP-wP|25QXDtpfGw}j47Fj zqMrXZO#xM~o{i(S$6Hk>>@l_AgVvaVrmn7OV5>k_6C`>Fj7D&shcUcgIoTO#KAepO zI5wAWcds5s{XvUsCrk!}INGhrZ>9x>u*!@gK|Q6kF(G&Z43gVycv0H8FI?R`Ov9!3 z5+1R+6ORrZBHP^?u`nuj8l`@y#YKjiOd!aPTE6@bXW&*8Pl5)i1sgf_2W>7~m<%8f zfxJMELCn1>mP}w$6=)sbxBi~KYd3Aa^olpHUFuDH|L`6r8g8oW_@^}g{Ncx9SL0_j zKfOyE{%D<_olaza7KA{vfH(DNugLNCliC9p1Jr+1^7$M2yddylY21+bE};KwvM$E= zN1SY-9yA&_OTOu1t3UJIScKSd!fjv^jbm2B22ZQsU*9k&OM`t3lYP6}rd4a%S%CtS zZ@xLzXfp6h^1^x-!hu@8fYLq~GRUeGkk9iatphZAAj zY;FzuRZh@KuZx3ARyTGx*#cRH_Uid%Rjs;e>O&K*;#0wHXXBzAZzQrO#Em_ehHH=% zwVObgefa!W+beUTFW%V@w8c~kq19ippG*PaeEd_BbWmBCv%>xb+^?@GA6&6 zs?ngWGnUD%yqiBneTIn?!cP5MYm~G>tKu1eN-TnpcaGdfBxzLWS8l+@8&_edS}2?k z)j=`L5wNB&bbXGT`imh~S(psS9b62!B_L2?a$ZA%ETCsC8W!D9cr_%N0k0TzWP_$a zfuq(0V;5k!N6PU6Nfq#?r-XD-li|jIgKWC%vXFQN4IHQ8Q!?ky7>4-1!YZ0Crd-8g zX`@0`zb6v5VAKu<0CZBiG$7Cz05c3iY~#wIa5RL%5G}7p%caK)qFG~XkP!kMr`IEl z&r2qgpss`1QSFJS1_^6nXc&<{@ilN-O?a#DBiYJVBq&)Bak74Kyl{*LL~+j?Av
nt_Wt!;REMF2KG%W4;AL$(U7{H5iThj(8 zEu$WRRwuWRP7n={r8+J)1KtL-Y3u&(39+TqBlM)ZZ_{cR!&|j(`(^21T)@Avn_}h= za^Sb*0=nPHQ?-F6CZ9VWa9NR(iTs*(yo)La3fuP9`jYeBz*zES%WDhh$pX8$urP_E z!hyE`TbpW$d`y|zu-SB6qMq2p5906p2}TK=N>0*p~h=@YQB zU)U>kE&*VXe<%9Cu@S}u*@`zV%YVSy;4)S&_QWk#WPP~UcI-l7mHbJQSJRAJkp# zEfU49%FYk2Z!PA9>@R;`Jl<^96`bnWKWWL4ve!{=%F@CRAY~%&5yi?k%fc8$b7hL%Cntt`?~MrgLm)&-GpXnRJVn6s<@EiF!e76v8b+?B=qir+&c#oB zi3B2*{98mL3)fEz>D>x{ul?fn3uhRtnGOzc?v}_VK~-0;G(#Chks5gHSju z@OZuEg3PmN3vsVkgnnslV>p$98<-l4>Q5d0+M!QxfePLrxx~;4v71=>_~9Zb|C!(1 zo(a7qtm6oQ*fQ4ck2}+?2N0#5yP5E?-)TnX%hS#0paLJ3mS`rYHs*T{h*T#_+L5UA z{q*(yR*F;mTt7RoYNCC?x4!Unx(_KWJc;rb<;J}G0&hwfLwDw#7lm{9yRO@e=#rLH zAp;$y(_P;^L>;%Zs~V>lsy|IGraP3 zSzU32kY&#|r7P>T^5d|2T}N)cvj6U?bY2TJWDM3Jpzl`uD`H7K=Rlq6WL7 rZF}o%8V$Anyh literal 0 HcmV?d00001 diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index 5b4269e2fa..033de18ab4 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -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 相关样式 */ diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 07581eadb3..acebf6171c 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -429,7 +429,86 @@ export const SYSTEM_MODELS: Record = { 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 = { name: 'Qwen Plus', group: 'Qwen' } + ], + cephalon: [ + { + id: 'DeepSeek-R1', + provider: 'cephalon', + name: 'DeepSeek-R1满血版', + group: 'DeepSeek' + } ] } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index c83522f358..629e6827fd 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -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' + } } } diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index dd2d47b89f..bc9a55fbd2 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -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 })) } diff --git a/src/renderer/src/hooks/useChat.tsx b/src/renderer/src/hooks/useChat.tsx index 64ac8f2f35..cb69eac541 100644 --- a/src/renderer/src/hooks/useChat.tsx +++ b/src/renderer/src/hooks/useChat.tsx @@ -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) { diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index d0d177d763..bd2f0cb7c0 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -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() -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 { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 73d55ea3cd..4a246547ad 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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?", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 1330363aee..45bae14e71 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index ead1d32056..19caafa5b8 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 179575a736..64dd191d04 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "确定要恢复数据吗?", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index bca0952797..42cb03ebb4 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "確定要復原資料嗎?", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 81ff1ab7f0..4362777ee7 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -840,6 +840,7 @@ "azure-openai": "Azure OpenAI", "baichuan": "Παράκειμαι", "baidu-cloud": "Baidu Cloud Qianfan", + "cephalon": "Cephalon", "copilot": "GitHub Copilot", "dashscope": "AliCloud Bailian", "deepseek": "Βαθιά Αναζήτηση", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 679dc678a6..f6323c7490 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -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", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index b7db9ff5fb..0289043f5c 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -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", diff --git a/src/renderer/src/pages/history/components/TopicsHistory.tsx b/src/renderer/src/pages/history/components/TopicsHistory.tsx index d95a3f7ae6..13738567f9 100644 --- a/src/renderer/src/pages/history/components/TopicsHistory.tsx +++ b/src/renderer/src/pages/history/components/TopicsHistory.tsx @@ -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 const TopicsHistory: React.FC = ({ 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()) }) diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index b161cdca53..f3382312ad 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -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' diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 8241742106..20683d2bbf 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -111,7 +111,7 @@ const Messages: React.FC = ({ 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] diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index f58be564ba..1929eba045 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -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 = ({ 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(null) @@ -104,16 +106,16 @@ const Topics: FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ assistant: _assistant, activeTopic, setActiveTopic exportMenuOptions.joplin, exportMenuOptions.siyuan, assistants, + topics.length, assistant, updateTopic, activeTopic.id, @@ -393,14 +396,14 @@ const Topics: FC = ({ 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 ( diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx index fc1f0a6014..348e68876c 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx @@ -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 = ({ 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 = ({ 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)) diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index 178089b25b..ebb8f2df82 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -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) { diff --git a/src/renderer/src/store/assistants.ts b/src/renderer/src/store/assistants.ts index 14213006d3..855008b4cd 100644 --- a/src/renderer/src/store/assistants.ts +++ b/src/renderer/src/store/assistants.ts @@ -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) => { 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) => { 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) => { - 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) => { state.tagsOrder = action.payload @@ -143,11 +112,6 @@ export const { addAssistant, removeAssistant, updateAssistant, - addTopic, - removeTopic, - updateTopic, - updateTopics, - removeAllTopics, setModel, setTagsOrder, updateAssistantSettings diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index d0ded856a0..f5c0a444ee 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -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({ diff --git a/src/renderer/src/store/llm.ts b/src/renderer/src/store/llm.ts index d5c8fd566d..1ca1dad6ae 100644 --- a/src/renderer/src/store/llm.ts +++ b/src/renderer/src/store/llm.ts @@ -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 }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 3f25e375d8..21dc198c35 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -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() + + // 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 = {} + + // 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 = {} + 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 + } } } diff --git a/src/renderer/src/store/topics.ts b/src/renderer/src/store/topics.ts new file mode 100644 index 0000000000..3a623315e0 --- /dev/null +++ b/src/renderer/src/store/topics.ts @@ -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() + +// 2. Define the State Interface +export interface TopicsState extends EntityState { + topicIdsByAssistant: Record // 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) { + const { assistantId, topics } = action.payload + topicsAdapter.upsertMany(state, topics) + state.topicIdsByAssistant[assistantId] = topics.map((t) => t.id) + }, + addTopic(state, action: PayloadAction) { + 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) { + 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) { + 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) { + 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) { + 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) + } +) diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 6c7ef0576f..fe5af00aa4 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -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 & { group?: string[] } +/** + * @deprecated 旧版消息类型,已废弃 + */ export type LegacyMessage = { id: string assistantId: string diff --git a/src/renderer/src/windows/mini/chat/components/Messages.tsx b/src/renderer/src/windows/mini/chat/components/Messages.tsx index 932446312f..92878e9d06 100644 --- a/src/renderer/src/windows/mini/chat/components/Messages.tsx +++ b/src/renderer/src/windows/mini/chat/components/Messages.tsx @@ -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 = ({ assistant, route }) => { // const [messages, setMessages] = useState([]) - 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(null) const messagesRef = useRef(messages) diff --git a/src/renderer/src/windows/mini/home/HomeWindow.tsx b/src/renderer/src/windows/mini/home/HomeWindow.tsx index f5b39fc381..4b9d345e61 100644 --- a/src/renderer/src/windows/mini/home/HomeWindow.tsx +++ b/src/renderer/src/windows/mini/home/HomeWindow.tsx @@ -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(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({