mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 11:44:28 +08:00
Merge 'refactor/assistant-debounce' into 'feat/sidebar-ui'
This commit is contained in:
parent
d95a4e56f5
commit
6ed30fd78a
BIN
src/renderer/src/assets/images/providers/302ai.webp
Normal file
BIN
src/renderer/src/assets/images/providers/302ai.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src/renderer/src/assets/images/providers/cephalon.jpeg
Normal file
BIN
src/renderer/src/assets/images/providers/cephalon.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
@ -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 相关样式 */
|
||||
|
||||
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }))
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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?",
|
||||
|
||||
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": "确定要恢复数据吗?",
|
||||
|
||||
@ -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": "確定要復原資料嗎?",
|
||||
|
||||
@ -840,6 +840,7 @@
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Παράκειμαι",
|
||||
"baidu-cloud": "Baidu Cloud Qianfan",
|
||||
"cephalon": "Cephalon",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "AliCloud Bailian",
|
||||
"deepseek": "Βαθιά Αναζήτηση",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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())
|
||||
})
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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']}>
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
185
src/renderer/src/store/topics.ts
Normal file
185
src/renderer/src/store/topics.ts
Normal 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)
|
||||
}
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user