refactor: enhance preference management and streamline component integrations

- Updated various components to utilize the usePreference hook, replacing previous useSettings references for improved consistency and maintainability.
- Introduced new preference types for ChatMessageStyle, ChatMessageNavigationMode, and MultiModelMessageStyle to enhance type safety.
- Refactored preference handling in multiple files, ensuring a more streamlined approach to managing user preferences.
- Cleaned up unused code and comments related to previous settings for better clarity and maintainability.
- Updated auto-generated preference mappings to reflect recent changes in preference structure.
This commit is contained in:
fullex 2025-09-03 16:48:04 +08:00
parent 68cd87e069
commit 566dd14fed
52 changed files with 3480 additions and 3333 deletions

View File

@ -65,3 +65,11 @@ export enum UpgradeChannel {
RC = 'rc', // 公测版本 RC = 'rc', // 公测版本
BETA = 'beta' // 预览版本 BETA = 'beta' // 预览版本
} }
export type ChatMessageStyle = 'plain' | 'bubble'
export type ChatMessageNavigationMode = 'none' | 'buttons' | 'anchor'
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
export type MultiModelGridPopoverTrigger = 'hover' | 'click'

View File

@ -13,7 +13,12 @@ import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import type { import type {
AssistantIconType, AssistantIconType,
AssistantTabSortType, AssistantTabSortType,
ChatMessageNavigationMode,
ChatMessageStyle,
LanguageVarious,
MultiModelFoldDisplayMode, MultiModelFoldDisplayMode,
MultiModelGridPopoverTrigger,
MultiModelMessageStyle,
ProxyMode, ProxyMode,
SelectionActionItem, SelectionActionItem,
SelectionFilterMode, SelectionFilterMode,
@ -22,7 +27,7 @@ import type {
SidebarIcon, SidebarIcon,
WindowStyle WindowStyle
} from '@shared/data/preferenceTypes' } from '@shared/data/preferenceTypes'
import { LanguageVarious, ThemeMode, UpgradeChannel } from '@shared/data/preferenceTypes' import { ThemeMode, UpgradeChannel } from '@shared/data/preferenceTypes'
/* eslint @typescript-eslint/member-ordering: ["error", { /* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" }, "interfaces": { "order": "alphabetically" },
@ -148,11 +153,11 @@ export interface PreferencesType {
// redux/settings/gridColumns // redux/settings/gridColumns
'chat.message.multi_model.grid_columns': number 'chat.message.multi_model.grid_columns': number
// redux/settings/gridPopoverTrigger // redux/settings/gridPopoverTrigger
'chat.message.multi_model.grid_popover_trigger': string 'chat.message.multi_model.grid_popover_trigger': MultiModelGridPopoverTrigger
// redux/settings/multiModelMessageStyle // redux/settings/multiModelMessageStyle
'chat.message.multi_model.style': string 'chat.message.multi_model.style': MultiModelMessageStyle
// redux/settings/messageNavigation // redux/settings/messageNavigation
'chat.message.navigation_mode': string 'chat.message.navigation_mode': ChatMessageNavigationMode
// redux/settings/renderInputMessageAsMarkdown // redux/settings/renderInputMessageAsMarkdown
'chat.message.render_as_markdown': boolean 'chat.message.render_as_markdown': boolean
// redux/settings/showMessageDivider // redux/settings/showMessageDivider
@ -162,7 +167,7 @@ export interface PreferencesType {
// redux/settings/showPrompt // redux/settings/showPrompt
'chat.message.show_prompt': boolean 'chat.message.show_prompt': boolean
// redux/settings/messageStyle // redux/settings/messageStyle
'chat.message.style': string 'chat.message.style': ChatMessageStyle
// redux/settings/thoughtAutoCollapse // redux/settings/thoughtAutoCollapse
'chat.message.thought.auto_collapse': boolean 'chat.message.thought.auto_collapse': boolean
// redux/settings/narrowMode // redux/settings/narrowMode

View File

@ -1,6 +1,6 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store'
import type { Topic } from '@renderer/types' import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { import {
@ -12,7 +12,6 @@ import {
} from '@renderer/utils/export' } from '@renderer/utils/export'
import { Alert, Empty, Form, Input, Modal, Select, Spin, Switch, TreeSelect } from 'antd' import { Alert, Empty, Form, Input, Modal, Select, Spin, Switch, TreeSelect } from 'antd'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
const logger = loggerService.withContext('ObsidianExportDialog') const logger = loggerService.withContext('ObsidianExportDialog')
const { Option } = Select const { Option } = Select
@ -142,7 +141,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
messages, messages,
topic topic
}) => { }) => {
const defaultObsidianVault = store.getState().settings.defaultObsidianVault const [defaultObsidianVault, setDefaultObsidianVault] = usePreference('data.integration.obsidian.default_vault')
const [state, setState] = useState({ const [state, setState] = useState({
title, title,
tags: obsidianTags || '', tags: obsidianTags || '',
@ -202,7 +201,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
} }
} }
fetchVaults() fetchVaults()
}, [defaultObsidianVault]) }, [defaultObsidianVault, setDefaultObsidianVault])
useEffect(() => { useEffect(() => {
if (selectedVault) { if (selectedVault) {
@ -232,9 +231,9 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
if (topic) { if (topic) {
markdown = await topicToMarkdown(topic, exportReasoning) markdown = await topicToMarkdown(topic, exportReasoning)
} else if (messages && messages.length > 0) { } else if (messages && messages.length > 0) {
markdown = messagesToMarkdown(messages, exportReasoning) markdown = await messagesToMarkdown(messages, exportReasoning)
} else if (message) { } else if (message) {
markdown = exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message) markdown = exportReasoning ? await messageToMarkdownWithReasoning(message) : await messageToMarkdown(message)
} else { } else {
markdown = '' markdown = ''
} }

View File

@ -1,7 +1,7 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { getBackupProgressLabel } from '@renderer/i18n/label' import { getBackupProgressLabel } from '@renderer/i18n/label'
import { backup } from '@renderer/services/BackupService' import { backup } from '@renderer/services/BackupService'
import store from '@renderer/store'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { Modal, Progress } from 'antd' import { Modal, Progress } from 'antd'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -27,7 +27,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [progressData, setProgressData] = useState<ProgressData>() const [progressData, setProgressData] = useState<ProgressData>()
const { t } = useTranslation() const { t } = useTranslation()
const skipBackupFile = store.getState().settings.skipBackupFile const [skipBackupFile] = usePreference('data.backup.general.skip_backup_file')
useEffect(() => { useEffect(() => {
const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => { const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => {

View File

@ -210,9 +210,9 @@ export class PreferenceService {
} }
/** /**
* Get multiple preferences at once * Get multiple preferences at once, return is Partial<PreferenceDefaultScopeType>
*/ */
public async getMultiple(keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> { public async getMultipleRaw(keys: PreferenceKeyType[]): Promise<Partial<PreferenceDefaultScopeType>> {
// Check which keys are already cached // Check which keys are already cached
const cachedResults: Partial<PreferenceDefaultScopeType> = {} const cachedResults: Partial<PreferenceDefaultScopeType> = {}
const uncachedKeys: PreferenceKeyType[] = [] const uncachedKeys: PreferenceKeyType[] = []
@ -258,6 +258,23 @@ export class PreferenceService {
return cachedResults return cachedResults
} }
/**
* Get multiple preferences at once and return them as a record of key-value pairs
*/
public async getMultiple<T extends Record<string, PreferenceKeyType>>(
keys: T
): Promise<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }> {
const values = await this.getMultipleRaw(Object.values(keys))
const result = {} as { [P in keyof T]: PreferenceDefaultScopeType[T[P]] }
for (const key in keys) {
result[key] = values[keys[key]]!
}
return result
}
/** /**
* Set multiple preferences at once with configurable update strategy * Set multiple preferences at once with configurable update strategy
*/ */
@ -454,7 +471,7 @@ export class PreferenceService {
if (uncachedKeys.length > 0) { if (uncachedKeys.length > 0) {
try { try {
const values = await this.getMultiple(uncachedKeys) const values = await this.getMultipleRaw(uncachedKeys)
logger.debug(`Preloaded ${Object.keys(values).length} preferences`) logger.debug(`Preloaded ${Object.keys(values).length} preferences`)
} catch (error) { } catch (error) {
logger.error('Failed to preload preferences:', error as Error) logger.error('Failed to preload preferences:', error as Error)

View File

@ -315,7 +315,7 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
}) })
if (uncachedKeys.length > 0) { if (uncachedKeys.length > 0) {
preferenceService.getMultiple(uncachedKeys).catch((error) => { preferenceService.getMultipleRaw(uncachedKeys).catch((error) => {
logger.error('Failed to load initial preferences:', error as Error) logger.error('Failed to load initial preferences:', error as Error)
}) })
} }

View File

@ -21,25 +21,19 @@ import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant' import { useDefaultModel } from './useAssistant'
import useFullScreenNotice from './useFullScreenNotice' import useFullScreenNotice from './useFullScreenNotice'
import { useRuntime } from './useRuntime' import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler' import useUpdateHandler from './useUpdateHandler'
const logger = loggerService.withContext('useAppInit') const logger = loggerService.withContext('useAppInit')
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const {
proxyUrl,
proxyBypassRules,
// language,
// windowStyle,
autoCheckUpdate,
proxyMode,
// customCss,
enableDataCollection
} = useSettings()
const [language] = usePreference('app.language') const [language] = usePreference('app.language')
const [windowStyle] = usePreference('ui.window_style') const [windowStyle] = usePreference('ui.window_style')
const [customCss] = usePreference('ui.custom_css') const [customCss] = usePreference('ui.custom_css')
const [proxyUrl] = usePreference('app.proxy.url')
const [proxyBypassRules] = usePreference('app.proxy.bypass_rules')
const [autoCheckUpdate] = usePreference('app.dist.auto_update.enabled')
const [proxyMode] = usePreference('app.proxy.mode')
const [enableDataCollection] = usePreference('app.privacy.data_collection.enabled')
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel() const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel()

View File

@ -1,8 +1,10 @@
import store, { useAppSelector } from '@renderer/store' //TODO data refactor
import { // this file will be removed
SettingsState
// setWindowStyle import { usePreference } from '@data/hooks/usePreference'
} from '@renderer/store/settings' import { useAppSelector } from '@renderer/store'
import store from '@renderer/store'
import { SettingsState } from '@renderer/store/settings'
export function useSettings() { export function useSettings() {
const settings = useAppSelector((state) => state.settings) const settings = useAppSelector((state) => state.settings)
@ -87,7 +89,7 @@ export function useSettings() {
} }
export function useMessageStyle() { export function useMessageStyle() {
const { messageStyle } = useSettings() const [messageStyle] = usePreference('chat.message.style')
const isBubbleStyle = messageStyle === 'bubble' const isBubbleStyle = messageStyle === 'bubble'
return { return {
@ -112,6 +114,6 @@ export const getStoreSetting = (key: keyof SettingsState) => {
// } // }
// } // }
export const getEnableDeveloperMode = () => { // export const getEnableDeveloperMode = () => {
return store.getState().settings.enableDeveloperMode // return store.getState().settings.enableDeveloperMode
} // }

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { fetchMessagesSummary } from '@renderer/services/ApiService' import { fetchMessagesSummary } from '@renderer/services/ApiService'
@ -13,7 +14,6 @@ import { find, isEmpty } from 'lodash'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant' import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings'
let _activeTopic: Topic let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void let _setActiveTopic: (topic: Topic) => void
@ -109,7 +109,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
topicRenamingLocks.add(topicId) topicRenamingLocks.add(topicId)
const topic = await getTopicById(topicId) const topic = await getTopicById(topicId)
const enableTopicNaming = getStoreSetting('enableTopicNaming') const enableTopicNaming = await preferenceService.get('topic.naming.enabled')
if (isEmpty(topic.messages)) { if (isEmpty(topic.messages)) {
return return

View File

@ -1,6 +1,6 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { builtinLanguages, UNKNOWN } from '@renderer/config/translate' import { builtinLanguages, UNKNOWN } from '@renderer/config/translate'
import { useAppSelector } from '@renderer/store'
import { TranslateLanguage } from '@renderer/types' import { TranslateLanguage } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import { getTranslateOptions } from '@renderer/utils/translate' import { getTranslateOptions } from '@renderer/utils/translate'
@ -16,7 +16,7 @@ const logger = loggerService.withContext('useTranslate')
* - getLanguageByLangcode: 通过语言代码获取语言对象 * - getLanguageByLangcode: 通过语言代码获取语言对象
*/ */
export default function useTranslate() { export default function useTranslate() {
const prompt = useAppSelector((state) => state.settings.translateModelPrompt) const [prompt] = usePreference('feature.translate.model_prompt')
const [translateLanguages, setTranslateLanguages] = useState<TranslateLanguage[]>(builtinLanguages) const [translateLanguages, setTranslateLanguages] = useState<TranslateLanguage[]>(builtinLanguages)
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import KeyvStorage from '@kangfenmao/keyv-storage' import KeyvStorage from '@kangfenmao/keyv-storage'
import { loggerService } from '@logger' import { loggerService } from '@logger'
@ -5,8 +6,6 @@ import { startAutoSync } from './services/BackupService'
import { startNutstoreAutoSync } from './services/NutstoreService' import { startNutstoreAutoSync } from './services/NutstoreService'
import storeSyncService from './services/StoreSyncService' import storeSyncService from './services/StoreSyncService'
import { webTraceService } from './services/WebTraceService' import { webTraceService } from './services/WebTraceService'
import store from './store'
loggerService.initWindowSource('mainWindow') loggerService.initWindowSource('mainWindow')
function initKeyv() { function initKeyv() {
@ -15,13 +14,18 @@ function initKeyv() {
} }
function initAutoSync() { function initAutoSync() {
setTimeout(() => { setTimeout(async () => {
const { webdavAutoSync, localBackupAutoSync, s3 } = store.getState().settings const autoSyncStates = await preferenceService.getMultiple({
const { nutstoreAutoSync } = store.getState().nutstore webdav: 'data.backup.webdav.auto_sync',
if (webdavAutoSync || (s3 && s3.autoSync) || localBackupAutoSync) { local: 'data.backup.local.auto_sync',
s3: 'data.backup.s3.auto_sync',
nutstore: 'data.backup.nutstore.auto_sync'
})
if (autoSyncStates.webdav || autoSyncStates.s3 || autoSyncStates.local) {
startAutoSync() startAutoSync()
} }
if (nutstoreAutoSync) { if (autoSyncStates.nutstore) {
startNutstoreAutoSync() startNutstoreAutoSync()
} }
}, 8000) }, 8000)

View File

@ -1,9 +1,9 @@
import { MessageOutlined } from '@ant-design/icons' import { MessageOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import useScrollPosition from '@renderer/hooks/useScrollPosition' import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { getTopicById } from '@renderer/hooks/useTopic' import { getTopicById } from '@renderer/hooks/useTopic'
import { getAssistantById } from '@renderer/services/AssistantService' import { getAssistantById } from '@renderer/services/AssistantService'
@ -19,7 +19,6 @@ import { FC, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { default as MessageItem } from '../../home/Messages/Message' import { default as MessageItem } from '../../home/Messages/Message'
interface Props extends React.HTMLAttributes<HTMLDivElement> { interface Props extends React.HTMLAttributes<HTMLDivElement> {
topic?: Topic topic?: Topic
} }
@ -27,7 +26,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => { const TopicMessages: FC<Props> = ({ topic: _topic, ...props }) => {
const navigate = NavigationService.navigate! const navigate = NavigationService.navigate!
const { handleScroll, containerRef } = useScrollPosition('TopicMessages') const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
const { messageStyle } = useSettings() const [messageStyle] = usePreference('chat.message.style')
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
const [topic, setTopic] = useState<Topic | undefined>(_topic) const [topic, setTopic] = useState<Topic | undefined>(_topic)

View File

@ -1,3 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch' import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
@ -6,7 +7,6 @@ import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useNavbarPosition } from '@renderer/hooks/useNavbar' import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
@ -36,7 +36,9 @@ interface Props {
const Chat: FC<Props> = (props) => { const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id) const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle, messageNavigation } = useSettings() const [topicPosition] = usePreference('topic.position')
const [messageStyle] = usePreference('chat.message.style')
const [messageNavigation] = usePreference('chat.message.navigation_mode')
const { showTopics } = useShowTopics() const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic) const { isMultiSelectMode } = useChatContext(props.activeTopic)
const { isTopNavbar } = useNavbarPosition() const { isTopNavbar } = useNavbarPosition()
@ -178,7 +180,8 @@ const Chat: FC<Props> = (props) => {
} }
export const useChatMaxWidth = () => { export const useChatMaxWidth = () => {
const { showTopics, topicPosition } = useSettings() const [showTopics] = usePreference('topic.tab.show')
const [topicPosition] = usePreference('topic.position')
const { isLeftNavbar } = useNavbarPosition() const { isLeftNavbar } = useNavbarPosition()
const { showAssistants } = useShowAssistants() const { showAssistants } = useShowAssistants()
const showRightTopics = showTopics && topicPosition === 'right' const showRightTopics = showTopics && topicPosition === 'right'

View File

@ -1,7 +1,7 @@
import { usePreference } from '@data/hooks/usePreference'
import { ErrorBoundary } from '@renderer/components/ErrorBoundary' import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
import { useNavbarPosition } from '@renderer/hooks/useNavbar' import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { useActiveTopic } from '@renderer/hooks/useTopic'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService' import NavigationService from '@renderer/services/NavigationService'
@ -30,7 +30,9 @@ const HomePage: FC = () => {
const [activeAssistant, _setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) const [activeAssistant, _setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id, state?.topic) const { activeTopic, setActiveTopic: _setActiveTopic } = useActiveTopic(activeAssistant?.id, state?.topic)
const { showAssistants, showTopics, topicPosition } = useSettings() const [showAssistants] = usePreference('assistant.tab.show')
const [showTopics] = usePreference('topic.tab.show')
const [topicPosition] = usePreference('topic.position')
const dispatch = useDispatch() const dispatch = useDispatch()
_activeAssistant = activeAssistant _activeAssistant = activeAssistant

View File

@ -1,6 +1,6 @@
import { usePreference } from '@data/hooks/usePreference'
import { HStack, VStack } from '@renderer/components/Layout' import { HStack, VStack } from '@renderer/components/Layout'
import MaxContextCount from '@renderer/components/MaxContextCount' import MaxContextCount from '@renderer/components/MaxContextCount'
import { useSettings } from '@renderer/hooks/useSettings'
import { Divider, Popover } from 'antd' import { Divider, Popover } from 'antd'
import { ArrowUp, MenuIcon } from 'lucide-react' import { ArrowUp, MenuIcon } from 'lucide-react'
import { FC } from 'react' import { FC } from 'react'
@ -16,7 +16,7 @@ type Props = {
const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount }) => { const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { showInputEstimatedTokens } = useSettings() const [showInputEstimatedTokens] = usePreference('chat.input.show_estimated_tokens')
if (!showInputEstimatedTokens) { if (!showInputEstimatedTokens) {
return null return null

View File

@ -3,9 +3,9 @@ import 'katex/dist/contrib/copy-tex'
import 'katex/dist/contrib/mhchem' import 'katex/dist/contrib/mhchem'
import 'remark-github-blockquote-alert/alert.css' import 'remark-github-blockquote-alert/alert.css'
import { usePreference } from '@data/hooks/usePreference'
import ImageViewer from '@renderer/components/ImageViewer' import ImageViewer from '@renderer/components/ImageViewer'
import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer' import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer'
import { useSettings } from '@renderer/hooks/useSettings'
import { useSmoothStream } from '@renderer/hooks/useSmoothStream' import { useSmoothStream } from '@renderer/hooks/useSmoothStream'
import type { MainTextMessageBlock, ThinkingMessageBlock, TranslationMessageBlock } from '@renderer/types/newMessage' import type { MainTextMessageBlock, ThinkingMessageBlock, TranslationMessageBlock } from '@renderer/types/newMessage'
import { removeSvgEmptyLines } from '@renderer/utils/formats' import { removeSvgEmptyLines } from '@renderer/utils/formats'
@ -46,7 +46,8 @@ interface Props {
const Markdown: FC<Props> = ({ block, postProcess }) => { const Markdown: FC<Props> = ({ block, postProcess }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { mathEngine, mathEnableSingleDollar } = useSettings() const [mathEngine] = usePreference('chat.message.math.engine')
const [mathEnableSingleDollar] = usePreference('chat.message.math.single_dollar')
const isTrulyDone = 'status' in block && block.status === 'success' const isTrulyDone = 'status' in block && block.status === 'success'
const [displayedContent, setDisplayedContent] = useState(postProcess ? postProcess(block.content) : block.content) const [displayedContent, setDisplayedContent] = useState(postProcess ? postProcess(block.content) : block.content)

View File

@ -1,4 +1,4 @@
import { useSettings } from '@renderer/hooks/useSettings' import { usePreference } from '@data/hooks/usePreference'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
import type { RootState } from '@renderer/store' import type { RootState } from '@renderer/store'
import { selectFormattedCitationsByBlockId } from '@renderer/store/messageBlock' import { selectFormattedCitationsByBlockId } from '@renderer/store/messageBlock'
@ -21,7 +21,7 @@ interface Props {
const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions = [] }) => { const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions = [] }) => {
// Use the passed citationBlockId directly in the selector // Use the passed citationBlockId directly in the selector
const { renderInputMessageAsMarkdown } = useSettings() const [renderInputMessageAsMarkdown] = usePreference('chat.message.render_as_markdown')
const rawCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId)) const rawCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))

View File

@ -1,7 +1,7 @@
import { CheckOutlined } from '@ant-design/icons' import { CheckOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import ThinkingEffect from '@renderer/components/ThinkingEffect' import ThinkingEffect from '@renderer/components/ThinkingEffect'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue' import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, type ThinkingMessageBlock } from '@renderer/types/newMessage'
import { Collapse, message as antdMessage, Tooltip } from 'antd' import { Collapse, message as antdMessage, Tooltip } from 'antd'
@ -19,7 +19,9 @@ interface Props {
const ThinkingBlock: React.FC<Props> = ({ block }) => { const ThinkingBlock: React.FC<Props> = ({ block }) => {
const [copied, setCopied] = useTemporaryValue(false, 2000) const [copied, setCopied] = useTemporaryValue(false, 2000)
const { t } = useTranslation() const { t } = useTranslation()
const { messageFont, fontSize, thoughtAutoCollapse } = useSettings() const [messageFont] = usePreference('chat.message.font')
const [fontSize] = usePreference('chat.message.font_size')
const [thoughtAutoCollapse] = usePreference('chat.message.thought.auto_collapse')
const [activeKey, setActiveKey] = useState<'thought' | ''>(thoughtAutoCollapse ? '' : 'thought') const [activeKey, setActiveKey] = useState<'thought' | ''>(thoughtAutoCollapse ? '' : 'thought')
const isThinking = useMemo(() => block.status === MessageBlockStatus.STREAMING, [block.status]) const isThinking = useMemo(() => block.status === MessageBlockStatus.STREAMING, [block.status])

View File

@ -1,12 +1,12 @@
import '@xyflow/react/dist/style.css' import '@xyflow/react/dist/style.css'
import { RobotOutlined, UserOutlined } from '@ant-design/icons' import { RobotOutlined, UserOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar' import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { RootState } from '@renderer/store' import { RootState } from '@renderer/store'
@ -205,7 +205,7 @@ const ChatFlowHistory: FC<ChatFlowHistoryProps> = ({ conversationId }) => {
const [nodes, setNodes, onNodesChange] = useNodesState<any>([]) const [nodes, setNodes, onNodesChange] = useNodesState<any>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<any>([]) const [edges, setEdges, onEdgesChange] = useEdgesState<any>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const { userName } = useSettings() const [userName] = usePreference('app.user.name')
const { settedTheme } = useTheme() const { settedTheme } = useTheme()
const topicId = conversationId const topicId = conversationId

View File

@ -6,7 +6,7 @@ import {
VerticalAlignBottomOutlined, VerticalAlignBottomOutlined,
VerticalAlignTopOutlined VerticalAlignTopOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings' import { usePreference } from '@data/hooks/usePreference'
import { RootState } from '@renderer/store' import { RootState } from '@renderer/store'
// import { selectCurrentTopicId } from '@renderer/store/newMessage' // import { selectCurrentTopicId } from '@renderer/store/newMessage'
import { Button, Drawer, Tooltip } from 'antd' import { Button, Drawer, Tooltip } from 'antd'
@ -44,7 +44,8 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null) const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId) const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
const lastMoveTime = useRef(0) const lastMoveTime = useRef(0)
const { topicPosition, showTopics } = useSettings() const [topicPosition] = usePreference('topic.position')
const [showTopics] = usePreference('topic.tab.show')
const showRightTopics = topicPosition === 'right' && showTopics const showRightTopics = topicPosition === 'right' && showTopics
// Reset hide timer and make buttons visible // Reset hide timer and make buttons visible

View File

@ -1,3 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useMessageEditing } from '@renderer/context/MessageEditingContext' import { useMessageEditing } from '@renderer/context/MessageEditingContext'
@ -5,7 +6,6 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useModel } from '@renderer/hooks/useModel' import { useModel } from '@renderer/hooks/useModel'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageModelId } from '@renderer/services/MessagesService' import { getMessageModelId } from '@renderer/services/MessagesService'
@ -67,7 +67,12 @@ const MessageItem: FC<Props> = ({
const { assistant, setModel } = useAssistant(message.assistantId) const { assistant, setModel } = useAssistant(message.assistantId)
const { isMultiSelectMode } = useChatContext(topic) const { isMultiSelectMode } = useChatContext(topic)
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
const { messageFont, fontSize, messageStyle, showMessageOutline } = useSettings()
const [messageFont] = usePreference('chat.message.font')
const [fontSize] = usePreference('chat.message.font_size')
const [messageStyle] = usePreference('chat.message.style')
const [showMessageOutline] = usePreference('chat.message.show_outline')
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic) const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
const messageContainerRef = useRef<HTMLDivElement>(null) const messageContainerRef = useRef<HTMLDivElement>(null)
const { editingMessageId, stopEditing } = useMessageEditing() const { editingMessageId, stopEditing } = useMessageEditing()

View File

@ -1,9 +1,9 @@
import { usePreference } from '@data/hooks/usePreference'
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar' import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env' import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { getMessageModelId } from '@renderer/services/MessagesService' import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelName } from '@renderer/services/ModelService' import { getModelName } from '@renderer/services/ModelService'
@ -33,7 +33,7 @@ const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
const avatar = useAvatar() const avatar = useAvatar()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { userName } = useSettings() const [userName] = usePreference('app.user.name')
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
const messagesListRef = useRef<HTMLDivElement>(null) const messagesListRef = useRef<HTMLDivElement>(null)

View File

@ -1,9 +1,9 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import CustomTag from '@renderer/components/Tags/CustomTag' import CustomTag from '@renderer/components/Tags/CustomTag'
import TranslateButton from '@renderer/components/TranslateButton' import TranslateButton from '@renderer/components/TranslateButton'
import { isGenerateImageModel, isVisionModel } from '@renderer/config/models' import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import PasteService from '@renderer/services/PasteService' import PasteService from '@renderer/services/PasteService'
@ -45,7 +45,10 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
const [isFileDragging, setIsFileDragging] = useState(false) const [isFileDragging, setIsFileDragging] = useState(false)
const { assistant } = useAssistant(message.assistantId) const { assistant } = useAssistant(message.assistantId)
const model = assistant.model || assistant.defaultModel const model = assistant.model || assistant.defaultModel
const { pasteLongTextThreshold, fontSize, sendMessageShortcut, enableSpellCheck } = useSettings() const [pasteLongTextThreshold] = usePreference('chat.input.paste_long_text_threshold')
const [fontSize] = usePreference('chat.message.font_size')
const [sendMessageShortcut] = usePreference('chat.input.send_message_shortcut')
const [enableSpellCheck] = usePreference('app.spell_check.enabled')
const { t } = useTranslation() const { t } = useTranslation()
const textareaRef = useRef<TextAreaRef>(null) const textareaRef = useRef<TextAreaRef>(null)
const attachmentButtonRef = useRef<AttachmentButtonRef>(null) const attachmentButtonRef = useRef<AttachmentButtonRef>(null)

View File

@ -1,15 +1,15 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types' import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils'
import type { MultiModelMessageStyle } from '@shared/data/preferenceTypes'
import { Popover } from 'antd' import { Popover } from 'antd'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -30,7 +30,9 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
// Hooks // Hooks
const { editMessage } = useMessageOperations(topic) const { editMessage } = useMessageOperations(topic)
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings() const [multiModelMessageStyleSetting] = usePreference('chat.message.multi_model.style')
const [gridColumns] = usePreference('chat.message.multi_model.grid_columns')
const [gridPopoverTrigger] = usePreference('chat.message.multi_model.grid_popover_trigger')
const { isMultiSelectMode } = useChatContext(topic) const { isMultiSelectMode } = useChatContext(topic)
const maxWidth = useChatMaxWidth() const maxWidth = useChatMaxWidth()
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()

View File

@ -9,11 +9,11 @@ import {
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types' import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { AssistantMessageStatus } from '@renderer/types/newMessage' import { AssistantMessageStatus } from '@renderer/types/newMessage'
import { getMainTextContent } from '@renderer/utils/messageUtils/find' import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { MultiModelMessageStyle } from '@shared/data/preferenceTypes'
import { Button, Tooltip } from 'antd' import { Button, Tooltip } from 'antd'
import { FC, memo } from 'react' import { FC, memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@ -1,5 +1,6 @@
// import { InfoCircleOutlined } from '@ant-design/icons' // import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { useMultiplePreferences } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons' import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -16,7 +17,7 @@ import useTranslate from '@renderer/hooks/useTranslate'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle } from '@renderer/services/MessagesService' import { getMessageTitle } from '@renderer/services/MessagesService'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
import store, { RootState, useAppDispatch } from '@renderer/store' import store, { useAppDispatch } from '@renderer/store'
import { messageBlocksSelectors, removeOneBlock } from '@renderer/store/messageBlock' import { messageBlocksSelectors, removeOneBlock } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage' import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { TraceIcon } from '@renderer/trace/pages/Component' import { TraceIcon } from '@renderer/trace/pages/Component'
@ -107,7 +108,19 @@ const MessageMenubar: FC<Props> = (props) => {
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) const [exportMenuOptions] = useMultiplePreferences({
image: 'data.export.menus.image',
markdown: 'data.export.menus.markdown',
markdown_reason: 'data.export.menus.markdown_reason',
notion: 'data.export.menus.notion',
yuque: 'data.export.menus.yuque',
joplin: 'data.export.menus.joplin',
obsidian: 'data.export.menus.obsidian',
siyuan: 'data.export.menus.siyuan',
docx: 'data.export.menus.docx',
plain_text: 'data.export.menus.plain_text'
})
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
// const processedMessage = useMemo(() => { // const processedMessage = useMemo(() => {
@ -263,7 +276,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'clipboard', key: 'clipboard',
onClick: async () => { onClick: async () => {
const title = await getMessageTitle(message) const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message) const markdown = await messageToMarkdown(message)
exportMessageToNotes(title, markdown, notesPath) exportMessageToNotes(title, markdown, notesPath)
} }
} }
@ -315,7 +328,7 @@ const MessageMenubar: FC<Props> = (props) => {
label: t('chat.topics.export.word'), label: t('chat.topics.export.word'),
key: 'word', key: 'word',
onClick: async () => { onClick: async () => {
const markdown = messageToMarkdown(message) const markdown = await messageToMarkdown(message)
const title = await getMessageTitle(message) const title = await getMessageTitle(message)
window.api.export.toWord(markdown, title) window.api.export.toWord(markdown, title)
} }
@ -325,7 +338,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'notion', key: 'notion',
onClick: async () => { onClick: async () => {
const title = await getMessageTitle(message) const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message) const markdown = await messageToMarkdown(message)
exportMessageToNotion(title, markdown, message) exportMessageToNotion(title, markdown, message)
} }
}, },
@ -334,7 +347,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'yuque', key: 'yuque',
onClick: async () => { onClick: async () => {
const title = await getMessageTitle(message) const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message) const markdown = await messageToMarkdown(message)
exportMarkdownToYuque(title, markdown) exportMarkdownToYuque(title, markdown)
} }
}, },
@ -359,7 +372,7 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'siyuan', key: 'siyuan',
onClick: async () => { onClick: async () => {
const title = await getMessageTitle(message) const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message) const markdown = await messageToMarkdown(message)
exportMarkdownToSiyuan(title, markdown) exportMarkdownToSiyuan(title, markdown)
} }
} }

View File

@ -1,8 +1,8 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { CopyIcon, LoadingIcon } from '@renderer/components/Icons' import { CopyIcon, LoadingIcon } from '@renderer/components/Icons'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider' import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import type { ToolMessageBlock } from '@renderer/types/newMessage' import type { ToolMessageBlock } from '@renderer/types/newMessage'
import { isToolAutoApproved } from '@renderer/utils/mcp-tools' import { isToolAutoApproved } from '@renderer/utils/mcp-tools'
@ -49,7 +49,8 @@ const MessageTools: FC<Props> = ({ block }) => {
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({}) const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
const [countdown, setCountdown] = useState<number>(COUNTDOWN_TIME) const [countdown, setCountdown] = useState<number>(COUNTDOWN_TIME)
const { t } = useTranslation() const { t } = useTranslation()
const { messageFont, fontSize } = useSettings() const [messageFont] = usePreference('chat.message.font')
const [fontSize] = usePreference('chat.message.font_size')
const { mcpServers, updateMCPServer } = useMCPServers() const { mcpServers, updateMCPServer } = useMCPServers()
const [expandedResponse, setExpandedResponse] = useState<{ content: string; title: string } | null>(null) const [expandedResponse, setExpandedResponse] = useState<{ content: string; title: string } | null>(null)
const [progress, setProgress] = useState<number>(0) const [progress, setProgress] = useState<number>(0)

View File

@ -1,3 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import ContextMenu from '@renderer/components/ContextMenu' import ContextMenu from '@renderer/components/ContextMenu'
import { LoadingIcon } from '@renderer/components/Icons' import { LoadingIcon } from '@renderer/components/Icons'
@ -7,7 +8,6 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
import useScrollPosition from '@renderer/hooks/useScrollPosition' import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic' import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
@ -62,7 +62,8 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
const [isProcessingContext, setIsProcessingContext] = useState(false) const [isProcessingContext, setIsProcessingContext] = useState(false)
const { updateTopic, addTopic } = useAssistant(assistant.id) const { updateTopic, addTopic } = useAssistant(assistant.id)
const { showPrompt, messageNavigation } = useSettings() const [showPrompt] = usePreference('chat.message.show_prompt')
const [messageNavigation] = usePreference('chat.message.navigation_mode')
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const messages = useTopicMessages(topic.id) const messages = useTopicMessages(topic.id)

View File

@ -1,4 +1,4 @@
import { useSettings } from '@renderer/hooks/useSettings' import { usePreference } from '@data/hooks/usePreference'
import { FC, HTMLAttributes } from 'react' import { FC, HTMLAttributes } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -7,7 +7,7 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
} }
const NarrowLayout: FC<Props> = ({ children, ...props }) => { const NarrowLayout: FC<Props> = ({ children, ...props }) => {
const { narrowMode } = useSettings() const [narrowMode] = usePreference('chat.narrow_mode')
return ( return (
<Container className={`narrow-mode ${narrowMode ? 'active' : ''}`} {...props}> <Container className={`narrow-mode ${narrowMode ? 'active' : ''}`} {...props}>

View File

@ -1,4 +1,5 @@
import { usePreference } from '@data/hooks/usePreference' import { usePreference } from '@data/hooks/usePreference'
import { useMultiplePreferences } from '@data/hooks/usePreference'
import { DraggableVirtualList } from '@renderer/components/DraggableList' import { DraggableVirtualList } from '@renderer/components/DraggableList'
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons' import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -191,7 +192,18 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
[setActiveTopic] [setActiveTopic]
) )
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) const [exportMenuOptions] = useMultiplePreferences({
image: 'data.export.menus.image',
markdown: 'data.export.menus.markdown',
markdown_reason: 'data.export.menus.markdown_reason',
notion: 'data.export.menus.notion',
yuque: 'data.export.menus.yuque',
joplin: 'data.export.menus.joplin',
obsidian: 'data.export.menus.obsidian',
siyuan: 'data.export.menus.siyuan',
docx: 'data.export.menus.docx',
plain_text: 'data.export.menus.plain_text'
})
const [_targetTopic, setTargetTopic] = useState<Topic | null>(null) const [_targetTopic, setTargetTopic] = useState<Topic | null>(null)
const targetTopic = useDeferredValue(_targetTopic) const targetTopic = useDeferredValue(_targetTopic)

View File

@ -1,7 +1,7 @@
import { usePreference } from '@data/hooks/usePreference'
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup' import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useNavbarPosition } from '@renderer/hooks/useNavbar' import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore' import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
@ -39,7 +39,7 @@ const HomeTabs: FC<Props> = ({
}) => { }) => {
const { addAssistant } = useAssistants() const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic') const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings() const [topicPosition] = usePreference('topic.position')
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { toggleShowTopics } = useShowTopics() const { toggleShowTopics } = useShowTopics()
const { isLeftNavbar } = useNavbarPosition() const { isLeftNavbar } = useNavbarPosition()

View File

@ -1,6 +1,6 @@
import { SyncOutlined } from '@ant-design/icons' import { SyncOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Button } from 'antd' import { Button } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -8,7 +8,7 @@ import styled from 'styled-components'
const UpdateAppButton: FC = () => { const UpdateAppButton: FC = () => {
const { update } = useRuntime() const { update } = useRuntime()
const { autoCheckUpdate } = useSettings() const [autoCheckUpdate] = usePreference('app.dist.auto_update.enabled')
const { t } = useTranslation() const { t } = useTranslation()
if (!update) { if (!update) {

View File

@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess' import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService' import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { KnowledgeBase, PreprocessProviderId } from '@renderer/types' import { KnowledgeBase, PreprocessProviderId } from '@renderer/types'
import { Tag } from 'antd' import { Tag } from 'antd'
@ -28,7 +28,7 @@ const QuotaTag: FC<{ base: KnowledgeBase; providerId: PreprocessProviderId; quot
return return
} }
if (quota === undefined) { if (quota === undefined) {
const userId = getStoreSetting('userId') const userId = await preferenceService.get('app.user.id')
const baseParams = getKnowledgeBaseParams(base) const baseParams = getKnowledgeBaseParams(base)
try { try {
const response = await window.api.knowledgeBase.checkQuota({ const response = await window.api.knowledgeBase.checkQuota({

View File

@ -1,4 +1,5 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons' import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore' import AiProvider from '@renderer/aiCore'
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg' import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
@ -13,7 +14,6 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings' import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider' import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getProviderLabel } from '@renderer/i18n/label' import { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
@ -94,7 +94,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
const { generating } = useRuntime() const { generating } = useRuntime()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { autoTranslateWithSpace } = useSettings() const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const spaceClickTimer = useRef<NodeJS.Timeout>(null) const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const aihubmixProvider = providers.find((p) => p.id === 'aihubmix')! const aihubmixProvider = providers.find((p) => p.id === 'aihubmix')!

View File

@ -1,4 +1,5 @@
import { PlusOutlined } from '@ant-design/icons' import { PlusOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore' import AiProvider from '@renderer/aiCore'
import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg' import IcImageUp from '@renderer/assets/images/paintings/ic_ImageUp.svg'
@ -12,7 +13,6 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings' import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider' import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { import {
getPaintingsBackgroundOptionsLabel, getPaintingsBackgroundOptionsLabel,
getPaintingsImageSizeOptionsLabel, getPaintingsImageSizeOptionsLabel,
@ -85,7 +85,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
const { generating } = useRuntime() const { generating } = useRuntime()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { autoTranslateWithSpace } = useSettings() const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const spaceClickTimer = useRef<NodeJS.Timeout>(null) const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const newApiProvider = providers.find((p) => p.id === 'new-api')! const newApiProvider = providers.find((p) => p.id === 'new-api')!

View File

@ -1,4 +1,5 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons' import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import AiProvider from '@renderer/aiCore' import AiProvider from '@renderer/aiCore'
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg' import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
@ -17,7 +18,6 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings' import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider' import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getProviderLabel } from '@renderer/i18n/label' import { getProviderLabel } from '@renderer/i18n/label'
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
@ -303,7 +303,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
setCurrentImageIndex(0) setCurrentImageIndex(0)
} }
const { autoTranslateWithSpace } = useSettings() const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const [spaceClickCount, setSpaceClickCount] = useState(0) const [spaceClickCount, setSpaceClickCount] = useState(0)
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const spaceClickTimer = useRef<NodeJS.Timeout>(null) const spaceClickTimer = useRef<NodeJS.Timeout>(null)

View File

@ -1,4 +1,5 @@
import { PlusOutlined } from '@ant-design/icons' import { PlusOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
@ -9,7 +10,6 @@ import { LanguagesEnum } from '@renderer/config/translate'
import { usePaintings } from '@renderer/hooks/usePaintings' import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider' import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { getProviderLabel } from '@renderer/i18n/label' import { getProviderLabel } from '@renderer/i18n/label'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
@ -74,7 +74,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
const { generating } = useRuntime() const { generating } = useRuntime()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { autoTranslateWithSpace } = useSettings() const [autoTranslateWithSpace] = usePreference('chat.input.translate.auto_translate_with_space')
const spaceClickTimer = useRef<NodeJS.Timeout>(null) const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const tokenfluxProvider = providers.find((p) => p.id === 'tokenflux')! const tokenfluxProvider = providers.find((p) => p.id === 'tokenflux')!
const textareaRef = useRef<any>(null) const textareaRef = useRef<any>(null)

View File

@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import store from '@renderer/store'
import { WebSearchState } from '@renderer/store/websearch' import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchProviderResponse, WebSearchProviderResult } from '@renderer/types' import { WebSearchProvider, WebSearchProviderResponse, WebSearchProviderResult } from '@renderer/types'
import { createAbortPromise } from '@renderer/utils/abortController' import { createAbortPromise } from '@renderer/utils/abortController'
@ -30,7 +30,7 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
httpOptions?: RequestInit httpOptions?: RequestInit
): Promise<WebSearchProviderResponse> { ): Promise<WebSearchProviderResponse> {
const uid = nanoid() const uid = nanoid()
const language = store.getState().settings.language const language = await preferenceService.get('app.language')
try { try {
if (!query.trim()) { if (!query.trim()) {
throw new Error('Search query cannot be empty') throw new Error('Search query cannot be empty')

View File

@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import db from '@renderer/databases' import db from '@renderer/databases'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService' import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { NotificationService } from '@renderer/services/NotificationService' import { NotificationService } from '@renderer/services/NotificationService'
import store from '@renderer/store' import store from '@renderer/store'
@ -96,7 +96,7 @@ class KnowledgeQueue {
private async processItem(baseId: string, item: KnowledgeItem): Promise<void> { private async processItem(baseId: string, item: KnowledgeItem): Promise<void> {
const notificationService = NotificationService.getInstance() const notificationService = NotificationService.getInstance()
const userId = getStoreSetting('userId') const userId = await preferenceService.get('app.user.id')
try { try {
if (item.retryCount && item.retryCount >= this.MAX_RETRIES) { if (item.retryCount && item.retryCount >= this.MAX_RETRIES) {
logger.info(`Item ${item.id} has reached max retries, skipping`) logger.info(`Item ${item.id} has reached max retries, skipping`)

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { CompletionsParams } from '@renderer/aiCore/middleware/schemas' import { CompletionsParams } from '@renderer/aiCore/middleware/schemas'
import { SYSTEM_PROMPT_THRESHOLD } from '@renderer/config/constant' import { SYSTEM_PROMPT_THRESHOLD } from '@renderer/config/constant'
@ -12,7 +13,6 @@ import {
isWebSearchModel isWebSearchModel
} from '@renderer/config/models' } from '@renderer/config/models'
import { getModel } from '@renderer/hooks/useModel' import { getModel } from '@renderer/hooks/useModel'
import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { currentSpan, withSpanResult } from '@renderer/services/SpanManagerService' import { currentSpan, withSpanResult } from '@renderer/services/SpanManagerService'
import store from '@renderer/store' import store from '@renderer/store'
@ -680,7 +680,7 @@ export async function fetchLanguageDetection({ text, onResponse }: FetchLanguage
} }
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) { export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
let prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title') let prompt = (await preferenceService.get('topic.naming_prompt')) || i18n.t('prompts.title')
const model = getQuickModel() || assistant.model || getDefaultModel() const model = getQuickModel() || assistant.model || getDefaultModel()
if (prompt && containsSupportedVariables(prompt)) { if (prompt && containsSupportedVariables(prompt)) {

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { import {
DEFAULT_CONTEXTCOUNT, DEFAULT_CONTEXTCOUNT,
@ -52,7 +53,10 @@ export function getDefaultAssistant(): Assistant {
} }
} }
export function getDefaultTranslateAssistant(targetLanguage: TranslateLanguage, text: string): TranslateAssistant { export async function getDefaultTranslateAssistant(
targetLanguage: TranslateLanguage,
text: string
): Promise<TranslateAssistant> {
const model = getTranslateModel() const model = getTranslateModel()
const assistant: Assistant = getDefaultAssistant() const assistant: Assistant = getDefaultAssistant()
@ -77,10 +81,8 @@ export function getDefaultTranslateAssistant(targetLanguage: TranslateLanguage,
prompt = '' prompt = ''
} else { } else {
content = 'follow system instruction' content = 'follow system instruction'
prompt = store const translateModelPrompt = await preferenceService.get('feature.translate.model_prompt')
.getState() prompt = translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value).replaceAll('{{text}}', text)
.settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value)
.replaceAll('{{text}}', text)
} }
const translateAssistant = { const translateAssistant = {

View File

@ -1,3 +1,7 @@
//TODO Data Refactor
// The code is messy, need to refactor all the backup related code
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import db from '@renderer/databases' import db from '@renderer/databases'
import { upgradeToV7, upgradeToV8 } from '@renderer/databases/upgrades' import { upgradeToV7, upgradeToV8 } from '@renderer/databases/upgrades'
@ -164,7 +168,16 @@ export async function backupToWebdav({
webdavMaxBackups, webdavMaxBackups,
webdavSkipBackupFile, webdavSkipBackupFile,
webdavDisableStream webdavDisableStream
} = store.getState().settings } = await preferenceService.getMultiple({
webdavHost: 'data.backup.webdav.host',
webdavUser: 'data.backup.webdav.user',
webdavPass: 'data.backup.webdav.pass',
webdavPath: 'data.backup.webdav.path',
webdavMaxBackups: 'data.backup.webdav.max_backups',
webdavSkipBackupFile: 'data.backup.webdav.skip_backup_file',
webdavDisableStream: 'data.backup.webdav.disable_stream'
})
let deviceType = 'unknown' let deviceType = 'unknown'
let hostname = 'unknown' let hostname = 'unknown'
try { try {
@ -294,7 +307,12 @@ export async function backupToWebdav({
// 从 webdav 恢复 // 从 webdav 恢复
export async function restoreFromWebdav(fileName?: string) { export async function restoreFromWebdav(fileName?: string) {
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings const { webdavHost, webdavUser, webdavPass, webdavPath } = await preferenceService.getMultiple({
webdavHost: 'data.backup.webdav.host',
webdavUser: 'data.backup.webdav.user',
webdavPass: 'data.backup.webdav.pass',
webdavPath: 'data.backup.webdav.path'
})
let data = '' let data = ''
try { try {
@ -334,7 +352,18 @@ export async function backupToS3({
store.dispatch(setS3SyncState({ syncing: true, lastSyncError: null })) store.dispatch(setS3SyncState({ syncing: true, lastSyncError: null }))
const s3Config = store.getState().settings.s3 const s3Config = await preferenceService.getMultiple({
autoSync: 'data.backup.s3.auto_sync',
accessKeyId: 'data.backup.s3.access_key_id',
secretAccessKey: 'data.backup.s3.secret_access_key',
endpoint: 'data.backup.s3.endpoint',
bucket: 'data.backup.s3.bucket',
region: 'data.backup.s3.region',
root: 'data.backup.s3.root',
maxBackups: 'data.backup.s3.max_backups',
skipBackupFile: 'data.backup.s3.skip_backup_file',
syncInterval: 'data.backup.s3.sync_interval'
})
let deviceType = 'unknown' let deviceType = 'unknown'
let hostname = 'unknown' let hostname = 'unknown'
try { try {
@ -445,7 +474,18 @@ export async function backupToS3({
// 从 S3 恢复 // 从 S3 恢复
export async function restoreFromS3(fileName?: string) { export async function restoreFromS3(fileName?: string) {
const s3Config = store.getState().settings.s3 const s3Config = await preferenceService.getMultiple({
autoSync: 'data.backup.s3.auto_sync',
accessKeyId: 'data.backup.s3.access_key_id',
secretAccessKey: 'data.backup.s3.secret_access_key',
endpoint: 'data.backup.s3.endpoint',
bucket: 'data.backup.s3.bucket',
region: 'data.backup.s3.region',
root: 'data.backup.s3.root',
maxBackups: 'data.backup.s3.max_backups',
skipBackupFile: 'data.backup.s3.skip_backup_file',
syncInterval: 'data.backup.s3.sync_interval'
})
if (!fileName) { if (!fileName) {
const files = await window.api.backup.listS3Files(s3Config) const files = await window.api.backup.listS3Files(s3Config)
@ -481,12 +521,22 @@ let isLocalAutoBackupRunning = false
type BackupType = 'webdav' | 's3' | 'local' type BackupType = 'webdav' | 's3' | 'local'
export function startAutoSync(immediate = false, type?: BackupType) { export async function startAutoSync(immediate = false, type?: BackupType) {
// 如果没有指定类型,启动所有配置的自动同步 // 如果没有指定类型,启动所有配置的自动同步
if (!type) { if (!type) {
const settings = store.getState().settings const { webdavAutoSync, webdavHost, localBackupAutoSync, localBackupDir } = await preferenceService.getMultiple({
const { webdavAutoSync, webdavHost, localBackupAutoSync, localBackupDir } = settings webdavAutoSync: 'data.backup.webdav.auto_sync',
const s3Settings = settings.s3 webdavHost: 'data.backup.webdav.host',
localBackupAutoSync: 'data.backup.local.auto_sync',
localBackupDir: 'data.backup.local.dir'
})
const s3Settings = await preferenceService.getMultiple({
autoSync: 'data.backup.s3.auto_sync',
endpoint: 'data.backup.s3.endpoint',
bucket: 'data.backup.s3.bucket',
region: 'data.backup.s3.region',
root: 'data.backup.s3.root'
})
if (webdavAutoSync && webdavHost) { if (webdavAutoSync && webdavHost) {
startAutoSync(immediate, 'webdav') startAutoSync(immediate, 'webdav')
@ -506,8 +556,10 @@ export function startAutoSync(immediate = false, type?: BackupType) {
return return
} }
const settings = store.getState().settings const { webdavAutoSync, webdavHost } = await preferenceService.getMultiple({
const { webdavAutoSync, webdavHost } = settings webdavAutoSync: 'data.backup.webdav.auto_sync',
webdavHost: 'data.backup.webdav.host'
})
if (!webdavAutoSync || !webdavHost) { if (!webdavAutoSync || !webdavHost) {
logger.info('[WebdavAutoSync] Invalid sync settings, auto sync disabled') logger.info('[WebdavAutoSync] Invalid sync settings, auto sync disabled')
@ -522,8 +574,10 @@ export function startAutoSync(immediate = false, type?: BackupType) {
return return
} }
const settings = store.getState().settings const s3Settings = await preferenceService.getMultiple({
const s3Settings = settings.s3 autoSync: 'data.backup.s3.auto_sync',
endpoint: 'data.backup.s3.endpoint'
})
if (!s3Settings?.autoSync || !s3Settings?.endpoint) { if (!s3Settings?.autoSync || !s3Settings?.endpoint) {
logger.verbose('Invalid sync settings, auto sync disabled') logger.verbose('Invalid sync settings, auto sync disabled')
@ -538,8 +592,10 @@ export function startAutoSync(immediate = false, type?: BackupType) {
return return
} }
const settings = store.getState().settings const { localBackupAutoSync, localBackupDir } = await preferenceService.getMultiple({
const { localBackupAutoSync, localBackupDir } = settings localBackupAutoSync: 'data.backup.local.auto_sync',
localBackupDir: 'data.backup.local.dir'
})
if (!localBackupAutoSync || !localBackupDir) { if (!localBackupAutoSync || !localBackupDir) {
logger.verbose('Invalid sync settings, auto sync disabled') logger.verbose('Invalid sync settings, auto sync disabled')
@ -551,13 +607,15 @@ export function startAutoSync(immediate = false, type?: BackupType) {
scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime', 'local') scheduleNextBackup(immediate ? 'immediate' : 'fromLastSyncTime', 'local')
} }
function scheduleNextBackup(scheduleType: 'immediate' | 'fromLastSyncTime' | 'fromNow', backupType: BackupType) { async function scheduleNextBackup(
scheduleType: 'immediate' | 'fromLastSyncTime' | 'fromNow',
backupType: BackupType
) {
let syncInterval: number let syncInterval: number
let lastSyncTime: number | undefined let lastSyncTime: number | undefined
let logPrefix: string let logPrefix: string
// 根据备份类型获取相应的配置和状态 // 根据备份类型获取相应的配置和状态
const settings = store.getState().settings
const backup = store.getState().backup const backup = store.getState().backup
if (backupType === 'webdav') { if (backupType === 'webdav') {
@ -565,7 +623,7 @@ export function startAutoSync(immediate = false, type?: BackupType) {
clearTimeout(webdavSyncTimeout) clearTimeout(webdavSyncTimeout)
webdavSyncTimeout = null webdavSyncTimeout = null
} }
syncInterval = settings.webdavSyncInterval syncInterval = await preferenceService.get('data.backup.webdav.sync_interval')
lastSyncTime = backup.webdavSync?.lastSyncTime || undefined lastSyncTime = backup.webdavSync?.lastSyncTime || undefined
logPrefix = '[WebdavAutoSync]' logPrefix = '[WebdavAutoSync]'
} else if (backupType === 's3') { } else if (backupType === 's3') {
@ -573,7 +631,7 @@ export function startAutoSync(immediate = false, type?: BackupType) {
clearTimeout(s3SyncTimeout) clearTimeout(s3SyncTimeout)
s3SyncTimeout = null s3SyncTimeout = null
} }
syncInterval = settings.s3?.syncInterval || 0 syncInterval = await preferenceService.get('data.backup.s3.sync_interval')
lastSyncTime = backup.s3Sync?.lastSyncTime || undefined lastSyncTime = backup.s3Sync?.lastSyncTime || undefined
logPrefix = '[S3AutoSync]' logPrefix = '[S3AutoSync]'
} else if (backupType === 'local') { } else if (backupType === 'local') {
@ -581,7 +639,7 @@ export function startAutoSync(immediate = false, type?: BackupType) {
clearTimeout(localSyncTimeout) clearTimeout(localSyncTimeout)
localSyncTimeout = null localSyncTimeout = null
} }
syncInterval = settings.localBackupSyncInterval syncInterval = await preferenceService.get('data.backup.local.sync_interval')
lastSyncTime = backup.localBackupSync?.lastSyncTime || undefined lastSyncTime = backup.localBackupSync?.lastSyncTime || undefined
logPrefix = '[LocalAutoSync]' logPrefix = '[LocalAutoSync]'
} else { } else {
@ -921,11 +979,12 @@ export async function backupToLocal({
store.dispatch(setLocalBackupSyncState({ syncing: true, lastSyncError: null })) store.dispatch(setLocalBackupSyncState({ syncing: true, lastSyncError: null }))
const { const { localBackupDirSetting, localBackupMaxBackups, localBackupSkipBackupFile } =
localBackupDir: localBackupDirSetting, await preferenceService.getMultiple({
localBackupMaxBackups, localBackupDirSetting: 'data.backup.local.dir',
localBackupSkipBackupFile localBackupMaxBackups: 'data.backup.local.max_backups',
} = store.getState().settings localBackupSkipBackupFile: 'data.backup.local.skip_backup_file'
})
const localBackupDir = await window.api.resolvePath(localBackupDirSetting) const localBackupDir = await window.api.resolvePath(localBackupDirSetting)
let deviceType = 'unknown' let deviceType = 'unknown'
let hostname = 'unknown' let hostname = 'unknown'
@ -1049,7 +1108,7 @@ export async function backupToLocal({
export async function restoreFromLocal(fileName: string) { export async function restoreFromLocal(fileName: string) {
try { try {
const { localBackupDir: localBackupDirSetting } = store.getState().settings const localBackupDirSetting = await preferenceService.get('data.backup.local.dir')
const localBackupDir = await window.api.resolvePath(localBackupDirSetting) const localBackupDir = await window.api.resolvePath(localBackupDirSetting)
const restoreData = await window.api.backup.restoreFromLocalBackup(fileName, localBackupDir) const restoreData = await window.api.backup.restoreFromLocalBackup(fileName, localBackupDir)
const data = JSON.parse(restoreData) const data = JSON.parse(restoreData)

View File

@ -1,5 +1,4 @@
import store from '@renderer/store' import { preferenceService } from '@data/PreferenceService'
import { initialState as defaultNotificationSettings } from '@renderer/store/settings'
import type { Notification } from '@renderer/types/notification' import type { Notification } from '@renderer/types/notification'
import { NotificationQueue } from '../queue/NotificationQueue' import { NotificationQueue } from '../queue/NotificationQueue'
@ -25,7 +24,11 @@ export class NotificationService {
* @param notification * @param notification
*/ */
public async send(notification: Notification): Promise<void> { public async send(notification: Notification): Promise<void> {
const notificationSettings = store.getState().settings.notification || defaultNotificationSettings const notificationSettings = await preferenceService.getMultiple({
assistant: 'app.notification.assistant.enabled',
backup: 'app.notification.backup.enabled',
knowledge: 'app.notification.knowledge.enabled'
})
if (notificationSettings[notification.source]) { if (notificationSettings[notification.source]) {
this.queue.add(notification) this.queue.add(notification)

View File

@ -87,7 +87,7 @@ export const translateText = async (
abortKey?: string abortKey?: string
) => { ) => {
try { try {
const assistant = getDefaultTranslateAssistant(targetLanguage, text) const assistant = await getDefaultTranslateAssistant(targetLanguage, text)
const translatedText = await fetchTranslate({ assistant, onResponse, abortKey }) const translatedText = await fetchTranslate({ assistant, onResponse, abortKey })

View File

@ -16,7 +16,7 @@ import llm from './llm'
import mcp from './mcp' import mcp from './mcp'
import memory from './memory' import memory from './memory'
import messageBlocksReducer from './messageBlock' import messageBlocksReducer from './messageBlock'
import migrate from './migrate' // import migrate from './migrate'
import minapps from './minapps' import minapps from './minapps'
import newMessagesReducer from './newMessage' import newMessagesReducer from './newMessage'
import { setNotesPath } from './note' import { setNotesPath } from './note'
@ -68,8 +68,8 @@ const persistedReducer = persistReducer(
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 144, version: 144,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs']
migrate // migrate
}, },
rootReducer rootReducer
) )

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -257,21 +257,21 @@ describe('export', () => {
mockedMessages = [userMsg, assistantMsg] mockedMessages = [userMsg, assistantMsg]
}) })
it('should handle empty content in message blocks', () => { it('should handle empty content in message blocks', async () => {
const msgWithEmptyContent = createMessage({ role: 'user', id: 'empty_block' }, [ const msgWithEmptyContent = createMessage({ role: 'user', id: 'empty_block' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '' } { type: MessageBlockType.MAIN_TEXT, content: '' }
]) ])
const markdown = messageToMarkdown(msgWithEmptyContent) const markdown = await messageToMarkdown(msgWithEmptyContent)
expect(markdown).toContain('## 🧑‍💻 User') expect(markdown).toContain('## 🧑‍💻 User')
// Should handle empty content gracefully // Should handle empty content gracefully
expect(markdown).toBeDefined() expect(markdown).toBeDefined()
expect(markdown.split('\n\n').filter((s) => s.trim()).length).toBeGreaterThanOrEqual(1) expect(markdown.split('\n\n').filter((s) => s.trim()).length).toBeGreaterThanOrEqual(1)
}) })
it('should format user message using main text block', () => { it('should format user message using main text block', async () => {
const msg = mockedMessages.find((m) => m.id === 'u1') const msg = mockedMessages.find((m) => m.id === 'u1')
expect(msg).toBeDefined() expect(msg).toBeDefined()
const markdown = messageToMarkdown(msg!) const markdown = await messageToMarkdown(msg!)
expect(markdown).toContain('## 🧑‍💻 User') expect(markdown).toContain('## 🧑‍💻 User')
expect(markdown).toContain('hello user') expect(markdown).toContain('hello user')
@ -281,10 +281,10 @@ describe('export', () => {
expect(sections.length).toBeGreaterThanOrEqual(2) // title section and content section expect(sections.length).toBeGreaterThanOrEqual(2) // title section and content section
}) })
it('should format assistant message using main text block', () => { it('should format assistant message using main text block', async () => {
const msg = mockedMessages.find((m) => m.id === 'a1') const msg = mockedMessages.find((m) => m.id === 'a1')
expect(msg).toBeDefined() expect(msg).toBeDefined()
const markdown = messageToMarkdown(msg!) const markdown = await messageToMarkdown(msg!)
expect(markdown).toContain('## 🤖 Assistant') expect(markdown).toContain('## 🤖 Assistant')
expect(markdown).toContain('hi assistant') expect(markdown).toContain('hi assistant')
@ -294,21 +294,21 @@ describe('export', () => {
expect(sections.length).toBeGreaterThanOrEqual(2) // title section and content section expect(sections.length).toBeGreaterThanOrEqual(2) // title section and content section
}) })
it('should handle message with no main text block gracefully', () => { it('should handle message with no main text block gracefully', async () => {
const msg = createMessage({ role: 'user', id: 'u2' }, []) const msg = createMessage({ role: 'user', id: 'u2' }, [])
mockedMessages.push(msg) mockedMessages.push(msg)
const markdown = messageToMarkdown(msg) const markdown = await messageToMarkdown(msg)
expect(markdown).toContain('## 🧑‍💻 User') expect(markdown).toContain('## 🧑‍💻 User')
// Check that it doesn't fail when no content exists // Check that it doesn't fail when no content exists
expect(markdown).toBeDefined() expect(markdown).toBeDefined()
}) })
it('should include citation content when citation blocks exist', () => { it('should include citation content when citation blocks exist', async () => {
const msgWithCitation = createMessage({ role: 'assistant', id: 'a_cite' }, [ const msgWithCitation = createMessage({ role: 'assistant', id: 'a_cite' }, [
{ type: MessageBlockType.MAIN_TEXT, content: 'Main content' }, { type: MessageBlockType.MAIN_TEXT, content: 'Main content' },
{ type: MessageBlockType.CITATION } { type: MessageBlockType.CITATION }
]) ])
const markdown = messageToMarkdown(msgWithCitation) const markdown = await messageToMarkdown(msgWithCitation)
expect(markdown).toContain('## 🤖 Assistant') expect(markdown).toContain('## 🤖 Assistant')
expect(markdown).toContain('Main content') expect(markdown).toContain('Main content')
expect(markdown).toContain('[1] [https://example1.com](Example Citation 1)') expect(markdown).toContain('[1] [https://example1.com](Example Citation 1)')
@ -337,10 +337,10 @@ describe('export', () => {
mockedMessages = [msgWithReasoning, msgWithThinkTag, msgWithoutReasoning, msgWithReasoningAndCitation] mockedMessages = [msgWithReasoning, msgWithThinkTag, msgWithoutReasoning, msgWithReasoningAndCitation]
}) })
it('should include reasoning content from thinking block in details section', () => { it('should include reasoning content from thinking block in details section', async () => {
const msg = mockedMessages.find((m) => m.id === 'a2') const msg = mockedMessages.find((m) => m.id === 'a2')
expect(msg).toBeDefined() expect(msg).toBeDefined()
const markdown = messageToMarkdownWithReasoning(msg!) const markdown = await messageToMarkdownWithReasoning(msg!)
expect(markdown).toContain('## 🤖 Assistant') expect(markdown).toContain('## 🤖 Assistant')
expect(markdown).toContain('Main Answer') expect(markdown).toContain('Main Answer')
expect(markdown).toContain('<details') expect(markdown).toContain('<details')
@ -404,9 +404,9 @@ describe('export', () => {
mockedMessages = [userMsg, assistantMsg, singleUserMsg] mockedMessages = [userMsg, assistantMsg, singleUserMsg]
}) })
it('should join multiple messages with markdown separator', () => { it('should join multiple messages with markdown separator', async () => {
const msgs = mockedMessages.filter((m) => ['u3', 'a5'].includes(m.id)) const msgs = mockedMessages.filter((m) => ['u3', 'a5'].includes(m.id))
const markdown = messagesToMarkdown(msgs) const markdown = await messagesToMarkdown(msgs)
expect(markdown).toContain('User query A') expect(markdown).toContain('User query A')
expect(markdown).toContain('Assistant response B') expect(markdown).toContain('Assistant response B')
@ -414,13 +414,13 @@ describe('export', () => {
expect(markdown.split('\n---\n').length).toBe(2) expect(markdown.split('\n---\n').length).toBe(2)
}) })
it('should handle an empty array of messages', () => { it('should handle an empty array of messages', async () => {
expect(messagesToMarkdown([])).toBe('') expect(messagesToMarkdown([])).toBe('')
}) })
it('should handle a single message without separator', () => { it('should handle a single message without separator', async () => {
const msgs = mockedMessages.filter((m) => m.id === 'u4') const msgs = mockedMessages.filter((m) => m.id === 'u4')
const markdown = messagesToMarkdown(msgs) const markdown = await messagesToMarkdown(msgs)
expect(markdown).toContain('Single user query') expect(markdown).toContain('Single user query')
expect(markdown.split('\n\n---\n\n').length).toBe(1) expect(markdown.split('\n\n---\n\n').length).toBe(1)
}) })
@ -458,7 +458,7 @@ describe('export', () => {
const { TopicManager } = await import('@renderer/hooks/useTopic') const { TopicManager } = await import('@renderer/hooks/useTopic')
;(TopicManager.getTopicMessages as any).mockResolvedValue([userMsg, assistantMsg]) ;(TopicManager.getTopicMessages as any).mockResolvedValue([userMsg, assistantMsg])
// Specific mock for this test to check formatting // Specific mock for this test to check formatting
;(markdownToPlainText as any).mockImplementation((str: string) => str.replace(/[#*]/g, '')) ;(markdownToPlainText as any).mockImplementation(async (str: string) => str.replace(/[#*]/g, ''))
const plainText = await topicToPlainText(testTopic) const plainText = await topicToPlainText(testTopic)
@ -471,13 +471,13 @@ describe('export', () => {
}) })
describe('messageToPlainText', () => { describe('messageToPlainText', () => {
it('should convert a single message content to plain text without role prefix', () => { it('should convert a single message content to plain text without role prefix', async () => {
const testMessage = createMessage({ role: 'user', id: 'single_msg_plain' }, [ const testMessage = createMessage({ role: 'user', id: 'single_msg_plain' }, [
{ type: MessageBlockType.MAIN_TEXT, content: '### Single Message Content' } { type: MessageBlockType.MAIN_TEXT, content: '### Single Message Content' }
]) ])
;(markdownToPlainText as any).mockImplementation((str: string) => str.replace(/[#*_]/g, '')) ;(markdownToPlainText as any).mockImplementation(async (str: string) => str.replace(/[#*_]/g, ''))
const result = messageToPlainText(testMessage) const result = await messageToPlainText(testMessage)
expect(result).toBe('Single Message Content') expect(result).toBe('Single Message Content')
expect(markdownToPlainText).toHaveBeenCalledWith('### Single Message Content') expect(markdownToPlainText).toHaveBeenCalledWith('### Single Message Content')
}) })

View File

@ -1,3 +1,4 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { Client } from '@notionhq/client' import { Client } from '@notionhq/client'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
@ -159,9 +160,11 @@ export function getTitleFromString(str: string, length: number = 80): string {
return title return title
} }
const getRoleText = (role: string, modelName?: string, providerId?: string): string => { const getRoleText = async (role: string, modelName?: string, providerId?: string): Promise<string> => {
const { showModelNameInMarkdown, showModelProviderInMarkdown } = store.getState().settings const { showModelNameInMarkdown, showModelProviderInMarkdown } = await preferenceService.getMultiple({
showModelNameInMarkdown: 'data.export.markdown.show_model_name',
showModelProviderInMarkdown: 'data.export.markdown.show_model_provider'
})
if (role === 'user') { if (role === 'user') {
return '🧑‍💻 User' return '🧑‍💻 User'
} else if (role === 'system') { } else if (role === 'system') {
@ -263,13 +266,13 @@ const formatCitationsAsFootnotes = (citations: string): string => {
return footnotes.join('\n\n') return footnotes.join('\n\n')
} }
const createBaseMarkdown = ( const createBaseMarkdown = async (
message: Message, message: Message,
includeReasoning: boolean = false, includeReasoning: boolean = false,
excludeCitations: boolean = false, excludeCitations: boolean = false,
normalizeCitations: boolean = true normalizeCitations: boolean = true
): { titleSection: string; reasoningSection: string; contentSection: string; citation: string } => { ): Promise<{ titleSection: string; reasoningSection: string; contentSection: string; citation: string }> => {
const { forceDollarMathInMarkdown } = store.getState().settings const forceDollarMathInMarkdown = await preferenceService.get('data.export.markdown.force_dollar_math')
const roleText = getRoleText(message.role, message.model?.name, message.model?.provider) const roleText = getRoleText(message.role, message.model?.name, message.model?.provider)
const titleSection = `## ${roleText}` const titleSection = `## ${roleText}`
let reasoningSection = '' let reasoningSection = ''
@ -313,10 +316,13 @@ const createBaseMarkdown = (
return { titleSection, reasoningSection, contentSection: processedContent, citation } return { titleSection, reasoningSection, contentSection: processedContent, citation }
} }
export const messageToMarkdown = (message: Message, excludeCitations?: boolean): string => { export const messageToMarkdown = async (message: Message, excludeCitations?: boolean): Promise<string> => {
const { excludeCitationsInExport, standardizeCitationsInExport } = store.getState().settings const { excludeCitationsInExport, standardizeCitationsInExport } = await preferenceService.getMultiple({
excludeCitationsInExport: 'data.export.markdown.exclude_citations',
standardizeCitationsInExport: 'data.export.markdown.standardize_citations'
})
const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport
const { titleSection, contentSection, citation } = createBaseMarkdown( const { titleSection, contentSection, citation } = await createBaseMarkdown(
message, message,
false, false,
shouldExcludeCitations, shouldExcludeCitations,
@ -325,10 +331,13 @@ export const messageToMarkdown = (message: Message, excludeCitations?: boolean):
return [titleSection, '', contentSection, citation].join('\n') return [titleSection, '', contentSection, citation].join('\n')
} }
export const messageToMarkdownWithReasoning = (message: Message, excludeCitations?: boolean): string => { export const messageToMarkdownWithReasoning = async (message: Message, excludeCitations?: boolean): Promise<string> => {
const { excludeCitationsInExport, standardizeCitationsInExport } = store.getState().settings const { excludeCitationsInExport, standardizeCitationsInExport } = await preferenceService.getMultiple({
excludeCitationsInExport: 'data.export.markdown.exclude_citations',
standardizeCitationsInExport: 'data.export.markdown.standardize_citations'
})
const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport const shouldExcludeCitations = excludeCitations ?? excludeCitationsInExport
const { titleSection, reasoningSection, contentSection, citation } = createBaseMarkdown( const { titleSection, reasoningSection, contentSection, citation } = await createBaseMarkdown(
message, message,
true, true,
shouldExcludeCitations, shouldExcludeCitations,
@ -337,18 +346,14 @@ export const messageToMarkdownWithReasoning = (message: Message, excludeCitation
return [titleSection, '', reasoningSection, contentSection, citation].join('\n') return [titleSection, '', reasoningSection, contentSection, citation].join('\n')
} }
export const messagesToMarkdown = ( export const messagesToMarkdown = async (
messages: Message[], messages: Message[],
exportReasoning?: boolean, exportReasoning?: boolean,
excludeCitations?: boolean excludeCitations?: boolean
): string => { ): Promise<string> => {
return messages const converter = exportReasoning ? messageToMarkdownWithReasoning : messageToMarkdown
.map((message) => const markdowns = await Promise.all(messages.map((message) => converter(message, excludeCitations)))
exportReasoning return markdowns.join('\n---\n')
? messageToMarkdownWithReasoning(message, excludeCitations)
: messageToMarkdown(message, excludeCitations)
)
.join('\n---\n')
} }
const formatMessageAsPlainText = (message: Message): string => { const formatMessageAsPlainText = (message: Message): string => {
@ -377,7 +382,7 @@ export const topicToMarkdown = async (
const messages = await fetchTopicMessages(topic.id) const messages = await fetchTopicMessages(topic.id)
if (messages && messages.length > 0) { if (messages && messages.length > 0) {
return topicName + '\n\n' + messagesToMarkdown(messages, exportReasoning, excludeCitations) return topicName + '\n\n' + (await messagesToMarkdown(messages, exportReasoning, excludeCitations))
} }
return topicName return topicName
@ -407,7 +412,7 @@ export const exportTopicAsMarkdown = async (
setExportingState(true) setExportingState(true)
const { markdownExportPath } = store.getState().settings const markdownExportPath = await preferenceService.get('data.export.markdown.path')
if (!markdownExportPath) { if (!markdownExportPath) {
try { try {
const fileName = removeSpecialCharactersForFileName(topic.name) + '.md' const fileName = removeSpecialCharactersForFileName(topic.name) + '.md'
@ -453,14 +458,14 @@ export const exportMessageAsMarkdown = async (
setExportingState(true) setExportingState(true)
const { markdownExportPath } = store.getState().settings const markdownExportPath = await preferenceService.get('data.export.markdown.path')
if (!markdownExportPath) { if (!markdownExportPath) {
try { try {
const title = await getMessageTitle(message) const title = await getMessageTitle(message)
const fileName = removeSpecialCharactersForFileName(title) + '.md' const fileName = removeSpecialCharactersForFileName(title) + '.md'
const markdown = exportReasoning const markdown = exportReasoning
? messageToMarkdownWithReasoning(message, excludeCitations) ? await messageToMarkdownWithReasoning(message, excludeCitations)
: messageToMarkdown(message, excludeCitations) : await messageToMarkdown(message, excludeCitations)
const result = await window.api.file.save(fileName, markdown) const result = await window.api.file.save(fileName, markdown)
if (result) { if (result) {
window.message.success({ window.message.success({
@ -480,8 +485,8 @@ export const exportMessageAsMarkdown = async (
const title = await getMessageTitle(message) const title = await getMessageTitle(message)
const fileName = removeSpecialCharactersForFileName(title) + ` ${timestamp}.md` const fileName = removeSpecialCharactersForFileName(title) + ` ${timestamp}.md`
const markdown = exportReasoning const markdown = exportReasoning
? messageToMarkdownWithReasoning(message, excludeCitations) ? await messageToMarkdownWithReasoning(message, excludeCitations)
: messageToMarkdown(message, excludeCitations) : await messageToMarkdown(message, excludeCitations)
await window.api.file.write(markdownExportPath + '/' + fileName, markdown) await window.api.file.write(markdownExportPath + '/' + fileName, markdown)
window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' }) window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' })
} catch (error: any) { } catch (error: any) {
@ -579,7 +584,11 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise<boo
return false return false
} }
const { notionDatabaseID, notionApiKey } = store.getState().settings const { notionDatabaseID, notionApiKey, notionPageNameKey } = await preferenceService.getMultiple({
notionDatabaseID: 'data.integration.notion.database_id',
notionPageNameKey: 'data.integration.notion.page_name_key',
notionApiKey: 'data.integration.notion.api_key'
})
if (!notionApiKey || !notionDatabaseID) { if (!notionApiKey || !notionDatabaseID) {
window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' }) window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' })
return false return false
@ -609,7 +618,7 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise<boo
const response = await notion.pages.create({ const response = await notion.pages.create({
parent: { database_id: notionDatabaseID }, parent: { database_id: notionDatabaseID },
properties: { properties: {
[store.getState().settings.notionPageNameKey || 'Name']: { [notionPageNameKey || 'Name']: {
title: [{ text: { content: title } }] title: [{ text: { content: title } }]
} }
} }
@ -645,7 +654,7 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise<boo
} }
export const exportMessageToNotion = async (title: string, content: string, message?: Message): Promise<boolean> => { export const exportMessageToNotion = async (title: string, content: string, message?: Message): Promise<boolean> => {
const { notionExportReasoning } = store.getState().settings const notionExportReasoning = await preferenceService.get('data.integration.notion.export_reasoning')
const notionBlocks = await convertMarkdownToNotionBlocks(content) const notionBlocks = await convertMarkdownToNotionBlocks(content)
@ -665,7 +674,10 @@ export const exportMessageToNotion = async (title: string, content: string, mess
} }
export const exportTopicToNotion = async (topic: Topic): Promise<boolean> => { export const exportTopicToNotion = async (topic: Topic): Promise<boolean> => {
const { notionExportReasoning, excludeCitationsInExport } = store.getState().settings const { notionExportReasoning, excludeCitationsInExport } = await preferenceService.getMultiple({
notionExportReasoning: 'data.integration.notion.export_reasoning',
excludeCitationsInExport: 'data.export.markdown.exclude_citations'
})
const topicMessages = await fetchTopicMessages(topic.id) const topicMessages = await fetchTopicMessages(topic.id)
@ -677,7 +689,7 @@ export const exportTopicToNotion = async (topic: Topic): Promise<boolean> => {
for (const message of topicMessages) { for (const message of topicMessages) {
// 将单个消息转换为markdown // 将单个消息转换为markdown
const messageMarkdown = messageToMarkdown(message, excludeCitationsInExport) const messageMarkdown = await messageToMarkdown(message, excludeCitationsInExport)
const messageBlocks = await convertMarkdownToNotionBlocks(messageMarkdown) const messageBlocks = await convertMarkdownToNotionBlocks(messageMarkdown)
if (notionExportReasoning) { if (notionExportReasoning) {
@ -699,7 +711,10 @@ export const exportTopicToNotion = async (topic: Topic): Promise<boolean> => {
} }
export const exportMarkdownToYuque = async (title: string, content: string): Promise<any | null> => { export const exportMarkdownToYuque = async (title: string, content: string): Promise<any | null> => {
const { yuqueToken, yuqueRepoId } = store.getState().settings const { yuqueToken, yuqueRepoId } = await preferenceService.getMultiple({
yuqueToken: 'data.integration.yuque.token',
yuqueRepoId: 'data.integration.yuque.repo_id'
})
if (getExportState()) { if (getExportState()) {
window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'yuque-exporting' }) window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'yuque-exporting' })
@ -897,7 +912,13 @@ export const exportMarkdownToJoplin = async (
title: string, title: string,
contentOrMessages: string | Message | Message[] contentOrMessages: string | Message | Message[]
): Promise<any | null> => { ): Promise<any | null> => {
const { joplinUrl, joplinToken, joplinExportReasoning, excludeCitationsInExport } = store.getState().settings const { joplinUrl, joplinToken, joplinExportReasoning, excludeCitationsInExport } =
await preferenceService.getMultiple({
joplinUrl: 'data.integration.joplin.url',
joplinToken: 'data.integration.joplin.token',
joplinExportReasoning: 'data.integration.joplin.export_reasoning',
excludeCitationsInExport: 'data.export.markdown.exclude_citations'
})
if (getExportState()) { if (getExportState()) {
window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'joplin-exporting' }) window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'joplin-exporting' })
@ -915,12 +936,12 @@ export const exportMarkdownToJoplin = async (
if (typeof contentOrMessages === 'string') { if (typeof contentOrMessages === 'string') {
content = contentOrMessages content = contentOrMessages
} else if (Array.isArray(contentOrMessages)) { } else if (Array.isArray(contentOrMessages)) {
content = messagesToMarkdown(contentOrMessages, joplinExportReasoning, excludeCitationsInExport) content = await messagesToMarkdown(contentOrMessages, joplinExportReasoning, excludeCitationsInExport)
} else { } else {
// 单条Message // 单条Message
content = joplinExportReasoning content = joplinExportReasoning
? messageToMarkdownWithReasoning(contentOrMessages, excludeCitationsInExport) ? await messageToMarkdownWithReasoning(contentOrMessages, excludeCitationsInExport)
: messageToMarkdown(contentOrMessages, excludeCitationsInExport) : await messageToMarkdown(contentOrMessages, excludeCitationsInExport)
} }
try { try {
@ -963,7 +984,12 @@ export const exportMarkdownToJoplin = async (
* @param content * @param content
*/ */
export const exportMarkdownToSiyuan = async (title: string, content: string): Promise<void> => { export const exportMarkdownToSiyuan = async (title: string, content: string): Promise<void> => {
const { siyuanApiUrl, siyuanToken, siyuanBoxId, siyuanRootPath } = store.getState().settings const { siyuanApiUrl, siyuanToken, siyuanBoxId, siyuanRootPath } = await preferenceService.getMultiple({
siyuanApiUrl: 'data.integration.siyuan.api_url',
siyuanToken: 'data.integration.siyuan.token',
siyuanBoxId: 'data.integration.siyuan.box_id',
siyuanRootPath: 'data.integration.siyuan.root_path'
})
if (getExportState()) { if (getExportState()) {
window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'siyuan-exporting' }) window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'siyuan-exporting' })

View File

@ -1,6 +1,8 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { preferenceService } from '@renderer/data/PreferenceService'
import store from '@renderer/store' import store from '@renderer/store'
import { MCPTool } from '@renderer/types' import { MCPTool } from '@renderer/types'
import { defaultLanguage } from '@shared/config/constant'
const logger = loggerService.withContext('Utils:Prompt') const logger = loggerService.withContext('Utils:Prompt')
@ -205,7 +207,7 @@ export const replacePromptVariables = async (userSystemPrompt: string, modelName
if (userSystemPrompt.includes('{{username}}')) { if (userSystemPrompt.includes('{{username}}')) {
try { try {
const userName = store.getState().settings.userName || 'Unknown Username' const userName = (await preferenceService.get('app.user.name')) || 'Unknown Username'
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, userName) userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, userName)
} catch (error) { } catch (error) {
logger.error('Failed to get username:', error as Error) logger.error('Failed to get username:', error as Error)
@ -225,8 +227,8 @@ export const replacePromptVariables = async (userSystemPrompt: string, modelName
if (userSystemPrompt.includes('{{language}}')) { if (userSystemPrompt.includes('{{language}}')) {
try { try {
const language = store.getState().settings.language const language = await preferenceService.get('app.language')
userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language) userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, language || navigator.language || defaultLanguage)
} catch (error) { } catch (error) {
logger.error('Failed to get language:', error as Error) logger.error('Failed to get language:', error as Error)
userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, 'Unknown System Language') userSystemPrompt = userSystemPrompt.replace(/{{language}}/g, 'Unknown System Language')

View File

@ -70,7 +70,7 @@ const PreferenceHookTests: React.FC = () => {
const testBatchOperations = async () => { const testBatchOperations = async () => {
try { try {
const keys: PreferenceKeyType[] = ['ui.theme_mode', 'app.language'] const keys: PreferenceKeyType[] = ['ui.theme_mode', 'app.language']
const result = await preferenceService.getMultiple(keys) const result = await preferenceService.getMultipleRaw(keys)
message.success(`批量获取成功: ${Object.keys(result).length}`) message.success(`批量获取成功: ${Object.keys(result).length}`)
logger.debug('Batch get result:', { result }) logger.debug('Batch get result:', { result })

View File

@ -87,13 +87,15 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
if (initialized.current || !action.selectedText) return if (initialized.current || !action.selectedText) return
initialized.current = true initialized.current = true
runAsyncFunction(async () => {
// Initialize assistant // Initialize assistant
const currentAssistant = getDefaultTranslateAssistant(targetLanguage, action.selectedText) const currentAssistant = await getDefaultTranslateAssistant(targetLanguage, action.selectedText!)
assistantRef.current = currentAssistant assistantRef.current = currentAssistant
// Initialize topic // Initialize topic
topicRef.current = getDefaultTopic(currentAssistant.id) topicRef.current = getDefaultTopic(currentAssistant.id)
})
}, [action, targetLanguage, translateModelPrompt]) }, [action, targetLanguage, translateModelPrompt])
const fetchResult = useCallback(async () => { const fetchResult = useCallback(async () => {
@ -141,7 +143,7 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
} }
} }
assistantRef.current = getDefaultTranslateAssistant(translateLang, action.selectedText) assistantRef.current = await getDefaultTranslateAssistant(translateLang, action.selectedText)
processMessages(assistantRef.current, topicRef.current, action.selectedText, setAskId, onStream, onFinish, onError) processMessages(assistantRef.current, topicRef.current, action.selectedText, setAskId, onStream, onFinish, onError)
}, [action, targetLanguage, alterLanguage, scrollToBottom]) }, [action, targetLanguage, alterLanguage, scrollToBottom])