refactor: add topics and settings table

dexie
This commit is contained in:
kangfenmao 2024-09-16 08:53:14 +08:00
parent cee373bb6f
commit fa1f00f4f5
12 changed files with 122 additions and 71 deletions

View File

@ -130,6 +130,10 @@
} }
} }
pre + pre {
margin-top: 10px;
}
blockquote { blockquote {
margin: 1em 0; margin: 1em 0;
padding-left: 1em; padding-left: 1em;

View File

@ -1,6 +1,6 @@
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import LocalStorage from '@renderer/services/storage' import ImageStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { setUserName } from '@renderer/store/settings' import { setUserName } from '@renderer/store/settings'
@ -55,8 +55,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
try { try {
const _file = file.originFileObj as File const _file = file.originFileObj as File
const compressedFile = await compressImage(_file) const compressedFile = await compressImage(_file)
await LocalStorage.storeImage('avatar', compressedFile) await ImageStorage.set('avatar', compressedFile)
dispatch(setAvatar(await LocalStorage.getImage('avatar'))) dispatch(setAvatar(await ImageStorage.get('avatar')))
} catch (error: any) { } catch (error: any) {
window.message.error(error.message) window.message.error(error.message)
} }

View File

@ -1,13 +1,27 @@
import { FileType } from '@renderer/types' import { FileType, Topic } from '@renderer/types'
import { Dexie, type EntityTable } from 'dexie' import { Dexie, type EntityTable } from 'dexie'
import { populateTopics } from './populate'
// Database declaration (move this to its own module also) // Database declaration (move this to its own module also)
export const db = new Dexie('CherryStudio') as Dexie & { export const db = new Dexie('CherryStudio') as Dexie & {
files: EntityTable<FileType, 'id'> files: EntityTable<FileType, 'id'>
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
settings: EntityTable<{ id: string; value: any }, 'id'>
} }
db.version(1).stores({ db.version(1).stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count' 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 export default db

View File

@ -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()
})
}

View File

@ -1,9 +1,10 @@
import { isLocalAi } from '@renderer/config/env' import { isLocalAi } from '@renderer/config/env'
import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import { useLiveQuery } from 'dexie-react-hooks'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant' import { useDefaultModel } from './useAssistant'
@ -13,13 +14,11 @@ export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { proxyUrl, language } = useSettings() const { proxyUrl, language } = useSettings()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel() const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { avatar?.value && dispatch(setAvatar(avatar.value))
const storedImage = await LocalStorage.getImage('avatar') }, [avatar, dispatch])
storedImage && dispatch(setAvatar(storedImage))
})
}, [dispatch])
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {

View File

@ -1,5 +1,4 @@
import { getDefaultTopic } from '@renderer/services/assistant' import { getDefaultTopic } from '@renderer/services/assistant'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
addAssistant, addAssistant,
@ -18,6 +17,8 @@ import {
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm' import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types' import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { TopicManager } from './useTopic'
export function useAssistants() { export function useAssistants() {
const { assistants } = useAppSelector((state) => state.assistants) const { assistants } = useAppSelector((state) => state.assistants)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -30,7 +31,7 @@ export function useAssistants() {
dispatch(removeAssistant({ id })) dispatch(removeAssistant({ id }))
const assistant = assistants.find((a) => a.id === id) const assistant = assistants.find((a) => a.id === id)
const topics = assistant?.topics || [] 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, model: assistant?.model ?? defaultModel,
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })), addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => { removeTopic: (topic: Topic) => {
LocalStorage.removeTopic(topic.id) TopicManager.removeTopic(topic.id)
dispatch(removeTopic({ assistantId: assistant.id, topic })) dispatch(removeTopic({ assistantId: assistant.id, topic }))
}, },
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })), updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),

View File

@ -1,3 +1,5 @@
import db from '@renderer/databases'
import { deleteMessageFiles } from '@renderer/services/messages'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash' import { find } from 'lodash'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -25,3 +27,38 @@ export function useActiveTopic(_assistant: Assistant) {
export function getTopic(assistant: Assistant, topicId: string) { export function getTopic(assistant: Assistant, topicId: string) {
return assistant?.topics.find((topic) => topic.id === topicId) 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)
}
}
}

View File

