diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index d093d77e..af007f98 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -130,6 +130,10 @@ } } + pre + pre { + margin-top: 10px; + } + blockquote { margin: 1em 0; padding-left: 1em; diff --git a/src/renderer/src/components/Popups/UserPopup.tsx b/src/renderer/src/components/Popups/UserPopup.tsx index 79ee2652..64e18f96 100644 --- a/src/renderer/src/components/Popups/UserPopup.tsx +++ b/src/renderer/src/components/Popups/UserPopup.tsx @@ -1,6 +1,6 @@ import useAvatar from '@renderer/hooks/useAvatar' import { useSettings } from '@renderer/hooks/useSettings' -import LocalStorage from '@renderer/services/storage' +import ImageStorage from '@renderer/services/storage' import { useAppDispatch } from '@renderer/store' import { setAvatar } from '@renderer/store/runtime' import { setUserName } from '@renderer/store/settings' @@ -55,8 +55,8 @@ const PopupContainer: React.FC = ({ resolve }) => { try { const _file = file.originFileObj as File const compressedFile = await compressImage(_file) - await LocalStorage.storeImage('avatar', compressedFile) - dispatch(setAvatar(await LocalStorage.getImage('avatar'))) + await ImageStorage.set('avatar', compressedFile) + dispatch(setAvatar(await ImageStorage.get('avatar'))) } catch (error: any) { window.message.error(error.message) } diff --git a/src/renderer/src/databases/index.ts b/src/renderer/src/databases/index.ts index 11e01d56..006a52ce 100644 --- a/src/renderer/src/databases/index.ts +++ b/src/renderer/src/databases/index.ts @@ -1,13 +1,27 @@ -import { FileType } from '@renderer/types' +import { FileType, Topic } from '@renderer/types' import { Dexie, type EntityTable } from 'dexie' +import { populateTopics } from './populate' + // Database declaration (move this to its own module also) export const db = new Dexie('CherryStudio') as Dexie & { files: EntityTable + topics: EntityTable, 'id'> + settings: EntityTable<{ id: string; value: any }, 'id'> } db.version(1).stores({ files: 'id, name, origin_name, path, size, ext, type, created_at, count' }) +db.version(2).stores({ + files: 'id, name, origin_name, path, size, ext, type, created_at, count', + topics: '&id, messages', + settings: '&id, value' +}) + +db.on('populate', async (trans) => { + populateTopics(trans) +}) + export default db diff --git a/src/renderer/src/databases/populate.ts b/src/renderer/src/databases/populate.ts new file mode 100644 index 00000000..3f041654 --- /dev/null +++ b/src/renderer/src/databases/populate.ts @@ -0,0 +1,24 @@ +import i18n from '@renderer/i18n' +import { Transaction } from 'dexie' +import localforage from 'localforage' + +export async function populateTopics(trans: Transaction) { + const indexedKeys = await localforage.keys() + for (const key of indexedKeys) { + const value: any = await localforage.getItem(key) + if (key.startsWith('topic:')) { + await trans.db.table('topics').add({ id: value.id, messages: value.messages }) + } + if (key === 'image://avatar') { + await trans.db.table('settings').add({ id: key, value: await localforage.getItem(key) }) + } + } + + window.modal.success({ + title: i18n.t('message.upgrade.success.title'), + content: i18n.t('message.upgrade.success.content'), + okText: i18n.t('message.upgrade.success.button'), + centered: true, + onOk: () => window.api.reload() + }) +} diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 40519826..673ff3f9 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -1,9 +1,10 @@ import { isLocalAi } from '@renderer/config/env' +import db from '@renderer/databases' import i18n from '@renderer/i18n' -import LocalStorage from '@renderer/services/storage' import { useAppDispatch } from '@renderer/store' import { setAvatar } from '@renderer/store/runtime' import { runAsyncFunction } from '@renderer/utils' +import { useLiveQuery } from 'dexie-react-hooks' import { useEffect } from 'react' import { useDefaultModel } from './useAssistant' @@ -13,13 +14,11 @@ export function useAppInit() { const dispatch = useAppDispatch() const { proxyUrl, language } = useSettings() const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel() + const avatar = useLiveQuery(() => db.settings.get('image://avatar')) useEffect(() => { - runAsyncFunction(async () => { - const storedImage = await LocalStorage.getImage('avatar') - storedImage && dispatch(setAvatar(storedImage)) - }) - }, [dispatch]) + avatar?.value && dispatch(setAvatar(avatar.value)) + }, [avatar, dispatch]) useEffect(() => { runAsyncFunction(async () => { diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index e807cf04..77bea1cb 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -1,5 +1,4 @@ import { getDefaultTopic } from '@renderer/services/assistant' -import LocalStorage from '@renderer/services/storage' import { useAppDispatch, useAppSelector } from '@renderer/store' import { addAssistant, @@ -18,6 +17,8 @@ import { import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm' import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types' +import { TopicManager } from './useTopic' + export function useAssistants() { const { assistants } = useAppSelector((state) => state.assistants) const dispatch = useAppDispatch() @@ -30,7 +31,7 @@ export function useAssistants() { dispatch(removeAssistant({ id })) const assistant = assistants.find((a) => a.id === id) const topics = assistant?.topics || [] - topics.forEach(({ id }) => LocalStorage.removeTopic(id)) + topics.forEach(({ id }) => TopicManager.removeTopic(id)) } } } @@ -45,7 +46,7 @@ export function useAssistant(id: string) { model: assistant?.model ?? defaultModel, addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })), removeTopic: (topic: Topic) => { - LocalStorage.removeTopic(topic.id) + TopicManager.removeTopic(topic.id) dispatch(removeTopic({ assistantId: assistant.id, topic })) }, updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })), diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index 0b1bc5a7..8727c59c 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -1,3 +1,5 @@ +import db from '@renderer/databases' +import { deleteMessageFiles } from '@renderer/services/messages' import { Assistant, Topic } from '@renderer/types' import { find } from 'lodash' import { useEffect, useState } from 'react' @@ -25,3 +27,38 @@ export function useActiveTopic(_assistant: Assistant) { export function getTopic(assistant: Assistant, topicId: string) { return assistant?.topics.find((topic) => topic.id === topicId) } + +export class TopicManager { + static async getTopic(id: string) { + return await db.topics.get(id) + } + + static async getTopicMessages(id: string) { + const topic = await this.getTopic(id) + return topic ? topic.messages : [] + } + + static async removeTopic(id: string) { + const messages = await this.getTopicMessages(id) + + for (const message of messages) { + await deleteMessageFiles(message) + } + + db.topics.delete(id) + } + + static async clearTopicMessages(id: string) { + const topic = await this.getTopic(id) + + if (topic) { + for (const message of topic?.messages ?? []) { + await deleteMessageFiles(message) + } + + topic.messages = [] + + await db.topics.update(id, topic) + } + } +} diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 4db51f72..829f4ff8 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -57,7 +57,10 @@ const resources = { 'restore.success': 'Restored successfully', 'reset.confirm.content': 'Are you sure you want to clear all data?', 'reset.double.confirm.title': 'DATA LOST !!!', - 'reset.double.confirm.content': 'All data will be lost, do you want to continue?' + 'reset.double.confirm.content': 'All data will be lost, do you want to continue?', + 'upgrade.success.title': 'Upgrade successfully', + 'upgrade.success.content': 'Please restart the application to complete the upgrade', + 'upgrade.success.button': 'Restart' }, chat: { save: 'Save', @@ -319,7 +322,10 @@ const resources = { 'restore.success': '恢复成功', 'reset.confirm.content': '确定要重置所有数据吗?', 'reset.double.confirm.title': '数据丢失!!!', - 'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?' + 'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?', + 'upgrade.success.title': '升级成功', + 'upgrade.success.content': '重启应用以完成升级', + 'upgrade.success.button': '重启' }, chat: { save: '保存', diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 69c101ad..9bae73d5 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -1,6 +1,7 @@ +import db from '@renderer/databases' import { useAssistant } from '@renderer/hooks/useAssistant' import { useProviderByAssistant } from '@renderer/hooks/useProvider' -import { getTopic } from '@renderer/hooks/useTopic' +import { getTopic, TopicManager } from '@renderer/hooks/useTopic' import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { @@ -9,11 +10,9 @@ import { filterMessages, getContextCount } from '@renderer/services/messages' -import LocalStorage from '@renderer/services/storage' import { Assistant, Message, Model, Topic } from '@renderer/types' import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils' import { t } from 'i18next' -import localforage from 'localforage' import { last, reverse } from 'lodash' import { FC, useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' @@ -39,7 +38,7 @@ const Messages: FC = ({ assistant, topic, setActiveTopic }) => { (message: Message) => { const _messages = [...messages, message] setMessages(_messages) - localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages }) + db.topics.add({ id: topic.id, messages: _messages }) }, [messages, topic] ) @@ -60,7 +59,7 @@ const Messages: FC = ({ assistant, topic, setActiveTopic }) => { (message: Message) => { const _messages = messages.filter((m) => m.id !== message.id) setMessages(_messages) - localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages }) + db.topics.update(topic.id, { messages: _messages }) deleteMessageFiles(message) }, [messages, topic.id] @@ -94,7 +93,7 @@ const Messages: FC = ({ assistant, topic, setActiveTopic }) => { EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => { setMessages([]) updateTopic({ ...topic, messages: [] }) - LocalStorage.clearTopicMessages(topic.id) + TopicManager.clearTopicMessages(topic.id) }), EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, () => { const lastMessage = last(messages) @@ -124,7 +123,7 @@ const Messages: FC = ({ assistant, topic, setActiveTopic }) => { useEffect(() => { runAsyncFunction(async () => { - const messages = (await LocalStorage.getTopicMessages(topic.id)) || [] + const messages = (await TopicManager.getTopicMessages(topic.id)) || [] setMessages(messages) }) }, [topic.id]) diff --git a/src/renderer/src/pages/home/Topics.tsx b/src/renderer/src/pages/home/Topics.tsx index 75f3d078..2f4f641b 100644 --- a/src/renderer/src/pages/home/Topics.tsx +++ b/src/renderer/src/pages/home/Topics.tsx @@ -2,8 +2,8 @@ import { CloseOutlined, DeleteOutlined, EditOutlined, OpenAIOutlined } from '@an import DragableList from '@renderer/components/DragableList' import PromptPopup from '@renderer/components/Popups/PromptPopup' import { useAssistant } from '@renderer/hooks/useAssistant' +import { TopicManager } from '@renderer/hooks/useTopic' import { fetchMessagesSummary } from '@renderer/services/api' -import LocalStorage from '@renderer/services/storage' import { useAppSelector } from '@renderer/store' import { Assistant, Topic } from '@renderer/types' import { Dropdown, MenuProps } from 'antd' @@ -53,7 +53,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic key: 'auto-rename', icon: , async onClick() { - const messages = await LocalStorage.getTopicMessages(topic.id) + const messages = await TopicManager.getTopicMessages(topic.id) if (messages.length >= 2) { const summaryText = await fetchMessagesSummary({ messages, assistant }) if (summaryText) { diff --git a/src/renderer/src/services/storage.ts b/src/renderer/src/services/storage.ts index 54168533..d620eb5f 100644 --- a/src/renderer/src/services/storage.ts +++ b/src/renderer/src/services/storage.ts @@ -1,60 +1,27 @@ -import { Topic } from '@renderer/types' +import db from '@renderer/databases' import { convertToBase64 } from '@renderer/utils' -import localforage from 'localforage' - -import { deleteMessageFiles } from './messages' const IMAGE_PREFIX = 'image://' -export default class LocalStorage { - static async getTopic(id: string) { - return localforage.getItem(`topic:${id}`) - } - - static async getTopicMessages(id: string) { - const topic = await this.getTopic(id) - return topic ? topic.messages : [] - } - - static async removeTopic(id: string) { - const messages = await this.getTopicMessages(id) - - for (const message of messages) { - await deleteMessageFiles(message) - } - - localforage.removeItem(`topic:${id}`) - } - - static async clearTopicMessages(id: string) { - const topic = await this.getTopic(id) - - if (topic) { - for (const message of topic?.messages ?? []) { - await deleteMessageFiles(message) - } - - topic.messages = [] - await localforage.setItem(`topic:${id}`, topic) - } - } - - static async storeImage(name: string, file: File) { +export default class ImageStorage { + static async set(key: string, file: File) { + const id = IMAGE_PREFIX + key try { const base64Image = await convertToBase64(file) if (typeof base64Image === 'string') { - await localforage.setItem(IMAGE_PREFIX + name, base64Image) + if (await db.settings.get(id)) { + db.settings.update(id, { value: base64Image }) + return + } + await db.settings.add({ id, value: base64Image }) } } catch (error) { console.error('Error storing the image', error) } } - static async getImage(name: string) { - return localforage.getItem(IMAGE_PREFIX + name) - } - - static async removeImage(name: string) { - await localforage.removeItem(IMAGE_PREFIX + name) + static async get(key: string): Promise { + const id = IMAGE_PREFIX + key + return (await db.settings.get(id))?.value } } diff --git a/src/renderer/src/store/assistants.ts b/src/renderer/src/store/assistants.ts index 826b611f..78f40b8c 100644 --- a/src/renderer/src/store/assistants.ts +++ b/src/renderer/src/store/assistants.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { TopicManager } from '@renderer/hooks/useTopic' import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant' -import LocalStorage from '@renderer/services/storage' import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types' import { uniqBy } from 'lodash' @@ -91,7 +91,7 @@ const assistantsSlice = createSlice({ removeAllTopics: (state, action: PayloadAction<{ assistantId: string }>) => { state.assistants = state.assistants.map((assistant) => { if (assistant.id === action.payload.assistantId) { - assistant.topics.forEach((topic) => LocalStorage.removeTopic(topic.id)) + assistant.topics.forEach((topic) => TopicManager.removeTopic(topic.id)) return { ...assistant, topics: [getDefaultTopic()]