@ -57,7 +57,10 @@ const resources = {
'restore.success': 'Restored successfully', 'restore.success': 'Restored successfully',
'reset.confirm.content': 'Are you sure you want to clear all data?', 'reset.confirm.content': 'Are you sure you want to clear all data?',
'reset.double.confirm.title': 'DATA LOST !!!', '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: { chat: {
save: 'Save', save: 'Save',
@ -319,7 +322,10 @@ const resources = {
'restore.success': '恢复成功', 'restore.success': '恢复成功',
'reset.confirm.content': '确定要重置所有数据吗?', 'reset.confirm.content': '确定要重置所有数据吗?',
'reset.double.confirm.title': '数据丢失!!!', 'reset.double.confirm.title': '数据丢失!!!',
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?' 'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?',
'upgrade.success.title': '升级成功',
'upgrade.success.content': '重启应用以完成升级',
'upgrade.success.button': '重启'
}, },
chat: { chat: {
save: '保存', save: '保存',

View File

@ -1,6 +1,7 @@
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviderByAssistant } from '@renderer/hooks/useProvider' 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 { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { import {
@ -9,11 +10,9 @@ import {
filterMessages, filterMessages,
getContextCount getContextCount
} from '@renderer/services/messages' } from '@renderer/services/messages'
import LocalStorage from '@renderer/services/storage'
import { Assistant, Message, Model, Topic } from '@renderer/types' import { Assistant, Message, Model, Topic } from '@renderer/types'
import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils' import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next' import { t } from 'i18next'
import localforage from 'localforage'
import { last, reverse } from 'lodash' import { last, reverse } from 'lodash'
import { FC, useCallback, useEffect, useRef, useState } from 'react' import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -39,7 +38,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
(message: Message) => { (message: Message) => {
const _messages = [...messages, message] const _messages = [...messages, message]
setMessages(_messages) setMessages(_messages)
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages }) db.topics.add({ id: topic.id, messages: _messages })
}, },
[messages, topic] [messages, topic]
) )
@ -60,7 +59,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
(message: Message) => { (message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id) const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages) setMessages(_messages)
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages }) db.topics.update(topic.id, { messages: _messages })
deleteMessageFiles(message) deleteMessageFiles(message)
}, },
[messages, topic.id] [messages, topic.id]
@ -94,7 +93,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => { EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
setMessages([]) setMessages([])
updateTopic({ ...topic, messages: [] }) updateTopic({ ...topic, messages: [] })
LocalStorage.clearTopicMessages(topic.id) TopicManager.clearTopicMessages(topic.id)
}), }),
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, () => { EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, () => {
const lastMessage = last(messages) const lastMessage = last(messages)
@ -124,7 +123,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
const messages = (await LocalStorage.getTopicMessages(topic.id)) || [] const messages = (await TopicManager.getTopicMessages(topic.id)) || []
setMessages(messages) setMessages(messages)
}) })
}, [topic.id]) }, [topic.id])

View File

@ -2,8 +2,8 @@ import { CloseOutlined, DeleteOutlined, EditOutlined, OpenAIOutlined } from '@an
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/api' import { fetchMessagesSummary } from '@renderer/services/api'
import LocalStorage from '@renderer/services/storage'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd' import { Dropdown, MenuProps } from 'antd'
@ -53,7 +53,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
key: 'auto-rename', key: 'auto-rename',
icon: <OpenAIOutlined />, icon: <OpenAIOutlined />,
async onClick() { async onClick() {
const messages = await LocalStorage.getTopicMessages(topic.id) const messages = await TopicManager.getTopicMessages(topic.id)
if (messages.length >= 2) { if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant }) const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) { if (summaryText) {

View File

@ -1,60 +1,27 @@
import { Topic } from '@renderer/types' import db from '@renderer/databases'
import { convertToBase64 } from '@renderer/utils' import { convertToBase64 } from '@renderer/utils'
import localforage from 'localforage'
import { deleteMessageFiles } from './messages'
const IMAGE_PREFIX = 'image://' const IMAGE_PREFIX = 'image://'
export default class LocalStorage { export default class ImageStorage {
static async getTopic(id: string) { static async set(key: string, file: File) {
return localforage.getItem<Topic>(`topic:${id}`) const id = IMAGE_PREFIX + key
}
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) {
try { try {
const base64Image = await convertToBase64(file) const base64Image = await convertToBase64(file)
if (typeof base64Image === 'string') { 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) { } catch (error) {
console.error('Error storing the image', error) console.error('Error storing the image', error)
} }
} }
static async getImage(name: string) { static async get(key: string): Promise<string> {
return localforage.getItem<string>(IMAGE_PREFIX + name) const id = IMAGE_PREFIX + key
} return (await db.settings.get(id))?.value
static async removeImage(name: string) {
await localforage.removeItem(IMAGE_PREFIX + name)
} }
} }

View File

@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { TopicManager } from '@renderer/hooks/useTopic'
import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant' import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant'
import LocalStorage from '@renderer/services/storage'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types' import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
@ -91,7 +91,7 @@ const assistantsSlice = createSlice({
removeAllTopics: (state, action: PayloadAction<{ assistantId: string }>) => { removeAllTopics: (state, action: PayloadAction<{ assistantId: string }>) => {
state.assistants = state.assistants.map((assistant) => { state.assistants = state.assistants.map((assistant) => {
if (assistant.id === action.payload.assistantId) { if (assistant.id === action.payload.assistantId) {
assistant.topics.forEach((topic) => LocalStorage.removeTopic(topic.id)) assistant.topics.forEach((topic) => TopicManager.removeTopic(topic.id))
return { return {
...assistant, ...assistant,
topics: [getDefaultTopic()] topics: [getDefaultTopic()]