From 9e2d65e6f810140d0c5772fb9a53999cade7cb09 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Sun, 10 Mar 2024 22:48:24 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E2=9C=A8=20feat:=20support=20sync=20with?= =?UTF-8?q?=20webrtc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + .../chat/(desktop)/features/SessionHeader.tsx | 6 +- src/app/chat/features/SyncStatusTag/index.tsx | 136 ++++++++++ .../settings/features/SettingList/index.tsx | 3 +- src/app/settings/sync/Sync.tsx | 49 ++++ src/app/settings/sync/layout.tsx | 9 + src/app/settings/sync/page.tsx | 18 ++ src/const/settings.ts | 6 + src/database/core/__tests__/model.test.ts | 4 +- src/database/core/db.ts | 2 +- src/database/core/index.ts | 1 + src/database/core/model.ts | 47 +++- src/database/core/sync.ts | 241 ++++++++++++++++++ src/database/models/file.ts | 2 +- src/database/models/message.ts | 10 +- src/database/models/plugin.ts | 5 +- src/database/models/session.ts | 8 +- src/database/models/sessionGroup.ts | 4 +- src/database/models/topic.ts | 7 +- src/hooks/useSyncData.ts | 43 ++++ src/layout/GlobalLayout/StoreHydration.tsx | 5 + src/locales/default/setting.ts | 16 +- src/services/global.ts | 17 ++ src/store/chat/slices/topic/action.ts | 31 ++- src/store/global/slices/common/action.ts | 67 ++++- .../global/slices/common/initialState.ts | 9 + .../slices/settings/selectors/settings.ts | 3 + src/types/settings/index.ts | 7 + src/types/sync.ts | 32 +++ src/utils/platform.ts | 13 +- 30 files changed, 764 insertions(+), 39 deletions(-) create mode 100644 src/app/chat/features/SyncStatusTag/index.tsx create mode 100644 src/app/settings/sync/Sync.tsx create mode 100644 src/app/settings/sync/layout.tsx create mode 100644 src/app/settings/sync/page.tsx create mode 100644 src/database/core/sync.ts create mode 100644 src/hooks/useSyncData.ts create mode 100644 src/types/sync.ts diff --git a/package.json b/package.json index 6d04b6f66e2c..a0fbaec9afb2 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,9 @@ "use-merge-value": "^1", "utility-types": "^3", "uuid": "^9", + "y-webrtc": "^10.3.0", "yaml": "^2", + "yjs": "^13.6.14", "zod": "^3", "zustand": "^4.5.2", "zustand-utils": "^1.3.2" diff --git a/src/app/chat/(desktop)/features/SessionHeader.tsx b/src/app/chat/(desktop)/features/SessionHeader.tsx index 2c40470f6eb4..0eec8fd2c53e 100644 --- a/src/app/chat/(desktop)/features/SessionHeader.tsx +++ b/src/app/chat/(desktop)/features/SessionHeader.tsx @@ -9,6 +9,7 @@ import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens'; import { useSessionStore } from '@/store/session'; import SessionSearchBar from '../../features/SessionSearchBar'; +import SyncStatusTag from '../../features/SyncStatusTag'; export const useStyles = createStyles(({ css, token }) => ({ logo: css` @@ -28,7 +29,10 @@ const Header = memo(() => { return ( - + + + + createSession()} diff --git a/src/app/chat/features/SyncStatusTag/index.tsx b/src/app/chat/features/SyncStatusTag/index.tsx new file mode 100644 index 000000000000..3f2f68b1e6a7 --- /dev/null +++ b/src/app/chat/features/SyncStatusTag/index.tsx @@ -0,0 +1,136 @@ +import { Avatar, Icon, Tag } from '@lobehub/ui'; +import { Tag as ATag, Badge, Button, Popover, Typography } from 'antd'; +import { useTheme } from 'antd-style'; +import isEqual from 'fast-deep-equal'; +import { + LucideCloudCog, + LucideCloudy, + LucideLaptop, + LucideRefreshCw, + LucideSmartphone, +} from 'lucide-react'; +import Link from 'next/link'; +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { useSyncEvent } from '@/hooks/useSyncData'; +import { useGlobalStore } from '@/store/global'; +import { settingsSelectors } from '@/store/global/selectors'; + +const text = { + ready: '已连接', + synced: '已同步', + syncing: '同步中', +} as const; + +const SyncStatusTag = memo(() => { + const [syncStatus, isSyncing, enableSync, channelName] = useGlobalStore((s) => [ + s.syncStatus, + s.syncStatus === 'syncing', + s.syncEnabled, + settingsSelectors.syncConfig(s).channelName, + s.setSettings, + ]); + const users = useGlobalStore((s) => s.syncAwareness, isEqual); + const refreshConnection = useGlobalStore((s) => s.refreshConnection); + const syncEvent = useSyncEvent(); + + const theme = useTheme(); + + return enableSync ? ( + + + 频道:{channelName} + + {/*
*/} + {/* {*/} + {/* setSettings({ sync: { channelName: e.target.value } });*/} + {/* }}*/} + {/* size={'small'}*/} + {/* value={channelName}*/} + {/* variant={'borderless'}*/} + {/* />*/} + {/*
*/} +
+ + {users.map((user) => ( + + + } + background={theme.purple1} + shape={'square'} + /> + + + + {user.name || user.id} + {user.current && ( + + current + + )} + + + {user.device} · {user.os} · {user.browser} + + + + ))} + +
+ } + // open + placement={'bottomLeft'} + > + } + > + {text[syncStatus]} + + + ) : ( + + 会话数据仅存储于当前使用的浏览器。如果使用不同浏览器打开时,数据不会互相同步。如你需要在多个设备间同步数据,请配置并开启云端同步。 + + + +
+ } + placement={'bottomLeft'} + > +
+ + 本地 + +
+ + ); +}); + +export default SyncStatusTag; diff --git a/src/app/settings/features/SettingList/index.tsx b/src/app/settings/features/SettingList/index.tsx index 5baed54d790d..795ff7ba2a47 100644 --- a/src/app/settings/features/SettingList/index.tsx +++ b/src/app/settings/features/SettingList/index.tsx @@ -1,4 +1,4 @@ -import { Bot, Info, Mic2, Settings2, Webhook } from 'lucide-react'; +import { Bot, Cloudy, Info, Mic2, Settings2, Webhook } from 'lucide-react'; import Link from 'next/link'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,6 +17,7 @@ const SettingList = memo(({ activeTab, mobile }) => { const items = [ { icon: Settings2, label: t('tab.common'), value: SettingsTabs.Common }, + { icon: Cloudy, label: t('tab.sync'), value: SettingsTabs.Sync }, { icon: Webhook, label: t('tab.llm'), value: SettingsTabs.LLM }, { icon: Mic2, label: t('tab.tts'), value: SettingsTabs.TTS }, { icon: Bot, label: t('tab.agent'), value: SettingsTabs.Agent }, diff --git a/src/app/settings/sync/Sync.tsx b/src/app/settings/sync/Sync.tsx new file mode 100644 index 000000000000..5964bfb8dfea --- /dev/null +++ b/src/app/settings/sync/Sync.tsx @@ -0,0 +1,49 @@ +import { SiWebrtc } from '@icons-pack/react-simple-icons'; +import { Form, type ItemGroup } from '@lobehub/ui'; +import { Form as AntForm, Input } from 'antd'; +import isEqual from 'fast-deep-equal'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { FORM_STYLE } from '@/const/layoutTokens'; +import { useGlobalStore } from '@/store/global'; +import { settingsSelectors } from '@/store/global/selectors'; + +import { useSyncSettings } from '../hooks/useSyncSettings'; + +type SettingItemGroup = ItemGroup; + +const Theme = memo(() => { + const { t } = useTranslation('setting'); + const [form] = AntForm.useForm(); + + const settings = useGlobalStore(settingsSelectors.currentSettings, isEqual); + const [setSettings] = useGlobalStore((s) => [s.setSettings]); + + useSyncSettings(form); + + const config: SettingItemGroup = { + children: [ + { + children: , + desc: t('sync.webrtc.channelName.desc'), + label: t('sync.webrtc.channelName.title'), + name: ['sync', 'channelName'], + }, + ], + icon: SiWebrtc, + title: t('sync.webrtc.title'), + }; + + return ( +
+ ); +}); + +export default Theme; diff --git a/src/app/settings/sync/layout.tsx b/src/app/settings/sync/layout.tsx new file mode 100644 index 000000000000..4693b7724472 --- /dev/null +++ b/src/app/settings/sync/layout.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react'; + +import { SettingsTabs } from '@/store/global/initialState'; + +import SettingLayout from '../layout.server'; + +export default ({ children }: PropsWithChildren) => { + return {children}; +}; diff --git a/src/app/settings/sync/page.tsx b/src/app/settings/sync/page.tsx new file mode 100644 index 000000000000..5a863a9a4f06 --- /dev/null +++ b/src/app/settings/sync/page.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import PageTitle from '@/components/PageTitle'; + +import Sync from './Sync'; + +export default memo(() => { + const { t } = useTranslation('setting'); + return ( + <> + + + + ); +}); diff --git a/src/const/settings.ts b/src/const/settings.ts index fd0a5375b1e9..59baf7226c25 100644 --- a/src/const/settings.ts +++ b/src/const/settings.ts @@ -6,6 +6,7 @@ import { GlobalDefaultAgent, GlobalLLMConfig, GlobalSettings, + GlobalSyncSettings, GlobalTTSConfig, } from '@/types/settings'; @@ -121,9 +122,14 @@ export const DEFAULT_TOOL_CONFIG = { }, }; +const DEFAULT_SYNC_CONFIG: GlobalSyncSettings = { + channelName: 'atc', +}; + export const DEFAULT_SETTINGS: GlobalSettings = { defaultAgent: DEFAULT_AGENT, languageModel: DEFAULT_LLM_CONFIG, + sync: DEFAULT_SYNC_CONFIG, tool: DEFAULT_TOOL_CONFIG, tts: DEFAULT_TTS_CONFIG, ...DEFAULT_BASE_SETTINGS, diff --git a/src/database/core/__tests__/model.test.ts b/src/database/core/__tests__/model.test.ts index b79c2a6d2a2f..75be5d261da7 100644 --- a/src/database/core/__tests__/model.test.ts +++ b/src/database/core/__tests__/model.test.ts @@ -37,7 +37,7 @@ describe('BaseModel', () => { content: 'Hello, World!', }; - const result = await baseModel['_add'](validData); + const result = await baseModel['_addWithSync'](validData); expect(result).toHaveProperty('id'); expect(console.error).not.toHaveBeenCalled(); @@ -49,7 +49,7 @@ describe('BaseModel', () => { content: 'Hello, World!', }; - await expect(baseModel['_add'](invalidData)).rejects.toThrow(TypeError); + await expect(baseModel['_addWithSync'](invalidData)).rejects.toThrow(TypeError); }); }); }); diff --git a/src/database/core/db.ts b/src/database/core/db.ts index 51d66ecfd952..7aa4a3ffe940 100644 --- a/src/database/core/db.ts +++ b/src/database/core/db.ts @@ -21,7 +21,7 @@ import { } from './schemas'; import { DBModel, LOBE_CHAT_LOCAL_DB_NAME } from './types/db'; -interface LobeDBSchemaMap { +export interface LobeDBSchemaMap { files: DB_File; messages: DB_Message; plugins: DB_Plugin; diff --git a/src/database/core/index.ts b/src/database/core/index.ts index a344f4ab2fbc..f686e56c091b 100644 --- a/src/database/core/index.ts +++ b/src/database/core/index.ts @@ -1,2 +1,3 @@ export { LocalDBInstance } from './db'; export * from './model'; +export { syncBus } from './sync'; diff --git a/src/database/core/model.ts b/src/database/core/model.ts index 42d003b44c54..22edc55b0237 100644 --- a/src/database/core/model.ts +++ b/src/database/core/model.ts @@ -5,6 +5,7 @@ import { DBBaseFieldsSchema } from '@/database/core/types/db'; import { nanoid } from '@/utils/uuid'; import { LocalDB, LocalDBInstance, LocalDBSchema } from './db'; +import { syncBus } from './sync'; export class BaseModel { protected readonly db: LocalDB; @@ -21,10 +22,14 @@ export class BaseModel( + protected async _addWithSync( data: T, id: string | number = nanoid(), primaryKey: string = 'id', @@ -51,6 +56,9 @@ export class BaseModel { + await this.updateYMapItem(item.id); + }); + await Promise.all(pools); return { added: validatedData.length, @@ -144,7 +156,7 @@ export class BaseModel) { + protected async _updateWithSync(id: string, data: Partial) { // we need to check whether the data is valid // pick data related schema from the full schema const keys = Object.keys(data); @@ -162,6 +174,37 @@ export class BaseModel { + this.yMap.delete(id); + }); + } + + protected async _clearWithSync() { + const result = await this.table.clear(); + // sync clear data to yjs data map + this.yMap.clear(); + return result; + } + + private updateYMapItem = async (id: string) => { + const newData = await this.table.get(id); + this.yMap.set(id, newData); + }; } diff --git a/src/database/core/sync.ts b/src/database/core/sync.ts new file mode 100644 index 000000000000..a4e7274fe71f --- /dev/null +++ b/src/database/core/sync.ts @@ -0,0 +1,241 @@ +import { throttle } from 'lodash-es'; +import { WebrtcProvider } from 'y-webrtc'; +import { Doc } from 'yjs'; + +import { OnSyncEvent, OnSyncStatusChange, StartDataSyncParams } from '@/types/sync'; + +import { LobeDBSchemaMap, LocalDBInstance } from './db'; + +declare global { + interface Window { + __ONLY_USE_FOR_CLEANUP_IN_DEV?: WebrtcProvider | null; + } +} + +let ydoc: Doc; + +if (typeof window !== 'undefined') { + ydoc = new Doc(); +} + +class SyncBus { + private ydoc: Doc = ydoc; + private provider: WebrtcProvider | null = null; + + startDataSync = async (params: StartDataSyncParams) => { + // 开发时由于存在 fast refresh 全局实例会缓存在运行时中 + // 因此需要在每次重新连接时清理上一次的实例 + if (window.__ONLY_USE_FOR_CLEANUP_IN_DEV) { + this.cleanProvider(window.__ONLY_USE_FOR_CLEANUP_IN_DEV); + } + + this.connect(params); + }; + + connect = (params: StartDataSyncParams) => { + const { + channel, + onSyncEvent, + onSyncStatusChange, + user, + onAwarenessChange, + signaling = 'wss://y-webrtc-signaling.lobehub.com', + } = params; + console.log('[YJS] start to listen sync event...'); + this.initYjsObserve(onSyncEvent, onSyncStatusChange); + + console.log(`[WebRTC] init provider... room: ${channel.name}`); + // clients connected to the same room-name share document updates + this.provider = new WebrtcProvider(channel.name, this.ydoc, { + password: channel.password, + signaling: [signaling], + }); + + // 只在开发时解决全局实例缓存问题 + if (process.env.NODE_ENV === 'development') { + window.__ONLY_USE_FOR_CLEANUP_IN_DEV = this.provider; + } + + const provider = this.provider; + + console.log(`[WebRTC] provider init success`); + + // 当本地设备正确连接到 WebRTC Provider 后,触发 status 事件 + // 当开始连接,则开始监听事件 + provider.on('status', async ({ connected }) => { + console.log('[WebRTC] peer connected status:', connected); + if (connected) { + // this.initObserve(onSyncEvent, onSyncStatusChange); + onSyncStatusChange?.('ready'); + } + }); + + // provider.on('peers', (peers) => { + // console.log(peers); + // }); + + // 当各方的数据均完成同步后,YJS 对象之间的数据已经一致时,触发 synced 事件 + provider.on('synced', async ({ synced }) => { + console.log('[WebRTC] peer sync status:', synced); + if (synced) { + console.groupCollapsed('[WebRTC] start to init yjs data...'); + onSyncStatusChange?.('syncing'); + await this.initSync(); + onSyncStatusChange?.('synced'); + console.groupEnd(); + console.log('[WebRTC] yjs data init success'); + } else { + console.log('[WebRTC] sync failed,try to reconnect...'); + await this.reconnect(params); + } + }); + + const awareness = provider.awareness; + + awareness.setLocalState({ clientID: awareness.clientID, user }); + onAwarenessChange?.([{ ...user, clientID: awareness.clientID, current: true }]); + + awareness.on('change', () => { + const state = Array.from(awareness.getStates().values()).map((s) => ({ + ...s.user, + clientID: s.clientID, + current: s.clientID === awareness.clientID, + })); + + onAwarenessChange?.(state); + }); + + return provider; + }; + + reconnect = async (params: StartDataSyncParams) => { + this.cleanProvider(this.provider); + + this.connect(params); + }; + + private cleanProvider(provider: WebrtcProvider | null) { + if (provider) { + console.log(`[WebRTC] clean provider...`); + provider.room?.destroy(); + provider.awareness?.destroy(); + provider.destroy(); + console.log(`[WebRTC] clean success`); + console.log(`[WebRTC] -------------------`); + } + } + + manualSync = async () => { + console.log('[WebRTC] try to manual init sync...'); + await this.initSync(); + }; + + getYMap = (tableKey: keyof LobeDBSchemaMap) => { + return this.ydoc.getMap(tableKey); + }; + + private initSync = async () => { + await Promise.all( + ['sessions', 'sessionGroups', 'topics', 'messages', 'plugins'].map(async (tableKey) => + this.loadDataFromDBtoYjs(tableKey as keyof LobeDBSchemaMap), + ), + ); + }; + + private initYjsObserve = (onEvent: OnSyncEvent, onSyncStatusChange: OnSyncStatusChange) => { + ['sessions', 'sessionGroups', 'topics', 'messages', 'plugins'].forEach((tableKey) => { + // listen yjs change + this.observeYMapChange(tableKey as keyof LobeDBSchemaMap, onEvent, onSyncStatusChange); + }); + }; + + private observeYMapChange = ( + tableKey: keyof LobeDBSchemaMap, + onEvent: OnSyncEvent, + onSyncStatusChange: OnSyncStatusChange, + ) => { + const table = LocalDBInstance[tableKey]; + const yItemMap = this.getYMap(tableKey); + const updateSyncEvent = throttle(onEvent, 1000); + + // 定义一个变量来保存定时器的ID + // eslint-disable-next-line no-undef + let debounceTimer: NodeJS.Timeout; + + yItemMap.observe(async (event) => { + // abort local change + if (event.transaction.local) return; + + // 每次有变更时,都先清除之前的定时器(如果有的话),然后设置新的定时器 + clearTimeout(debounceTimer); + + onSyncStatusChange('syncing'); + + console.log(`[YJS] observe ${tableKey} changes:`, event.keysChanged.size); + const pools = Array.from(event.keys).map(async ([id, payload]) => { + const item: any = yItemMap.get(id); + + switch (payload.action) { + case 'add': + case 'update': { + const itemInTable = await table.get(id); + if (!itemInTable) { + await table.add(item, id); + } else { + await table.update(id, item); + } + break; + } + + case 'delete': { + await table.delete(id); + break; + } + } + }); + + await Promise.all(pools); + + updateSyncEvent(tableKey); + + // 设置定时器,500ms 后更新状态为'synced' + debounceTimer = setTimeout(() => { + onSyncStatusChange('synced'); + }, 1000); + }); + }; + + private loadDataFromDBtoYjs = async (tableKey: keyof LobeDBSchemaMap) => { + const table = LocalDBInstance[tableKey]; + const items = await table.toArray(); + const yItemMap = this.getYMap(tableKey); + + // 定义每批次最多包含的数据条数 + const batchSize = 50; + + // 计算总批次数 + const totalBatches = Math.ceil(items.length / batchSize); + + for (let i = 0; i < totalBatches; i++) { + // 计算当前批次的起始和结束索引 + const start = i * batchSize; + const end = start + batchSize; + + // 获取当前批次的数据 + const batchItems = items.slice(start, end); + + // 将当前批次的数据推送到 Yjs 中 + + this.ydoc.transact(() => { + batchItems.forEach((item) => { + // TODO: 需要改表,所有 table 都需要有 id 字段 + yItemMap.set(item.id || (item as any).identifier, item); + }); + }); + } + + console.log('[DB]:', tableKey, yItemMap.size); + }; +} + +export const syncBus = new SyncBus(); diff --git a/src/database/models/file.ts b/src/database/models/file.ts index d04766d56009..df742208f04d 100644 --- a/src/database/models/file.ts +++ b/src/database/models/file.ts @@ -11,7 +11,7 @@ class _FileModel extends BaseModel<'files'> { async create(file: DB_File) { const id = nanoid(); - return this._add(file, `file-${id}`); + return this._addWithSync(file, `file-${id}`); } async findById(id: string) { diff --git a/src/database/models/message.ts b/src/database/models/message.ts index 143c71a3e247..e57bcce0afad 100644 --- a/src/database/models/message.ts +++ b/src/database/models/message.ts @@ -107,7 +107,7 @@ class _MessageModel extends BaseModel { const messageData: DB_Message = this.mapChatMessageToDBMessage(data as ChatMessage); - return this._add(messageData, id); + return this._addWithSync(messageData, id); } async batchCreate(messages: ChatMessage[]) { @@ -148,11 +148,11 @@ class _MessageModel extends BaseModel { // **************** Delete *************** // async delete(id: string) { - return this.table.delete(id); + return super._deleteWithSync(id); } async clearTable() { - return this.table.clear(); + return this._clearWithSync(); } /** @@ -178,13 +178,13 @@ class _MessageModel extends BaseModel { const messageIds = await query.primaryKeys(); // Use the bulkDelete method to delete all selected messages in bulk - return this.table.bulkDelete(messageIds); + return this._bulkDeleteWithSync(messageIds); } // **************** Update *************** // async update(id: string, data: DeepPartial) { - return this._update(id, data); + return super._updateWithSync(id, data); } async updatePluginState(id: string, key: string, value: any) { diff --git a/src/database/models/plugin.ts b/src/database/models/plugin.ts index ba91966e5f05..23d4928b6ebd 100644 --- a/src/database/models/plugin.ts +++ b/src/database/models/plugin.ts @@ -21,7 +21,6 @@ class _PluginModel extends BaseModel { getList = async (): Promise => { return this.table.toArray(); }; - // **************** Create *************** // create = async (plugin: InstallPluginParams) => { @@ -39,10 +38,10 @@ class _PluginModel extends BaseModel { // **************** Delete *************** // delete(id: string) { - return this.table.delete(id); + return this._deleteWithSync(id); } clear() { - return this.table.clear(); + return this._clearWithSync(); } // **************** Update *************** // diff --git a/src/database/models/session.ts b/src/database/models/session.ts index 43ba801731c4..e79e51028b3d 100644 --- a/src/database/models/session.ts +++ b/src/database/models/session.ts @@ -173,7 +173,7 @@ class _SessionModel extends BaseModel { async create(type: 'agent' | 'group', defaultValue: Partial, id = uuid()) { const data = merge(DEFAULT_AGENT_LOBE_SESSION, { type, ...defaultValue }); const dataDB = this.mapToDB_Session(data); - return this._add(dataDB, id); + return this._addWithSync(dataDB, id); } async batchCreate(sessions: LobeAgentSession[]) { @@ -200,7 +200,7 @@ class _SessionModel extends BaseModel { const newSession = merge(session, { meta: { title: newTitle } }); - return this._add(newSession, uuid()); + return this._addWithSync(newSession, uuid()); } // **************** Delete *************** // @@ -225,7 +225,7 @@ class _SessionModel extends BaseModel { } // Finally, delete the session itself - await this.table.delete(id); + await this._deleteWithSync(id); }); } @@ -236,7 +236,7 @@ class _SessionModel extends BaseModel { // **************** Update *************** // async update(id: string, data: Partial) { - return this._update(id, data); + return super._updateWithSync(id, data); } async updatePinned(id: string, pinned: boolean) { diff --git a/src/database/models/sessionGroup.ts b/src/database/models/sessionGroup.ts index 01d3eeddbe1a..ffce3c19e0cb 100644 --- a/src/database/models/sessionGroup.ts +++ b/src/database/models/sessionGroup.ts @@ -42,7 +42,7 @@ class _SessionGroupModel extends BaseModel { // **************** Create *************** // async create(name: string, sort?: number, id = nanoid()) { - return this._add({ name, sort }, id); + return this._addWithSync({ name, sort }, id); } async batchCreate(groups: SessionGroups) { @@ -69,7 +69,7 @@ class _SessionGroupModel extends BaseModel { // **************** Update *************** // async update(id: string, data: Partial) { - return super._update(id, data); + return super._updateWithSync(id, data); } async updateOrder(sortMap: { id: string; sort: number }[]) { diff --git a/src/database/models/topic.ts b/src/database/models/topic.ts index addb36658bc1..838314411e9a 100644 --- a/src/database/models/topic.ts +++ b/src/database/models/topic.ts @@ -116,7 +116,10 @@ class _TopicModel extends BaseModel { // **************** Create *************** // async create({ title, favorite, sessionId, messages }: CreateTopicParams, id = nanoid()) { - const topic = await this._add({ favorite: favorite ? 1 : 0, sessionId, title: title }, id); + const topic = await this._addWithSync( + { favorite: favorite ? 1 : 0, sessionId, title: title }, + id, + ); // add topicId to these messages if (messages) { @@ -214,7 +217,7 @@ class _TopicModel extends BaseModel { // **************** Update *************** // async update(id: string, data: Partial) { - return this._update(id, data); + return super._updateWithSync(id, { ...data, updatedAt: Date.now() }); } async toggleFavorite(id: string, newState?: boolean) { diff --git a/src/hooks/useSyncData.ts b/src/hooks/useSyncData.ts new file mode 100644 index 000000000000..e44660575782 --- /dev/null +++ b/src/hooks/useSyncData.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react'; + +import { useChatStore } from '@/store/chat'; +import { useGlobalStore } from '@/store/global'; +import { useSessionStore } from '@/store/session'; + +export const useSyncEvent = () => { + const [refreshMessages, refreshTopic] = useChatStore((s) => [s.refreshMessages, s.refreshTopic]); + const [refreshSessions] = useSessionStore((s) => [s.refreshSessions]); + + return useCallback((tableKey: string) => { + // console.log('triggerSync Event:', tableKey); + + switch (tableKey) { + case 'messages': { + refreshMessages(); + break; + } + + case 'topics': { + refreshTopic(); + break; + } + + case 'sessions': { + refreshSessions(); + break; + } + + default: { + break; + } + } + }, []); +}; + +export const useEnabledDataSync = () => { + const [userId, useEnabledSync] = useGlobalStore((s) => [s.userId, s.useEnabledSync]); + + const syncEvent = useSyncEvent(); + + useEnabledSync(userId, syncEvent); +}; diff --git a/src/layout/GlobalLayout/StoreHydration.tsx b/src/layout/GlobalLayout/StoreHydration.tsx index 1bb5087db523..57b0fa47cc91 100644 --- a/src/layout/GlobalLayout/StoreHydration.tsx +++ b/src/layout/GlobalLayout/StoreHydration.tsx @@ -2,6 +2,7 @@ import { useResponsive } from 'antd-style'; import { useRouter } from 'next/navigation'; import { memo, useEffect } from 'react'; +import { useEnabledDataSync } from '@/hooks/useSyncData'; import { useGlobalStore } from '@/store/global'; import { useEffectAfterGlobalHydrated } from '@/store/global/hooks/useEffectAfterHydrated'; @@ -10,10 +11,13 @@ const StoreHydration = memo(() => { s.useFetchServerConfig, s.useFetchUserConfig, ]); + const { isLoading } = useFetchServerConfig(); useFetchUserConfig(!isLoading); + useEnabledDataSync(); + useEffect(() => { // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated useGlobalStore.persist.rehydrate(); @@ -46,6 +50,7 @@ const StoreHydration = memo(() => { router.prefetch('/market'); router.prefetch('/settings/common'); router.prefetch('/settings/agent'); + router.prefetch('/settings/sync'); }, [router]); return null; diff --git a/src/locales/default/setting.ts b/src/locales/default/setting.ts index 79b5639429d9..9d146ebba185 100644 --- a/src/locales/default/setting.ts +++ b/src/locales/default/setting.ts @@ -442,12 +442,26 @@ export default { placeholder: '请输入助手的标识符,需要是唯一的,比如 web-development', tooltips: '分享到助手市场', }, - + sync: { + webrtc: { + channelName: { + desc: 'WebRTC 将使用此名创建同步频道,确保频道名称唯一', + placeholder: '请输入同步频道名称', + title: '同步频道名称', + }, + channelPassword: { + desc: '请输入一个同步频道密码', + title: '同步频道密码', + }, + title: 'WebRTC 同步', + }, + }, tab: { about: '关于', agent: '默认助手', common: '通用设置', llm: '语言模型', + sync: '云端同步', tts: '语音服务', }, tools: { diff --git a/src/services/global.ts b/src/services/global.ts index 12d6618c7de4..7d6324414a1e 100644 --- a/src/services/global.ts +++ b/src/services/global.ts @@ -1,4 +1,6 @@ +import { syncBus } from '@/database/core'; import { GlobalServerConfig } from '@/types/settings'; +import { StartDataSyncParams } from '@/types/sync'; import { API_ENDPOINTS } from './_url'; @@ -20,6 +22,21 @@ class GlobalService { return res.json(); }; + + enabledSync = async (params: StartDataSyncParams) => { + if (typeof window === 'undefined') return false; + + await syncBus.startDataSync(params); + return true; + }; + + reconnect = async (params: StartDataSyncParams) => { + if (typeof window === 'undefined') return false; + + await syncBus.reconnect(params); + await syncBus.manualSync(); + return true; + }; } export const globalService = new GlobalService(); diff --git a/src/store/chat/slices/topic/action.ts b/src/store/chat/slices/topic/action.ts index 319f9985f1cb..a2b1b7ff6bfd 100644 --- a/src/store/chat/slices/topic/action.ts +++ b/src/store/chat/slices/topic/action.ts @@ -9,6 +9,7 @@ import { StateCreator } from 'zustand/vanilla'; import { chainSummaryTitle } from '@/chains/summaryTitle'; import { LOADING_FLAT } from '@/const/message'; import { TraceNameMap } from '@/const/trace'; +import { useClientDataSWR } from '@/libs/swr'; import { chatService } from '@/services/chat'; import { messageService } from '@/services/message'; import { topicService } from '@/services/topic'; @@ -22,6 +23,9 @@ import { topicSelectors } from './selectors'; const n = setNamespace('topic'); +const SWR_USE_FETCH_TOPIC = 'SWR_USE_FETCH_TOPIC'; +const SWR_USE_SEARCH_TOPIC = 'SWR_USE_SEARCH_TOPIC'; + export interface ChatTopicAction { favoriteTopic: (id: string, favState: boolean) => Promise; openNewTopicOrSaveTopic: () => Promise; @@ -141,18 +145,25 @@ export const chatTopic: StateCreator< }, // query useFetchTopics: (sessionId) => - useSWR(sessionId, async (sessionId) => topicService.getTopics({ sessionId }), { - onSuccess: (topics) => { - set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId })); + useClientDataSWR( + [SWR_USE_FETCH_TOPIC, sessionId], + async ([, sessionId]: [string, string]) => topicService.getTopics({ sessionId }), + { + onSuccess: (topics) => { + set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId })); + }, }, - dedupingInterval: 0, - }), + ), useSearchTopics: (keywords) => - useSWR(keywords, topicService.searchTopics, { - onSuccess: (data) => { - set({ searchTopics: data }, false, n('useSearchTopics(success)', { keywords })); + useSWR( + [SWR_USE_SEARCH_TOPIC, keywords], + ([, keywords]: [string, string]) => topicService.searchTopics(keywords), + { + onSuccess: (data) => { + set({ searchTopics: data }, false, n('useSearchTopics(success)', { keywords })); + }, }, - }), + ), switchTopic: async (id) => { set({ activeTopicId: id }, false, n('toggleTopic')); @@ -213,6 +224,6 @@ export const chatTopic: StateCreator< set({ topicLoadingId: id }, false, n('updateTopicLoading')); }, refreshTopic: async () => { - await mutate(get().activeId); + await mutate([SWR_USE_FETCH_TOPIC, get().activeId]); }, }); diff --git a/src/store/global/slices/common/action.ts b/src/store/global/slices/common/action.ts index e7d52de0a035..20421b03e772 100644 --- a/src/store/global/slices/common/action.ts +++ b/src/store/global/slices/common/action.ts @@ -11,7 +11,9 @@ import { messageService } from '@/services/message'; import { UserConfig, userService } from '@/services/user'; import type { GlobalStore } from '@/store/global'; import type { GlobalServerConfig, GlobalSettings } from '@/types/settings'; +import { OnSyncEvent } from '@/types/sync'; import { merge } from '@/utils/merge'; +import { browserInfo } from '@/utils/platform'; import { setNamespace } from '@/utils/storeDebug'; import { switchLang } from '@/utils/switchLang'; @@ -24,11 +26,13 @@ const n = setNamespace('common'); * 设置操作 */ export interface CommonAction { + refreshConnection: (onEvent: OnSyncEvent) => Promise; refreshUserConfig: () => Promise; switchBackToChat: (sessionId?: string) => void; updateAvatar: (avatar: string) => Promise; useCheckLatestVersion: () => SWRResponse; useCheckTrace: (shouldFetch: boolean) => SWRResponse; + useEnabledSync: (userId: string | undefined, onEvent: OnSyncEvent) => SWRResponse; useFetchServerConfig: () => SWRResponse; useFetchUserConfig: (initServer: boolean) => SWRResponse; } @@ -41,13 +45,38 @@ export const createCommonSlice: StateCreator< [], CommonAction > = (set, get) => ({ + refreshConnection: async (onEvent) => { + const sync = settingsSelectors.syncConfig(get()); + if (!sync.channelName) return; + + await globalService.reconnect({ + channel: { + name: sync.channelName, + password: sync.channelPassword, + }, + onAwarenessChange(state) { + set({ syncAwareness: state }); + }, + onSyncEvent: onEvent, + onSyncStatusChange: (status) => { + set({ syncStatus: status }); + }, + signaling: sync.signaling, + user: { + // name: userId, + id: get().userId!, + ...browserInfo, + }, + }); + }, + refreshUserConfig: async () => { await mutate([USER_CONFIG_FETCH_KEY, true]); }, + switchBackToChat: (sessionId) => { get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile)); }, - updateAvatar: async (avatar) => { await userService.updateAvatar(avatar); await get().refreshUserConfig(); @@ -78,6 +107,42 @@ export const createCommonSlice: StateCreator< revalidateOnFocus: false, }, ), + useEnabledSync: (userId, onEvent) => + useSWR( + ['enableSync', userId], + async () => { + if (!userId) return false; + + const sync = settingsSelectors.syncConfig(get()); + if (!sync.channelName) return false; + + return globalService.enabledSync({ + channel: { + name: sync.channelName, + password: sync.channelPassword, + }, + onAwarenessChange(state) { + set({ syncAwareness: state }); + }, + onSyncEvent: onEvent, + onSyncStatusChange: (status) => { + set({ syncStatus: status }); + }, + signaling: sync.signaling, + user: { + // name: userId, + id: userId, + ...browserInfo, + }, + }); + }, + { + onSuccess: (syncEnabled) => { + set({ syncEnabled }); + }, + revalidateOnFocus: false, + }, + ), useFetchServerConfig: () => useSWR('fetchGlobalConfig', globalService.getGlobalConfig, { onSuccess: (data) => { diff --git a/src/store/global/slices/common/initialState.ts b/src/store/global/slices/common/initialState.ts index dcdb2b828e3b..72c31bf20e0e 100644 --- a/src/store/global/slices/common/initialState.ts +++ b/src/store/global/slices/common/initialState.ts @@ -1,5 +1,7 @@ import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; +import { PeerSyncStatus, SyncAwarenessState } from '@/types/sync'; + export enum SidebarTabKey { Chat = 'chat', Market = 'market', @@ -11,6 +13,7 @@ export enum SettingsTabs { Agent = 'agent', Common = 'common', LLM = 'llm', + Sync = 'sync', TTS = 'tts', } @@ -25,9 +28,15 @@ export interface GlobalCommonState { latestVersion?: string; router?: AppRouterInstance; sidebarKey: SidebarTabKey; + syncAwareness: SyncAwarenessState[]; + syncEnabled: boolean; + syncStatus: PeerSyncStatus; } export const initialCommonState: GlobalCommonState = { isMobile: false, sidebarKey: SidebarTabKey.Chat, + syncAwareness: [], + syncEnabled: false, + syncStatus: 'ready', }; diff --git a/src/store/global/slices/settings/selectors/settings.ts b/src/store/global/slices/settings/selectors/settings.ts index aa7fa80ea855..9ff1f656c937 100644 --- a/src/store/global/slices/settings/selectors/settings.ts +++ b/src/store/global/slices/settings/selectors/settings.ts @@ -44,6 +44,8 @@ const currentLanguage = (s: GlobalStore) => { const dalleConfig = (s: GlobalStore) => currentSettings(s).tool?.dalle || {}; const isDalleAutoGenerating = (s: GlobalStore) => currentSettings(s).tool?.dalle?.autoGenerate; +const syncConfig = (s: GlobalStore) => currentSettings(s).sync; + export const settingsSelectors = { currentLanguage, currentSettings, @@ -55,4 +57,5 @@ export const settingsSelectors = { exportSettings, isDalleAutoGenerating, password, + syncConfig, }; diff --git a/src/types/settings/index.ts b/src/types/settings/index.ts index 3a931761dd26..66c821da0eb5 100644 --- a/src/types/settings/index.ts +++ b/src/types/settings/index.ts @@ -28,12 +28,19 @@ export interface GlobalServerConfig { }; } +export interface GlobalSyncSettings { + channelName?: string; + channelPassword?: string; + signaling?: string; +} + /** * 配置设置 */ export interface GlobalSettings extends GlobalBaseSettings { defaultAgent: GlobalDefaultAgent; languageModel: GlobalLLMConfig; + sync: GlobalSyncSettings; tool: GlobalTool; tts: GlobalTTSConfig; } diff --git a/src/types/sync.ts b/src/types/sync.ts new file mode 100644 index 000000000000..4d5bfed108fc --- /dev/null +++ b/src/types/sync.ts @@ -0,0 +1,32 @@ +import { LobeDBSchemaMap } from '@/database/core/db'; + +export type OnSyncEvent = (tableKey: keyof LobeDBSchemaMap) => void; +export type OnSyncStatusChange = (status: PeerSyncStatus) => void; + +export type PeerSyncStatus = 'syncing' | 'synced' | 'ready'; + +export interface StartDataSyncParams { + channel: { + name: string; + password?: string; + }; + onAwarenessChange: (state: SyncAwarenessState[]) => void; + onSyncEvent: OnSyncEvent; + onSyncStatusChange: OnSyncStatusChange; + signaling?: string; + user: SyncUserInfo; +} + +export interface SyncUserInfo { + browser?: string; + device?: string; + id: string; + isMobile: boolean; + name?: string; + os?: string; +} + +export interface SyncAwarenessState extends SyncUserInfo { + clientID: number; + current: boolean; +} diff --git a/src/utils/platform.ts b/src/utils/platform.ts index 44e5e8d16b11..0a5eb8704489 100644 --- a/src/utils/platform.ts +++ b/src/utils/platform.ts @@ -1,6 +1,6 @@ import UAParser from 'ua-parser-js'; -const getPaser = () => { +const getParser = () => { if (typeof window === 'undefined') return new UAParser('Node'); let ua = navigator.userAgent; @@ -8,11 +8,18 @@ const getPaser = () => { }; export const getPlatform = () => { - return getPaser().getOS().name; + return getParser().getOS().name; }; export const getBrowser = () => { - return getPaser().getResult().browser.name; + return getParser().getResult().browser.name; +}; + +export const browserInfo = { + browser: getBrowser(), + device: getParser().getDevice().vendor, + isMobile: getParser().getDevice().type === 'mobile', + os: getParser().getOS().name, }; export const isMacOS = () => getPlatform() === 'Mac OS'; From f712ab7b81453341902818d2bbc0181364dfad4a Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 14 Mar 2024 20:30:34 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=F0=9F=8E=A8=20chore:=20improve=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/database/models/topic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/models/topic.ts b/src/database/models/topic.ts index 838314411e9a..27bcf261d587 100644 --- a/src/database/models/topic.ts +++ b/src/database/models/topic.ts @@ -217,7 +217,7 @@ class _TopicModel extends BaseModel { // **************** Update *************** // async update(id: string, data: Partial) { - return super._updateWithSync(id, { ...data, updatedAt: Date.now() }); + return super._updateWithSync(id, data); } async toggleFavorite(id: string, newState?: boolean) { From f880f7bc67c91a00e10ed605588141a422fd0aed Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 14 Mar 2024 20:38:38 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=F0=9F=8E=A8=20chore:=20improve=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/chat/slices/topic/action.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/chat/slices/topic/action.test.ts b/src/store/chat/slices/topic/action.test.ts index 34de50b38c60..a55faf5d4960 100644 --- a/src/store/chat/slices/topic/action.test.ts +++ b/src/store/chat/slices/topic/action.test.ts @@ -148,7 +148,7 @@ describe('topic action', () => { }); // Check if mutate has been called with the active session ID - expect(mutate).toHaveBeenCalledWith(activeId); + expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_TOPIC', activeId]); }); it('should handle errors during refreshing topics', async () => { From 0f3a527b29ac4bd505e267e0a0e41080d64c6d22 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Fri, 15 Mar 2024 00:27:39 +0800 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20dont=20sync?= =?UTF-8?q?=20when=20a=20node=20enter=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/app/chat/features/SyncStatusTag/index.tsx | 14 +- src/database/core/index.ts | 2 +- src/database/core/model.ts | 16 +- src/database/core/sync.ts | 156 +++++++++++------- src/services/global.ts | 8 +- src/types/sync.ts | 3 +- 7 files changed, 127 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index a0fbaec9afb2..e047cfc1a323 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "use-merge-value": "^1", "utility-types": "^3", "uuid": "^9", + "y-protocols": "^1.0.6", "y-webrtc": "^10.3.0", "yaml": "^2", "yjs": "^13.6.14", diff --git a/src/app/chat/features/SyncStatusTag/index.tsx b/src/app/chat/features/SyncStatusTag/index.tsx index 3f2f68b1e6a7..6ab5e509980a 100644 --- a/src/app/chat/features/SyncStatusTag/index.tsx +++ b/src/app/chat/features/SyncStatusTag/index.tsx @@ -7,6 +7,7 @@ import { LucideCloudy, LucideLaptop, LucideRefreshCw, + LucideRouter, LucideSmartphone, } from 'lucide-react'; import Link from 'next/link'; @@ -90,7 +91,7 @@ const SyncStatusTag = memo(() => { )} - {user.device} · {user.os} · {user.browser} + {user.device} · {user.os} · {user.browser} · {user.clientID} @@ -98,13 +99,20 @@ const SyncStatusTag = memo(() => { } - // open + open placement={'bottomLeft'} > } + icon={ + + } > {text[syncStatus]} diff --git a/src/database/core/index.ts b/src/database/core/index.ts index f686e56c091b..58363a1031ff 100644 --- a/src/database/core/index.ts +++ b/src/database/core/index.ts @@ -1,3 +1,3 @@ export { LocalDBInstance } from './db'; export * from './model'; -export { syncBus } from './sync'; +export { dataSync } from './sync'; diff --git a/src/database/core/model.ts b/src/database/core/model.ts index 22edc55b0237..92bb9f415e32 100644 --- a/src/database/core/model.ts +++ b/src/database/core/model.ts @@ -5,7 +5,7 @@ import { DBBaseFieldsSchema } from '@/database/core/types/db'; import { nanoid } from '@/utils/uuid'; import { LocalDB, LocalDBInstance, LocalDBSchema } from './db'; -import { syncBus } from './sync'; +import { dataSync } from './sync'; export class BaseModel { protected readonly db: LocalDB; @@ -23,7 +23,7 @@ export class BaseModel { - this.yMap.delete(id); + dataSync.transact(() => { + keys.forEach((id) => { + this.yMap?.delete(id); + }); }); } protected async _clearWithSync() { const result = await this.table.clear(); // sync clear data to yjs data map - this.yMap.clear(); + this.yMap?.clear(); return result; } private updateYMapItem = async (id: string) => { const newData = await this.table.get(id); - this.yMap.set(id, newData); + this.yMap?.set(id, newData); }; } diff --git a/src/database/core/sync.ts b/src/database/core/sync.ts index a4e7274fe71f..ba0c2c618cfe 100644 --- a/src/database/core/sync.ts +++ b/src/database/core/sync.ts @@ -1,8 +1,13 @@ import { throttle } from 'lodash-es'; -import { WebrtcProvider } from 'y-webrtc'; -import { Doc } from 'yjs'; +import type { WebrtcProvider } from 'y-webrtc'; +import type { Doc, Transaction } from 'yjs'; -import { OnSyncEvent, OnSyncStatusChange, StartDataSyncParams } from '@/types/sync'; +import { + OnAwarenessChange, + OnSyncEvent, + OnSyncStatusChange, + StartDataSyncParams, +} from '@/types/sync'; import { LobeDBSchemaMap, LocalDBInstance } from './db'; @@ -12,27 +17,35 @@ declare global { } } -let ydoc: Doc; +class DataSync { + private _ydoc: Doc | null = null; + private provider: WebrtcProvider | null = null; -if (typeof window !== 'undefined') { - ydoc = new Doc(); -} + private syncParams!: StartDataSyncParams; + private onAwarenessChange!: OnAwarenessChange; -class SyncBus { - private ydoc: Doc = ydoc; - private provider: WebrtcProvider | null = null; + transact(fn: (transaction: Transaction) => unknown) { + this._ydoc?.transact(fn); + } + + getYMap = (tableKey: keyof LobeDBSchemaMap) => { + return this._ydoc?.getMap(tableKey); + }; startDataSync = async (params: StartDataSyncParams) => { + this.syncParams = params; + this.onAwarenessChange = params.onAwarenessChange; + // 开发时由于存在 fast refresh 全局实例会缓存在运行时中 // 因此需要在每次重新连接时清理上一次的实例 if (window.__ONLY_USE_FOR_CLEANUP_IN_DEV) { - this.cleanProvider(window.__ONLY_USE_FOR_CLEANUP_IN_DEV); + await this.cleanConnection(window.__ONLY_USE_FOR_CLEANUP_IN_DEV); } - this.connect(params); + await this.connect(params); }; - connect = (params: StartDataSyncParams) => { + connect = async (params: StartDataSyncParams) => { const { channel, onSyncEvent, @@ -41,12 +54,16 @@ class SyncBus { onAwarenessChange, signaling = 'wss://y-webrtc-signaling.lobehub.com', } = params; + await this.initYDoc(); + console.log('[YJS] start to listen sync event...'); this.initYjsObserve(onSyncEvent, onSyncStatusChange); console.log(`[WebRTC] init provider... room: ${channel.name}`); + const { WebrtcProvider } = await import('y-webrtc'); + // clients connected to the same room-name share document updates - this.provider = new WebrtcProvider(channel.name, this.ydoc, { + this.provider = new WebrtcProvider(channel.name, this._ydoc!, { password: channel.password, signaling: [signaling], }); @@ -70,10 +87,6 @@ class SyncBus { } }); - // provider.on('peers', (peers) => { - // console.log(peers); - // }); - // 当各方的数据均完成同步后,YJS 对象之间的数据已经一致时,触发 synced 事件 provider.on('synced', async ({ synced }) => { console.log('[WebRTC] peer sync status:', synced); @@ -85,55 +98,61 @@ class SyncBus { console.groupEnd(); console.log('[WebRTC] yjs data init success'); } else { - console.log('[WebRTC] sync failed,try to reconnect...'); - await this.reconnect(params); + console.log('[WebRTC] data not sync, try to reconnect in 1s...'); + // await this.reconnect(params); + setTimeout(() => { + onSyncStatusChange?.('syncing'); + this.reconnect(params); + }, 1000); } }); - const awareness = provider.awareness; + this.initAwareness({ onAwarenessChange, user }); - awareness.setLocalState({ clientID: awareness.clientID, user }); - onAwarenessChange?.([{ ...user, clientID: awareness.clientID, current: true }]); + return provider; + }; - awareness.on('change', () => { - const state = Array.from(awareness.getStates().values()).map((s) => ({ - ...s.user, - clientID: s.clientID, - current: s.clientID === awareness.clientID, - })); + manualSync = async () => { + console.log('[WebRTC] try to manual init sync...'); + await this.reconnect(this.syncParams); + }; - onAwarenessChange?.(state); - }); + reconnect = async (params: StartDataSyncParams) => { + await this.cleanConnection(this.provider); - return provider; + await this.connect(params); }; - reconnect = async (params: StartDataSyncParams) => { - this.cleanProvider(this.provider); + private initYDoc = async () => { + if (typeof window === 'undefined') return; - this.connect(params); + console.log('[YJS] init YDoc...'); + const { Doc } = await import('yjs'); + this._ydoc = new Doc(); }; - private cleanProvider(provider: WebrtcProvider | null) { + private async cleanConnection(provider: WebrtcProvider | null) { if (provider) { - console.log(`[WebRTC] clean provider...`); + console.groupCollapsed(`[WebRTC] clean Connection...`); + console.log(`[WebRTC] clean awareness...`); + provider.awareness.destroy(); + + console.log(`[WebRTC] clean room...`); + provider.room?.disconnect(); provider.room?.destroy(); - provider.awareness?.destroy(); + + console.log(`[WebRTC] clean provider...`); + provider.disconnect(); provider.destroy(); - console.log(`[WebRTC] clean success`); + + console.log(`[WebRTC] clean yjs doc...`); + this._ydoc?.destroy(); + console.groupEnd(); + console.log(`[WebRTC] -------------------`); } } - manualSync = async () => { - console.log('[WebRTC] try to manual init sync...'); - await this.initSync(); - }; - - getYMap = (tableKey: keyof LobeDBSchemaMap) => { - return this.ydoc.getMap(tableKey); - }; - private initSync = async () => { await Promise.all( ['sessions', 'sessionGroups', 'topics', 'messages', 'plugins'].map(async (tableKey) => @@ -162,7 +181,7 @@ class SyncBus { // eslint-disable-next-line no-undef let debounceTimer: NodeJS.Timeout; - yItemMap.observe(async (event) => { + yItemMap?.observe(async (event) => { // abort local change if (event.transaction.local) return; @@ -198,10 +217,10 @@ class SyncBus { updateSyncEvent(tableKey); - // 设置定时器,500ms 后更新状态为'synced' + // 设置定时器,2000ms 后更新状态为'synced' debounceTimer = setTimeout(() => { onSyncStatusChange('synced'); - }, 1000); + }, 2000); }); }; @@ -225,17 +244,40 @@ class SyncBus { const batchItems = items.slice(start, end); // 将当前批次的数据推送到 Yjs 中 - - this.ydoc.transact(() => { + this._ydoc?.transact(() => { batchItems.forEach((item) => { - // TODO: 需要改表,所有 table 都需要有 id 字段 - yItemMap.set(item.id || (item as any).identifier, item); + yItemMap!.set(item.id, item); }); }); } - console.log('[DB]:', tableKey, yItemMap.size); + console.log('[DB]:', tableKey, yItemMap?.size); + }; + + private initAwareness = ({ user }: Pick) => { + if (!this.provider) return; + + const awareness = this.provider.awareness; + + awareness.setLocalState({ clientID: awareness.clientID, user }); + this.onAwarenessChange?.([{ ...user, clientID: awareness.clientID, current: true }]); + + awareness.on('change', () => this.syncAwarenessToUI()); + }; + + private syncAwarenessToUI = async () => { + const awareness = this.provider?.awareness; + + if (!awareness) return; + + const state = Array.from(awareness.getStates().values()).map((s) => ({ + ...s.user, + clientID: s.clientID, + current: s.clientID === awareness.clientID, + })); + + this.onAwarenessChange?.(state); }; } -export const syncBus = new SyncBus(); +export const dataSync = new DataSync(); diff --git a/src/services/global.ts b/src/services/global.ts index 7d6324414a1e..f6a04c4d86bb 100644 --- a/src/services/global.ts +++ b/src/services/global.ts @@ -1,4 +1,4 @@ -import { syncBus } from '@/database/core'; +import { dataSync } from '@/database/core'; import { GlobalServerConfig } from '@/types/settings'; import { StartDataSyncParams } from '@/types/sync'; @@ -26,15 +26,15 @@ class GlobalService { enabledSync = async (params: StartDataSyncParams) => { if (typeof window === 'undefined') return false; - await syncBus.startDataSync(params); + await dataSync.startDataSync(params); return true; }; reconnect = async (params: StartDataSyncParams) => { if (typeof window === 'undefined') return false; - await syncBus.reconnect(params); - await syncBus.manualSync(); + await dataSync.reconnect(params); + await dataSync.manualSync(); return true; }; } diff --git a/src/types/sync.ts b/src/types/sync.ts index 4d5bfed108fc..457cfb88b40f 100644 --- a/src/types/sync.ts +++ b/src/types/sync.ts @@ -2,6 +2,7 @@ import { LobeDBSchemaMap } from '@/database/core/db'; export type OnSyncEvent = (tableKey: keyof LobeDBSchemaMap) => void; export type OnSyncStatusChange = (status: PeerSyncStatus) => void; +export type OnAwarenessChange = (state: SyncAwarenessState[]) => void; export type PeerSyncStatus = 'syncing' | 'synced' | 'ready'; @@ -10,7 +11,7 @@ export interface StartDataSyncParams { name: string; password?: string; }; - onAwarenessChange: (state: SyncAwarenessState[]) => void; + onAwarenessChange: OnAwarenessChange; onSyncEvent: OnSyncEvent; onSyncStatusChange: OnSyncStatusChange; signaling?: string; From a68710a1842f1d50687a019fe6d1674ee6ccd2f3 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Tue, 19 Mar 2024 01:36:12 +0800 Subject: [PATCH 05/18] =?UTF-8?q?=E2=9C=A8=20feat:=20support=20webrtc=20co?= =?UTF-8?q?nfig=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- .../chat/(desktop)/features/SessionHeader.tsx | 2 +- src/app/settings/sync/Sync.tsx | 49 ---- src/app/settings/sync/SyncSwitch/index.css | 237 ++++++++++++++++++ src/app/settings/sync/SyncSwitch/index.tsx | 79 ++++++ src/app/settings/sync/WebRTC.tsx | 116 +++++++++ src/app/settings/sync/page.tsx | 4 +- src/const/settings.ts | 2 +- src/database/core/sync.ts | 4 +- .../SyncStatusInspector/DisableSync.tsx | 49 ++++ .../SyncStatusInspector/EnableSync.tsx} | 106 ++++---- src/features/SyncStatusInspector/index.tsx | 17 ++ src/hooks/useSyncData.ts | 9 +- src/locales/default/common.ts | 13 + src/locales/default/setting.ts | 10 +- src/store/global/slices/common/action.ts | 21 +- .../global/slices/settings/selectors/index.ts | 1 + .../slices/settings/selectors/settings.ts | 3 - .../global/slices/settings/selectors/sync.ts | 10 + src/types/settings/index.ts | 8 +- src/types/settings/sync.ts | 10 + src/types/sync.ts | 1 - 22 files changed, 627 insertions(+), 129 deletions(-) delete mode 100644 src/app/settings/sync/Sync.tsx create mode 100644 src/app/settings/sync/SyncSwitch/index.css create mode 100644 src/app/settings/sync/SyncSwitch/index.tsx create mode 100644 src/app/settings/sync/WebRTC.tsx create mode 100644 src/features/SyncStatusInspector/DisableSync.tsx rename src/{app/chat/features/SyncStatusTag/index.tsx => features/SyncStatusInspector/EnableSync.tsx} (56%) create mode 100644 src/features/SyncStatusInspector/index.tsx create mode 100644 src/store/global/slices/settings/selectors/sync.ts create mode 100644 src/types/settings/sync.ts diff --git a/package.json b/package.json index e047cfc1a323..0348ec282892 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,6 @@ "@lobehub/icons": "latest", "@lobehub/tts": "latest", "@lobehub/ui": "^1.133.3", - "@theguild/remark-mermaid": "^0.0.6", "@vercel/analytics": "^1", "ahooks": "^3", "ai": "^3.0.0", @@ -130,7 +129,7 @@ "react-dom": "^18", "react-hotkeys-hook": "^4", "react-i18next": "^14", - "react-layout-kit": "^1", + "react-layout-kit": "^1.9.0", "react-lazy-load": "^4", "react-virtuoso": "^4", "react-wrap-balancer": "^1", @@ -174,7 +173,7 @@ "@types/lodash-es": "^4", "@types/node": "^20", "@types/numeral": "^2", - "@types/react": "^18", + "@types/react": "^18.2.67", "@types/react-dom": "^18", "@types/rtl-detect": "^1", "@types/semver": "^7", diff --git a/src/app/chat/(desktop)/features/SessionHeader.tsx b/src/app/chat/(desktop)/features/SessionHeader.tsx index 0eec8fd2c53e..914001984573 100644 --- a/src/app/chat/(desktop)/features/SessionHeader.tsx +++ b/src/app/chat/(desktop)/features/SessionHeader.tsx @@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens'; +import SyncStatusTag from '@/features/SyncStatusInspector'; import { useSessionStore } from '@/store/session'; import SessionSearchBar from '../../features/SessionSearchBar'; -import SyncStatusTag from '../../features/SyncStatusTag'; export const useStyles = createStyles(({ css, token }) => ({ logo: css` diff --git a/src/app/settings/sync/Sync.tsx b/src/app/settings/sync/Sync.tsx deleted file mode 100644 index 5964bfb8dfea..000000000000 --- a/src/app/settings/sync/Sync.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { SiWebrtc } from '@icons-pack/react-simple-icons'; -import { Form, type ItemGroup } from '@lobehub/ui'; -import { Form as AntForm, Input } from 'antd'; -import isEqual from 'fast-deep-equal'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { FORM_STYLE } from '@/const/layoutTokens'; -import { useGlobalStore } from '@/store/global'; -import { settingsSelectors } from '@/store/global/selectors'; - -import { useSyncSettings } from '../hooks/useSyncSettings'; - -type SettingItemGroup = ItemGroup; - -const Theme = memo(() => { - const { t } = useTranslation('setting'); - const [form] = AntForm.useForm(); - - const settings = useGlobalStore(settingsSelectors.currentSettings, isEqual); - const [setSettings] = useGlobalStore((s) => [s.setSettings]); - - useSyncSettings(form); - - const config: SettingItemGroup = { - children: [ - { - children: , - desc: t('sync.webrtc.channelName.desc'), - label: t('sync.webrtc.channelName.title'), - name: ['sync', 'channelName'], - }, - ], - icon: SiWebrtc, - title: t('sync.webrtc.title'), - }; - - return ( - - ); -}); - -export default Theme; diff --git a/src/app/settings/sync/SyncSwitch/index.css b/src/app/settings/sync/SyncSwitch/index.css new file mode 100644 index 000000000000..9cb368a03825 --- /dev/null +++ b/src/app/settings/sync/SyncSwitch/index.css @@ -0,0 +1,237 @@ +/* stylelint-disable */ +.wrapper { + --hue: 223; + --off-hue: 3; + --on-hue1: 123; + --on-hue2: 168; + --fg: hsl(var(--hue), 10%, 90%); + --primary: hsl(var(--hue), 90%, 50%); + --trans-dur: 0.6s; + --trans-timing: cubic-bezier(0.65, 0, 0.35, 1); + + font-size: 14px; +} + +.switch, +.switch__input { + -webkit-tap-highlight-color: #0000; +} + +.switch { + position: relative; + + display: block; + + width: 5em; + height: 3em; + margin: auto; +} + +.switch__base-outer, +.switch__base-inner { + position: absolute; + display: block; +} + +.switch__base-outer { + top: 0.125em; + left: 0.125em; + + width: 4.75em; + height: 2.75em; + + border-radius: 1.25em; + box-shadow: + -0.125em -0.125em 0.25em hsl(var(--hue), 10%, 30%), + 0.125em 0.125em 0.125em hsl(var(--hue), 10%, 30%) inset, + 0.125em 0.125em 0.25em hsl(0deg, 0%, 0%), + -0.125em -0.125em 0.125em hsl(var(--hue), 10%, 5%) inset; +} + +.switch__base-inner { + top: 0.375em; + left: 0.375em; + + width: 4.25em; + height: 2.25em; + + border-radius: 1.125em; + box-shadow: + -0.25em -0.25em 0.25em hsl(var(--hue), 10%, 30%) inset, + 0.0625em 0.0625em 0.125em hsla(var(--hue), 10%, 30%), + 0.125em 0.25em 0.25em hsl(var(--hue), 10%, 5%) inset, + -0.0625em -0.0625em 0.125em hsla(var(--hue), 10%, 5%); +} + +.switch__base-neon { + position: absolute; + top: 0; + left: 0; + + overflow: visible; + display: block; + + width: 100%; + height: auto; +} + +.switch__base-neon path { + stroke-dasharray: 0 104.26 0; + transition: stroke-dasharray var(--trans-dur) var(--trans-timing); +} + +.switch__input { + position: relative; + + width: 100%; + height: 100%; + appearance: none; + outline: transparent; +} + +.switch__input::before { + content: ''; + + position: absolute; + inset: -0.125em; + + display: block; + + border-radius: 0.125em; + box-shadow: 0 0 0 0.125em hsla(var(--hue), 90%, 50%, 0%); + + transition: box-shadow 0.15s linear; +} + +.switch__input:focus-visible::before { + box-shadow: 0 0 0 0.125em var(--primary); +} + +.switch__knob, +.switch__knob-container { + position: absolute; + display: block; + border-radius: 1em; +} + +.switch__knob { + width: 2em; + height: 2em; + + background-color: hsl(var(--hue), 10%, 15%); + background-image: radial-gradient( + 88% 88% at 50% 50%, + hsl(var(--hue), 10%, 20%) 47%, + hsla(var(--hue), 10%, 20%, 0%) 50% + ), + radial-gradient( + 88% 88% at 47% 47%, + hsl(var(--hue), 10%, 85%) 45%, + hsla(var(--hue), 10%, 85%, 0%) 50% + ), + radial-gradient( + 65% 70% at 40% 60%, + hsl(var(--hue), 10%, 20%) 46%, + hsla(var(--hue), 10%, 20%, 0%) 50% + ); + box-shadow: + -0.0625em -0.0625em 0.0625em hsl(var(--hue), 10%, 15%) inset, + -0.125em -0.125em 0.0625em hsl(var(--hue), 10%, 5%) inset, + 0.75em 0.25em 0.125em hsla(0deg, 0%, 0%, 80%); + + transition: transform var(--trans-dur) var(--trans-timing); +} + +.switch__knob-container { + top: 0.5em; + left: 0.5em; + + overflow: hidden; + + width: 4em; + height: 2em; +} + +.switch__knob-neon { + display: block; + width: 2em; + height: auto; +} + +.switch__knob-neon circle { + opacity: 0; + stroke-dasharray: 0 90.32 0 54.19; + transition: + opacity var(--trans-dur) steps(1, end), + stroke-dasharray var(--trans-dur) var(--trans-timing); +} + +.switch__knob-shadow { + position: absolute; + top: 0.5em; + left: 0.5em; + + display: block; + + width: 2em; + height: 2em; + + border-radius: 50%; + box-shadow: 0.125em 0.125em 0.125em hsla(0deg, 0%, 0%, 90%); + + transition: transform var(--trans-dur) var(--trans-timing); +} + +.switch__led { + position: absolute; + top: 0; + left: 0; + + display: block; + + width: 0.25em; + height: 0.25em; + + background-color: hsl(var(--off-hue), 90%, 70%); + border-radius: 50%; + box-shadow: + 0 -0.0625em 0.0625em hsl(var(--off-hue), 90%, 40%) inset, + 0 0 0.125em hsla(var(--off-hue), 90%, 70%, 30%), + 0 0 0.125em hsla(var(--off-hue), 90%, 70%, 30%), + 0.125em 0.125em 0.125em hsla(0deg, 0%, 0%, 50%); + + transition: + background-color var(--trans-dur) var(--trans-timing), + box-shadow var(--trans-dur) var(--trans-timing); +} + +.switch__text { + position: absolute; + overflow: hidden; + width: 1px; + height: 1px; +} + +.switch__input:checked ~ .switch__led { + background-color: hsl(var(--on-hue1), 90%, 70%); + box-shadow: + 0 -0.0625em 0.0625em hsl(var(--on-hue1), 90%, 40%) inset, + 0 -0.125em 0.125em hsla(var(--on-hue1), 90%, 70%, 30%), + 0 0.125em 0.125em hsla(var(--on-hue1), 90%, 70%, 30%), + 0.125em 0.125em 0.125em hsla(0deg, 0%, 0%, 50%); +} + +.switch__input:checked ~ .switch__base-neon path { + stroke-dasharray: 52.13 0 52.13; +} + +.switch__input:checked ~ .switch__knob-shadow, +.switch__input:checked ~ .switch__knob-container .switch__knob { + transform: translateX(100%); +} + +.switch__input:checked ~ .switch__knob-container .switch__knob-neon circle { + opacity: 1; + stroke-dasharray: 45.16 0 45.16 54.19; + transition-timing-function: steps(1, start), var(--trans-timing); +} diff --git a/src/app/settings/sync/SyncSwitch/index.tsx b/src/app/settings/sync/SyncSwitch/index.tsx new file mode 100644 index 000000000000..455f25375cef --- /dev/null +++ b/src/app/settings/sync/SyncSwitch/index.tsx @@ -0,0 +1,79 @@ +import { memo } from 'react'; + +import './index.css'; + +interface SyncSwitchProps { + onChange?: (checked: boolean) => void; + value?: boolean; +} +const SyncSwitch = memo(({ value, onChange }) => { + return ( +
+ +
+ ); +}); + +export default SyncSwitch; diff --git a/src/app/settings/sync/WebRTC.tsx b/src/app/settings/sync/WebRTC.tsx new file mode 100644 index 000000000000..8d5bdb7e9dc8 --- /dev/null +++ b/src/app/settings/sync/WebRTC.tsx @@ -0,0 +1,116 @@ +import { SiWebrtc } from '@icons-pack/react-simple-icons'; +import { ActionIcon, Form, type ItemGroup, Tooltip } from '@lobehub/ui'; +import { Form as AntForm, Input, Switch, Typography } from 'antd'; +import { LucideDices } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { FORM_STYLE } from '@/const/layoutTokens'; +import SyncStatusInspector from '@/features/SyncStatusInspector'; +import { useGlobalStore } from '@/store/global'; +import { uuid } from '@/utils/uuid'; + +import { useSyncSettings } from '../hooks/useSyncSettings'; + +type SettingItemGroup = ItemGroup; + +const Theme = memo(() => { + const { t } = useTranslation('setting'); + const [form] = AntForm.useForm(); + + const [setSettings] = useGlobalStore((s) => [s.setSettings]); + + useSyncSettings(form); + + const channelName = AntForm.useWatch(['sync', 'webrtc', 'channelName'], form); + + const config: SettingItemGroup = { + children: [ + { + children: ( + { + const name = uuid(); + form.setFieldValue(['sync', 'webrtc', 'channelName'], name); + form.submit(); + }} + size={'small'} + style={{ + marginRight: -4, + }} + title={t('sync.webrtc.channelName.shuffle')} + /> + } + /> + ), + desc: t('sync.webrtc.channelName.desc'), + label: t('sync.webrtc.channelName.title'), + name: ['sync', 'webrtc', 'channelName'], + }, + { + children: ( + + ), + desc: t('sync.webrtc.channelPassword.desc'), + label: t('sync.webrtc.channelPassword.title'), + name: ['sync', 'webrtc', 'channelPassword'], + }, + { + children: !channelName ? ( + + + + ) : ( + + // + ), + + label: t('sync.webrtc.enabled.title'), + minWidth: undefined, + name: ['sync', 'webrtc', 'enabled'], + }, + ], + extra: ( +
{ + e.stopPropagation(); + }} + > + +
+ ), + title: ( + + {/* @ts-ignore */} + + + {t('sync.webrtc.title')} + + {t('sync.webrtc.desc')} + + + + ), + }; + + return ( + + ); +}); + +export default Theme; diff --git a/src/app/settings/sync/page.tsx b/src/app/settings/sync/page.tsx index 5a863a9a4f06..cbdca80edd1a 100644 --- a/src/app/settings/sync/page.tsx +++ b/src/app/settings/sync/page.tsx @@ -5,14 +5,14 @@ import { useTranslation } from 'react-i18next'; import PageTitle from '@/components/PageTitle'; -import Sync from './Sync'; +import WebRTC from './WebRTC'; export default memo(() => { const { t } = useTranslation('setting'); return ( <> - + ); }); diff --git a/src/const/settings.ts b/src/const/settings.ts index 59baf7226c25..8d106bfeaa54 100644 --- a/src/const/settings.ts +++ b/src/const/settings.ts @@ -123,7 +123,7 @@ export const DEFAULT_TOOL_CONFIG = { }; const DEFAULT_SYNC_CONFIG: GlobalSyncSettings = { - channelName: 'atc', + webrtc: { enabled: false }, }; export const DEFAULT_SETTINGS: GlobalSettings = { diff --git a/src/database/core/sync.ts b/src/database/core/sync.ts index ba0c2c618cfe..7f333fdc7b0f 100644 --- a/src/database/core/sync.ts +++ b/src/database/core/sync.ts @@ -1,4 +1,4 @@ -import { throttle } from 'lodash-es'; +import { throttle, uniqBy } from 'lodash-es'; import type { WebrtcProvider } from 'y-webrtc'; import type { Doc, Transaction } from 'yjs'; @@ -276,7 +276,7 @@ class DataSync { current: s.clientID === awareness.clientID, })); - this.onAwarenessChange?.(state); + this.onAwarenessChange?.(uniqBy(state, 'id')); }; } diff --git a/src/features/SyncStatusInspector/DisableSync.tsx b/src/features/SyncStatusInspector/DisableSync.tsx new file mode 100644 index 000000000000..42d7731dcf92 --- /dev/null +++ b/src/features/SyncStatusInspector/DisableSync.tsx @@ -0,0 +1,49 @@ +import { Icon, Tag } from '@lobehub/ui'; +import { Badge, Button, Popover } from 'antd'; +import { LucideCloudCog, LucideCloudy } from 'lucide-react'; +import Link from 'next/link'; +import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +interface DisableSyncProps { + noPopover?: boolean; +} +const DisableSync = memo(({ noPopover }) => { + const tag = ( +
+ + + 同步未开启 + +
+ ); + + return noPopover ? ( + tag + ) : ( + + 当前会话数据仅存储于此浏览器中。如果你需要在多个设备间同步数据,请配置并开启云端同步。 + + + + + } + placement={'bottomLeft'} + title={ + + + 数据同步未开启 + + } + > + {tag} + + ); +}); + +export default DisableSync; diff --git a/src/app/chat/features/SyncStatusTag/index.tsx b/src/features/SyncStatusInspector/EnableSync.tsx similarity index 56% rename from src/app/chat/features/SyncStatusTag/index.tsx rename to src/features/SyncStatusInspector/EnableSync.tsx index 6ab5e509980a..b715fe7fc71d 100644 --- a/src/app/chat/features/SyncStatusTag/index.tsx +++ b/src/features/SyncStatusInspector/EnableSync.tsx @@ -1,5 +1,5 @@ -import { Avatar, Icon, Tag } from '@lobehub/ui'; -import { Tag as ATag, Badge, Button, Popover, Typography } from 'antd'; +import { ActionIcon, Avatar, Icon } from '@lobehub/ui'; +import { Tag as ATag, Popover, Typography } from 'antd'; import { useTheme } from 'antd-style'; import isEqual from 'fast-deep-equal'; import { @@ -12,24 +12,33 @@ import { } from 'lucide-react'; import Link from 'next/link'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; import { useSyncEvent } from '@/hooks/useSyncData'; import { useGlobalStore } from '@/store/global'; -import { settingsSelectors } from '@/store/global/selectors'; +import { syncSettingsSelectors } from '@/store/global/selectors'; +import { pathString } from '@/utils/url'; -const text = { - ready: '已连接', - synced: '已同步', - syncing: '同步中', -} as const; +const { Text } = Typography; -const SyncStatusTag = memo(() => { - const [syncStatus, isSyncing, enableSync, channelName] = useGlobalStore((s) => [ +const EllipsisChannelName = memo<{ text: string }>(({ text }) => { + const start = text.slice(0, 16); + const suffix = text.slice(-4).trim(); + + return ( + + {start}... + + ); +}); + +const EnableSync = memo(() => { + const { t } = useTranslation('common'); + const [syncStatus, isSyncing, channelName] = useGlobalStore((s) => [ s.syncStatus, s.syncStatus === 'syncing', - s.syncEnabled, - settingsSelectors.syncConfig(s).channelName, + syncSettingsSelectors.webrtcConfig(s).channelName, s.setSettings, ]); const users = useGlobalStore((s) => s.syncAwareness, isEqual); @@ -38,23 +47,39 @@ const SyncStatusTag = memo(() => { const theme = useTheme(); - return enableSync ? ( + return ( - - 频道:{channelName} - + + + + {t('sync.title')} + + {t('sync.channel')}: + + + + + + { + refreshConnection(syncEvent); + }} + title={t('sync.actions.settings')} + /> + + { + refreshConnection(syncEvent); + }} + title={t('sync.actions.sync')} + /> + {/*
*/} {/* {*/} @@ -86,12 +111,12 @@ const SyncStatusTag = memo(() => { {user.name || user.id} {user.current && ( - current + {t('sync.awareness.current')} )} - {user.device} · {user.os} · {user.browser} · {user.clientID} + {user.os} · {user.browser} · {user.clientID} @@ -114,31 +139,10 @@ const SyncStatusTag = memo(() => { /> } > - {text[syncStatus]} + {t(`sync.status.${syncStatus}`)} - ) : ( - - 会话数据仅存储于当前使用的浏览器。如果使用不同浏览器打开时,数据不会互相同步。如你需要在多个设备间同步数据,请配置并开启云端同步。 - - - - - } - placement={'bottomLeft'} - > -
- - 本地 - -
-
); }); -export default SyncStatusTag; +export default EnableSync; diff --git a/src/features/SyncStatusInspector/index.tsx b/src/features/SyncStatusInspector/index.tsx new file mode 100644 index 000000000000..fd3575fe702e --- /dev/null +++ b/src/features/SyncStatusInspector/index.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react'; + +import { useGlobalStore } from '@/store/global'; + +import DisableSync from './DisableSync'; +import EnableSync from './EnableSync'; + +interface SyncStatusTagProps { + hiddenEnableGuide?: boolean; +} +const SyncStatusTag = memo(({ hiddenEnableGuide }) => { + const [enableSync] = useGlobalStore((s) => [s.syncEnabled]); + + return enableSync ? : ; +}); + +export default SyncStatusTag; diff --git a/src/hooks/useSyncData.ts b/src/hooks/useSyncData.ts index e44660575782..e3129446aa37 100644 --- a/src/hooks/useSyncData.ts +++ b/src/hooks/useSyncData.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { useChatStore } from '@/store/chat'; import { useGlobalStore } from '@/store/global'; +import { syncSettingsSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; export const useSyncEvent = () => { @@ -35,9 +36,13 @@ export const useSyncEvent = () => { }; export const useEnabledDataSync = () => { - const [userId, useEnabledSync] = useGlobalStore((s) => [s.userId, s.useEnabledSync]); + const [userId, userEnableSync, useEnabledSync] = useGlobalStore((s) => [ + s.userId, + syncSettingsSelectors.enableWebRTC(s), + s.useEnabledSync, + ]); const syncEvent = useSyncEvent(); - useEnabledSync(userId, syncEvent); + useEnabledSync(userEnableSync, userId, syncEvent); }; diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 20382edebed1..fff986506ef2 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -128,6 +128,19 @@ export default { setting: '设置', share: '分享', stop: '停止', + sync: { + actions: { settings: '同步设置', sync: '立即同步' }, + awareness: { + current: '当前设备', + }, + channel: '频道', + status: { + ready: '已连接', + synced: '已同步', + syncing: '同步中', + }, + title: '同步状态', + }, tab: { chat: '会话', market: '发现', diff --git a/src/locales/default/setting.ts b/src/locales/default/setting.ts index 9d146ebba185..130bdf37e99f 100644 --- a/src/locales/default/setting.ts +++ b/src/locales/default/setting.ts @@ -447,12 +447,20 @@ export default { channelName: { desc: 'WebRTC 将使用此名创建同步频道,确保频道名称唯一', placeholder: '请输入同步频道名称', + shuffle: '随机生成', title: '同步频道名称', }, channelPassword: { - desc: '请输入一个同步频道密码', + desc: '添加密码确保频道私密性,只有密码正确时,设备才可加入频道', + placeholder: '请输入同步频道密码', title: '同步频道密码', }, + desc: '实时、点对点的设备数据同步,此模式需确保设备同时在线时才可进行同步', + enabled: { + invalid: '请填写同步频道名称后再开启', + // desc: 'WebRTC 将使用此名创建同步频道,确保频道名称唯一', + title: '开启同步', + }, title: 'WebRTC 同步', }, }, diff --git a/src/store/global/slices/common/action.ts b/src/store/global/slices/common/action.ts index 20421b03e772..6f7c75571182 100644 --- a/src/store/global/slices/common/action.ts +++ b/src/store/global/slices/common/action.ts @@ -18,7 +18,7 @@ import { setNamespace } from '@/utils/storeDebug'; import { switchLang } from '@/utils/switchLang'; import { preferenceSelectors } from '../preference/selectors'; -import { settingsSelectors } from '../settings/selectors'; +import { settingsSelectors, syncSettingsSelectors } from '../settings/selectors'; const n = setNamespace('common'); @@ -32,7 +32,11 @@ export interface CommonAction { updateAvatar: (avatar: string) => Promise; useCheckLatestVersion: () => SWRResponse; useCheckTrace: (shouldFetch: boolean) => SWRResponse; - useEnabledSync: (userId: string | undefined, onEvent: OnSyncEvent) => SWRResponse; + useEnabledSync: ( + userEnableSync: boolean, + userId: string | undefined, + onEvent: OnSyncEvent, + ) => SWRResponse; useFetchServerConfig: () => SWRResponse; useFetchUserConfig: (initServer: boolean) => SWRResponse; } @@ -46,7 +50,7 @@ export const createCommonSlice: StateCreator< CommonAction > = (set, get) => ({ refreshConnection: async (onEvent) => { - const sync = settingsSelectors.syncConfig(get()); + const sync = syncSettingsSelectors.webrtcConfig(get()); if (!sync.channelName) return; await globalService.reconnect({ @@ -107,13 +111,16 @@ export const createCommonSlice: StateCreator< revalidateOnFocus: false, }, ), - useEnabledSync: (userId, onEvent) => + useEnabledSync: (userEnableSync, userId, onEvent) => useSWR( - ['enableSync', userId], + ['enableSync', userEnableSync, userId], async () => { - if (!userId) return false; + // if user don't enable sync or no userId ,don't start sync + if (!userEnableSync || !userId) return false; - const sync = settingsSelectors.syncConfig(get()); + // double-check the sync ability + // if there is no channelName, don't start sync + const sync = syncSettingsSelectors.webrtcConfig(get()); if (!sync.channelName) return false; return globalService.enabledSync({ diff --git a/src/store/global/slices/settings/selectors/index.ts b/src/store/global/slices/settings/selectors/index.ts index 9268ed9068cb..5fbf6450c30b 100644 --- a/src/store/global/slices/settings/selectors/index.ts +++ b/src/store/global/slices/settings/selectors/index.ts @@ -1,2 +1,3 @@ export { modelProviderSelectors } from './modelProvider'; export { settingsSelectors } from './settings'; +export { syncSettingsSelectors } from './sync'; diff --git a/src/store/global/slices/settings/selectors/settings.ts b/src/store/global/slices/settings/selectors/settings.ts index 9ff1f656c937..aa7fa80ea855 100644 --- a/src/store/global/slices/settings/selectors/settings.ts +++ b/src/store/global/slices/settings/selectors/settings.ts @@ -44,8 +44,6 @@ const currentLanguage = (s: GlobalStore) => { const dalleConfig = (s: GlobalStore) => currentSettings(s).tool?.dalle || {}; const isDalleAutoGenerating = (s: GlobalStore) => currentSettings(s).tool?.dalle?.autoGenerate; -const syncConfig = (s: GlobalStore) => currentSettings(s).sync; - export const settingsSelectors = { currentLanguage, currentSettings, @@ -57,5 +55,4 @@ export const settingsSelectors = { exportSettings, isDalleAutoGenerating, password, - syncConfig, }; diff --git a/src/store/global/slices/settings/selectors/sync.ts b/src/store/global/slices/settings/selectors/sync.ts new file mode 100644 index 000000000000..72ea46ea618a --- /dev/null +++ b/src/store/global/slices/settings/selectors/sync.ts @@ -0,0 +1,10 @@ +import { GlobalStore } from '../../../store'; +import { currentSettings } from './settings'; + +const webrtcConfig = (s: GlobalStore) => currentSettings(s).sync.webrtc; +const enableWebRTC = (s: GlobalStore) => webrtcConfig(s).enabled; + +export const syncSettingsSelectors = { + enableWebRTC, + webrtcConfig, +}; diff --git a/src/types/settings/index.ts b/src/types/settings/index.ts index 66c821da0eb5..aa56e5ab7127 100644 --- a/src/types/settings/index.ts +++ b/src/types/settings/index.ts @@ -4,12 +4,14 @@ import type { LobeAgentSession } from '@/types/session'; import { GlobalBaseSettings } from './base'; import { GlobalLLMConfig } from './modelProvider'; +import { GlobalSyncSettings } from './sync'; import { GlobalTTSConfig } from './tts'; export type GlobalDefaultAgent = Pick; export * from './base'; export * from './modelProvider'; +export * from './sync'; export * from './tts'; export interface GlobalTool { @@ -28,12 +30,6 @@ export interface GlobalServerConfig { }; } -export interface GlobalSyncSettings { - channelName?: string; - channelPassword?: string; - signaling?: string; -} - /** * 配置设置 */ diff --git a/src/types/settings/sync.ts b/src/types/settings/sync.ts new file mode 100644 index 000000000000..11073864ff05 --- /dev/null +++ b/src/types/settings/sync.ts @@ -0,0 +1,10 @@ +export interface WebRTCSyncConfig { + channelName?: string; + channelPassword?: string; + enabled: boolean; + signaling?: string; +} +export interface GlobalSyncSettings { + deviceName?: string; + webrtc: WebRTCSyncConfig; +} diff --git a/src/types/sync.ts b/src/types/sync.ts index 457cfb88b40f..8c89b092b28a 100644 --- a/src/types/sync.ts +++ b/src/types/sync.ts @@ -20,7 +20,6 @@ export interface StartDataSyncParams { export interface SyncUserInfo { browser?: string; - device?: string; id: string; isMobile: boolean; name?: string; From 7fbdd686f81c3c36b6621c78616bd14345432c3e Mon Sep 17 00:00:00 2001 From: arvinxx Date: Tue, 19 Mar 2024 01:59:54 +0800 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20device=20info?= =?UTF-8?q?=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SessionListContent/List/SkeletonList.tsx | 1 - src/app/settings/sync/DeviceInfo/Card.tsx | 37 ++++++ .../settings/sync/DeviceInfo/DeviceName.tsx | 63 ++++++++++ src/app/settings/sync/DeviceInfo/index.tsx | 118 ++++++++++++++++++ src/app/settings/sync/PageTitle.tsx | 11 ++ src/app/settings/sync/WebRTC.tsx | 6 +- .../settings/sync/components/SystemIcon.tsx | 16 +++ src/app/settings/sync/page.tsx | 12 +- .../BrowserIcon/components/Brave.tsx | 56 +++++++++ .../BrowserIcon/components/Chrome.tsx | 14 +++ .../BrowserIcon/components/Chromium.tsx | 14 +++ .../BrowserIcon/components/Edge.tsx | 36 ++++++ .../BrowserIcon/components/Firefox.tsx | 38 ++++++ .../BrowserIcon/components/Opera.tsx | 19 +++ .../BrowserIcon/components/Safari.tsx | 23 ++++ .../BrowserIcon/components/Samsung.tsx | 21 ++++ src/components/BrowserIcon/index.tsx | 50 ++++++++ src/components/BrowserIcon/types.ts | 8 ++ .../SyncStatusInspector/DisableSync.tsx | 10 +- .../SyncStatusInspector/EnableSync.tsx | 2 +- src/locales/default/common.ts | 7 ++ src/locales/default/setting.ts | 10 ++ src/services/chat.ts | 8 +- src/store/global/slices/common/action.ts | 65 ++++------ src/store/global/slices/common/selectors.ts | 1 + .../global/slices/settings/selectors/sync.ts | 2 + src/utils/platform.ts | 1 - src/utils/responsive.ts | 21 ++++ 28 files changed, 614 insertions(+), 56 deletions(-) create mode 100644 src/app/settings/sync/DeviceInfo/Card.tsx create mode 100644 src/app/settings/sync/DeviceInfo/DeviceName.tsx create mode 100644 src/app/settings/sync/DeviceInfo/index.tsx create mode 100644 src/app/settings/sync/PageTitle.tsx create mode 100644 src/app/settings/sync/components/SystemIcon.tsx create mode 100644 src/components/BrowserIcon/components/Brave.tsx create mode 100644 src/components/BrowserIcon/components/Chrome.tsx create mode 100644 src/components/BrowserIcon/components/Chromium.tsx create mode 100644 src/components/BrowserIcon/components/Edge.tsx create mode 100644 src/components/BrowserIcon/components/Firefox.tsx create mode 100644 src/components/BrowserIcon/components/Opera.tsx create mode 100644 src/components/BrowserIcon/components/Safari.tsx create mode 100644 src/components/BrowserIcon/components/Samsung.tsx create mode 100644 src/components/BrowserIcon/index.tsx create mode 100644 src/components/BrowserIcon/types.ts diff --git a/src/app/chat/features/SessionListContent/List/SkeletonList.tsx b/src/app/chat/features/SessionListContent/List/SkeletonList.tsx index c4c27b7823ad..b9cdbd181cba 100644 --- a/src/app/chat/features/SessionListContent/List/SkeletonList.tsx +++ b/src/app/chat/features/SessionListContent/List/SkeletonList.tsx @@ -3,7 +3,6 @@ import { createStyles } from 'antd-style'; import { Flexbox } from 'react-layout-kit'; const useStyles = createStyles(({ css }) => ({ - avatar: css``, paragraph: css` height: 12px !important; margin-top: 12px !important; diff --git a/src/app/settings/sync/DeviceInfo/Card.tsx b/src/app/settings/sync/DeviceInfo/Card.tsx new file mode 100644 index 000000000000..927901db3faa --- /dev/null +++ b/src/app/settings/sync/DeviceInfo/Card.tsx @@ -0,0 +1,37 @@ +import { createStyles } from 'antd-style'; +import { ReactNode, memo } from 'react'; +import { Center, Flexbox } from 'react-layout-kit'; + +const useStyles = createStyles(({ css, token }) => ({ + container: css` + background: ${token.colorFillTertiary}; + border-radius: 12px; + `, + icon: css` + width: 40px; + height: 40px; + `, + title: css` + font-size: 20px; + `, +})); + +const Card = memo<{ icon: ReactNode; title: string }>(({ title, icon }) => { + const { styles } = useStyles(); + + return ( + +
{icon}
+ {title} +
+ ); +}); + +export default Card; diff --git a/src/app/settings/sync/DeviceInfo/DeviceName.tsx b/src/app/settings/sync/DeviceInfo/DeviceName.tsx new file mode 100644 index 000000000000..6152e4050e86 --- /dev/null +++ b/src/app/settings/sync/DeviceInfo/DeviceName.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { EditableText } from '@lobehub/ui'; +import { Typography } from 'antd'; +import { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { useGlobalStore } from '@/store/global'; +import { syncSettingsSelectors } from '@/store/global/selectors'; + +const DeviceName = memo(() => { + const { t } = useTranslation('setting'); + + const [deviceName, setSettings] = useGlobalStore((s) => [ + syncSettingsSelectors.deviceName(s), + s.setSettings, + ]); + + const [editing, setEditing] = useState(false); + + const updateDeviceName = (deviceName: string) => { + setSettings({ sync: { deviceName } }); + setEditing(false); + }; + + return ( + +
{t('sync.device.deviceName.title')}
+ + {!deviceName && !editing && ( + { + setEditing(true); + }} + style={{ cursor: 'pointer' }} + > + {t('sync.device.deviceName.hint')} + + )} + { + updateDeviceName(e.target.value); + }} + onChange={(e) => { + updateDeviceName(e); + }} + onEditingChange={setEditing} + placeholder={t('sync.device.deviceName.placeholder')} + value={deviceName} + /> + +
+ ); +}); + +export default DeviceName; diff --git a/src/app/settings/sync/DeviceInfo/index.tsx b/src/app/settings/sync/DeviceInfo/index.tsx new file mode 100644 index 000000000000..db944d9ea430 --- /dev/null +++ b/src/app/settings/sync/DeviceInfo/index.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { Avatar, Icon } from '@lobehub/ui'; +import { Typography } from 'antd'; +import { createStyles } from 'antd-style'; +import { LucideLaptop, LucideSmartphone } from 'lucide-react'; +import { rgba } from 'polished'; +import { PropsWithChildren, memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { MAX_WIDTH } from '@/const/layoutTokens'; + +import { BrowserIcon } from '../../../../components/BrowserIcon'; +import SystemIcon from '../components/SystemIcon'; +import Card from './Card'; +import DeviceName from './DeviceName'; + +const useStyles = createStyles(({ css, cx, responsive, isDarkMode, token, stylish }) => ({ + cards: css` + flex-direction: row; + ${responsive.mobile} { + flex-direction: column; + } + `, + container: css` + border-radius: ${token.borderRadiusLG}px; + `, + content: cx( + stylish.blurStrong, + css` + z-index: 2; + background: ${rgba(token.colorBgContainer, isDarkMode ? 0.7 : 1)}; + border-radius: ${token.borderRadiusLG - 1}px; + `, + ), + glow: cx( + stylish.gradientAnimation, + css` + pointer-events: none; + opacity: 0.5; + background-image: linear-gradient( + -45deg, + ${isDarkMode ? token.geekblue4 : token.geekblue}, + ${isDarkMode ? token.cyan4 : token.cyan} + ); + animation-duration: 10s; + `, + ), +})); + +interface DeviceCardProps { + browser?: string; + isMobile?: boolean; + os?: string; +} +const DeviceCard = memo>(({ browser, isMobile, os }) => { + const { styles, theme } = useStyles(); + const { t } = useTranslation('setting'); + + return ( + + +
+ {t('sync.device.title')} +
+
+ + + } title={os || t('sync.device.unknownOS')} /> + } + title={browser || t('sync.device.unknownBrowser')} + /> + + } + background={theme.geekblue2} + shape={'square'} + /> + } + title={isMobile ? 'Mobile' : 'Desktop'} + /> + + + + + + + ); +}); + +export default DeviceCard; diff --git a/src/app/settings/sync/PageTitle.tsx b/src/app/settings/sync/PageTitle.tsx new file mode 100644 index 000000000000..950dcb1438c9 --- /dev/null +++ b/src/app/settings/sync/PageTitle.tsx @@ -0,0 +1,11 @@ +'use client'; + +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import PageTitle from '@/components/PageTitle'; + +export default memo(() => { + const { t } = useTranslation('setting'); + return ; +}); diff --git a/src/app/settings/sync/WebRTC.tsx b/src/app/settings/sync/WebRTC.tsx index 8d5bdb7e9dc8..ff782781f1d6 100644 --- a/src/app/settings/sync/WebRTC.tsx +++ b/src/app/settings/sync/WebRTC.tsx @@ -1,3 +1,5 @@ +'use client'; + import { SiWebrtc } from '@icons-pack/react-simple-icons'; import { ActionIcon, Form, type ItemGroup, Tooltip } from '@lobehub/ui'; import { Form as AntForm, Input, Switch, Typography } from 'antd'; @@ -15,7 +17,7 @@ import { useSyncSettings } from '../hooks/useSyncSettings'; type SettingItemGroup = ItemGroup; -const Theme = memo(() => { +const WebRTC = memo(() => { const { t } = useTranslation('setting'); const [form] = AntForm.useForm(); @@ -113,4 +115,4 @@ const Theme = memo(() => { ); }); -export default Theme; +export default WebRTC; diff --git a/src/app/settings/sync/components/SystemIcon.tsx b/src/app/settings/sync/components/SystemIcon.tsx new file mode 100644 index 000000000000..af145c3c636c --- /dev/null +++ b/src/app/settings/sync/components/SystemIcon.tsx @@ -0,0 +1,16 @@ +import { SiApple } from '@icons-pack/react-simple-icons'; +import { memo } from 'react'; + +const SystemIcon = memo<{ title?: string }>(({ title }) => { + if (!title) return; + + if (['Mac OS', 'iOS'].includes(title)) { + // TODO: 等 simple icons 修复类型,移除 ignore + // @ts-ignore + return ; + } + + return null; +}); + +export default SystemIcon; diff --git a/src/app/settings/sync/page.tsx b/src/app/settings/sync/page.tsx index cbdca80edd1a..1d298cae58b1 100644 --- a/src/app/settings/sync/page.tsx +++ b/src/app/settings/sync/page.tsx @@ -1,17 +1,17 @@ -'use client'; - import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import PageTitle from '@/components/PageTitle'; +import { gerServerDeviceInfo } from '@/utils/responsive'; +import DeviceCard from './DeviceInfo'; +import PageTitle from './PageTitle'; import WebRTC from './WebRTC'; export default memo(() => { - const { t } = useTranslation('setting'); + const { os, browser, isMobile } = gerServerDeviceInfo(); return ( <> - + + ); diff --git a/src/components/BrowserIcon/components/Brave.tsx b/src/components/BrowserIcon/components/Brave.tsx new file mode 100644 index 000000000000..36c45518db01 --- /dev/null +++ b/src/components/BrowserIcon/components/Brave.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/BrowserIcon/components/Chrome.tsx b/src/components/BrowserIcon/components/Chrome.tsx new file mode 100644 index 000000000000..3d32cfa0ddc0 --- /dev/null +++ b/src/components/BrowserIcon/components/Chrome.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + + ); +}; diff --git a/src/components/BrowserIcon/components/Chromium.tsx b/src/components/BrowserIcon/components/Chromium.tsx new file mode 100644 index 000000000000..85dcf9644073 --- /dev/null +++ b/src/components/BrowserIcon/components/Chromium.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + + ); +}; diff --git a/src/components/BrowserIcon/components/Edge.tsx b/src/components/BrowserIcon/components/Edge.tsx new file mode 100644 index 000000000000..96b374280553 --- /dev/null +++ b/src/components/BrowserIcon/components/Edge.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/BrowserIcon/components/Firefox.tsx b/src/components/BrowserIcon/components/Firefox.tsx new file mode 100644 index 000000000000..7b4b95703117 --- /dev/null +++ b/src/components/BrowserIcon/components/Firefox.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/BrowserIcon/components/Opera.tsx b/src/components/BrowserIcon/components/Opera.tsx new file mode 100644 index 000000000000..cd387469a413 --- /dev/null +++ b/src/components/BrowserIcon/components/Opera.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + ); +}; diff --git a/src/components/BrowserIcon/components/Safari.tsx b/src/components/BrowserIcon/components/Safari.tsx new file mode 100644 index 000000000000..d98ee5e5d1ab --- /dev/null +++ b/src/components/BrowserIcon/components/Safari.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/src/components/BrowserIcon/components/Samsung.tsx b/src/components/BrowserIcon/components/Samsung.tsx new file mode 100644 index 000000000000..41ec8628ef67 --- /dev/null +++ b/src/components/BrowserIcon/components/Samsung.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { SVGComponent } from '../types'; + +export default ({ ...props }: SVGComponent) => { + return ( + + + + + + + + + ); +}; diff --git a/src/components/BrowserIcon/index.tsx b/src/components/BrowserIcon/index.tsx new file mode 100644 index 000000000000..09b6d9cbe2fc --- /dev/null +++ b/src/components/BrowserIcon/index.tsx @@ -0,0 +1,50 @@ +import React, { memo } from 'react'; + +import Brave from './components/Brave'; +import Chrome from './components/Chrome'; +import Chromium from './components/Chromium'; +import Edge from './components/Edge'; +import Firefox from './components/Firefox'; +import Opera from './components/Opera'; +import Safari from './components/Safari'; +import Samsung from './components/Samsung'; + +const lastVersion = { + 'Brave': Brave, + 'Chrome': Chrome, + 'Chromium': Chromium, + 'Edge': Edge, + 'Firefox': Firefox, + 'Mobile Safari': Safari, + 'Opera': Opera, + 'Safari': Safari, + 'Samsung': Samsung, +}; + +export type Browsers = keyof typeof lastVersion; + +interface BrowserIconProps { + browser: string; + className?: string; + size: number | string; + style?: React.CSSProperties; +} + +export const BrowserIcon = memo(({ browser, className, style, size }) => { + const Component = lastVersion[browser as Browsers]; + + if (!Component) return null; + + return ( + + ); +}); diff --git a/src/components/BrowserIcon/types.ts b/src/components/BrowserIcon/types.ts new file mode 100644 index 000000000000..ed08f2b3327e --- /dev/null +++ b/src/components/BrowserIcon/types.ts @@ -0,0 +1,8 @@ +import { CSSProperties } from 'react'; + +export interface SVGComponent { + className?: string; + height?: number | string; + style?: CSSProperties; + width?: number | string; +} diff --git a/src/features/SyncStatusInspector/DisableSync.tsx b/src/features/SyncStatusInspector/DisableSync.tsx index 42d7731dcf92..3774d82fe9e7 100644 --- a/src/features/SyncStatusInspector/DisableSync.tsx +++ b/src/features/SyncStatusInspector/DisableSync.tsx @@ -3,17 +3,19 @@ import { Badge, Button, Popover } from 'antd'; import { LucideCloudCog, LucideCloudy } from 'lucide-react'; import Link from 'next/link'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; interface DisableSyncProps { noPopover?: boolean; } const DisableSync = memo(({ noPopover }) => { + const { t } = useTranslation('common'); const tag = (
- 同步未开启 + {t('sync.status.disabled')}
); @@ -25,10 +27,10 @@ const DisableSync = memo(({ noPopover }) => { arrow={false} content={ - 当前会话数据仅存储于此浏览器中。如果你需要在多个设备间同步数据,请配置并开启云端同步。 + {t('sync.disabled.desc')} @@ -37,7 +39,7 @@ const DisableSync = memo(({ noPopover }) => { title={ - 数据同步未开启 + {t('sync.disabled.title')} } > diff --git a/src/features/SyncStatusInspector/EnableSync.tsx b/src/features/SyncStatusInspector/EnableSync.tsx index b715fe7fc71d..61468308c32a 100644 --- a/src/features/SyncStatusInspector/EnableSync.tsx +++ b/src/features/SyncStatusInspector/EnableSync.tsx @@ -116,7 +116,7 @@ const EnableSync = memo(() => { )}
- {user.os} · {user.browser} · {user.clientID} + {user.os} · {user.browser} diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index fff986506ef2..21b5a8376ba9 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -134,11 +134,18 @@ export default { current: '当前设备', }, channel: '频道', + disabled: { + actions: { settings: '配置云端同步' }, + desc: '当前会话数据仅存储于此浏览器中。如果你需要在多个设备间同步数据,请配置并开启云端同步。', + title: '数据同步未开启', + }, status: { + disabled: '同步未开启', ready: '已连接', synced: '已同步', syncing: '同步中', }, + title: '同步状态', }, tab: { diff --git a/src/locales/default/setting.ts b/src/locales/default/setting.ts index 130bdf37e99f..b00b1fd9a2ae 100644 --- a/src/locales/default/setting.ts +++ b/src/locales/default/setting.ts @@ -443,6 +443,16 @@ export default { tooltips: '分享到助手市场', }, sync: { + device: { + deviceName: { + hint: '添加名称以便于识别', + placeholder: '请输入设备名称', + title: '设备名称', + }, + title: '设备信息', + unknownBrowser: '未知浏览器', + unknownOS: '未知系统', + }, webrtc: { channelName: { desc: 'WebRTC 将使用此名创建同步频道,确保频道名称唯一', diff --git a/src/services/chat.ts b/src/services/chat.ts index ef2d258f32e7..ddce4b6a7869 100644 --- a/src/services/chat.ts +++ b/src/services/chat.ts @@ -7,7 +7,11 @@ import { TracePayload, TraceTagMap } from '@/const/trace'; import { ModelProvider } from '@/libs/agent-runtime'; import { filesSelectors, useFileStore } from '@/store/file'; import { useGlobalStore } from '@/store/global'; -import { modelProviderSelectors, preferenceSelectors } from '@/store/global/selectors'; +import { + commonSelectors, + modelProviderSelectors, + preferenceSelectors, +} from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; import { agentSelectors } from '@/store/session/selectors'; import { useToolStore } from '@/store/tool'; @@ -310,7 +314,7 @@ class ChatService { ...trace, enabled: true, tags: [tag, ...(trace?.tags || []), ...tags].filter(Boolean) as string[], - userId: useGlobalStore.getState().userId, + userId: commonSelectors.userId(useGlobalStore.getState()), }; } } diff --git a/src/store/global/slices/common/action.ts b/src/store/global/slices/common/action.ts index 6f7c75571182..8e0dfcbf9dec 100644 --- a/src/store/global/slices/common/action.ts +++ b/src/store/global/slices/common/action.ts @@ -19,6 +19,7 @@ import { switchLang } from '@/utils/switchLang'; import { preferenceSelectors } from '../preference/selectors'; import { settingsSelectors, syncSettingsSelectors } from '../settings/selectors'; +import { commonSelectors } from './selectors'; const n = setNamespace('common'); @@ -29,6 +30,7 @@ export interface CommonAction { refreshConnection: (onEvent: OnSyncEvent) => Promise; refreshUserConfig: () => Promise; switchBackToChat: (sessionId?: string) => void; + triggerEnableSync: (userId: string, onEvent: OnSyncEvent) => Promise; updateAvatar: (avatar: string) => Promise; useCheckLatestVersion: () => SWRResponse; useCheckTrace: (shouldFetch: boolean) => SWRResponse; @@ -50,10 +52,29 @@ export const createCommonSlice: StateCreator< CommonAction > = (set, get) => ({ refreshConnection: async (onEvent) => { + const userId = commonSelectors.userId(get()); + + if (!userId) return; + + await get().triggerEnableSync(userId, onEvent); + }, + + refreshUserConfig: async () => { + await mutate([USER_CONFIG_FETCH_KEY, true]); + }, + + switchBackToChat: (sessionId) => { + get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile)); + }, + triggerEnableSync: async (userId: string, onEvent: OnSyncEvent) => { + // double-check the sync ability + // if there is no channelName, don't start sync const sync = syncSettingsSelectors.webrtcConfig(get()); - if (!sync.channelName) return; + if (!sync.channelName) return false; - await globalService.reconnect({ + const name = syncSettingsSelectors.deviceName(get()); + + return globalService.enabledSync({ channel: { name: sync.channelName, password: sync.channelPassword, @@ -66,21 +87,9 @@ export const createCommonSlice: StateCreator< set({ syncStatus: status }); }, signaling: sync.signaling, - user: { - // name: userId, - id: get().userId!, - ...browserInfo, - }, + user: { id: userId, name: name, ...browserInfo }, }); }, - - refreshUserConfig: async () => { - await mutate([USER_CONFIG_FETCH_KEY, true]); - }, - - switchBackToChat: (sessionId) => { - get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile)); - }, updateAvatar: async (avatar) => { await userService.updateAvatar(avatar); await get().refreshUserConfig(); @@ -111,6 +120,7 @@ export const createCommonSlice: StateCreator< revalidateOnFocus: false, }, ), + useEnabledSync: (userEnableSync, userId, onEvent) => useSWR( ['enableSync', userEnableSync, userId], @@ -118,30 +128,7 @@ export const createCommonSlice: StateCreator< // if user don't enable sync or no userId ,don't start sync if (!userEnableSync || !userId) return false; - // double-check the sync ability - // if there is no channelName, don't start sync - const sync = syncSettingsSelectors.webrtcConfig(get()); - if (!sync.channelName) return false; - - return globalService.enabledSync({ - channel: { - name: sync.channelName, - password: sync.channelPassword, - }, - onAwarenessChange(state) { - set({ syncAwareness: state }); - }, - onSyncEvent: onEvent, - onSyncStatusChange: (status) => { - set({ syncStatus: status }); - }, - signaling: sync.signaling, - user: { - // name: userId, - id: userId, - ...browserInfo, - }, - }); + return get().triggerEnableSync(userId, onEvent); }, { onSuccess: (syncEnabled) => { diff --git a/src/store/global/slices/common/selectors.ts b/src/store/global/slices/common/selectors.ts index 154b0370c2fc..a1937257a76b 100644 --- a/src/store/global/slices/common/selectors.ts +++ b/src/store/global/slices/common/selectors.ts @@ -4,4 +4,5 @@ export const commonSelectors = { enabledOAuthSSO: (s: GlobalStore) => s.serverConfig.enabledOAuthSSO, enabledTelemetryChat: (s: GlobalStore) => s.serverConfig.telemetry.langfuse || false, userAvatar: (s: GlobalStore) => s.avatar || '', + userId: (s: GlobalStore) => s.userId, }; diff --git a/src/store/global/slices/settings/selectors/sync.ts b/src/store/global/slices/settings/selectors/sync.ts index 72ea46ea618a..71d3288a5183 100644 --- a/src/store/global/slices/settings/selectors/sync.ts +++ b/src/store/global/slices/settings/selectors/sync.ts @@ -3,8 +3,10 @@ import { currentSettings } from './settings'; const webrtcConfig = (s: GlobalStore) => currentSettings(s).sync.webrtc; const enableWebRTC = (s: GlobalStore) => webrtcConfig(s).enabled; +const deviceName = (s: GlobalStore) => currentSettings(s).sync.deviceName; export const syncSettingsSelectors = { + deviceName, enableWebRTC, webrtcConfig, }; diff --git a/src/utils/platform.ts b/src/utils/platform.ts index 0a5eb8704489..7e78ba2fcf32 100644 --- a/src/utils/platform.ts +++ b/src/utils/platform.ts @@ -17,7 +17,6 @@ export const getBrowser = () => { export const browserInfo = { browser: getBrowser(), - device: getParser().getDevice().vendor, isMobile: getParser().getDevice().type === 'mobile', os: getParser().getOS().name, }; diff --git a/src/utils/responsive.ts b/src/utils/responsive.ts index 0434bd3bc3f4..c2e5ef38ba99 100644 --- a/src/utils/responsive.ts +++ b/src/utils/responsive.ts @@ -17,3 +17,24 @@ export const isMobileDevice = () => { return device.type === 'mobile'; }; + +/** + * check mobile device in server + */ +export const gerServerDeviceInfo = () => { + if (typeof process === 'undefined') { + throw new Error('[Server method] you are importing a server-only module outside of server'); + } + + const { get } = headers(); + const ua = get('user-agent'); + + // console.debug(ua); + const parser = new UAParser(ua || ''); + + return { + browser: parser.getBrowser().name, + isMobile: isMobileDevice(), + os: parser.getOS().name, + }; +}; From 6a6ae5641b240a925c1f3d8e68c488a4c377709d Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 20 Mar 2024 00:22:19 +0800 Subject: [PATCH 07/18] =?UTF-8?q?=E2=9C=A8=20feat:=20finish=20sync=20setti?= =?UTF-8?q?ngs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/sync/DeviceInfo/DeviceName.tsx | 19 ++- src/app/settings/sync/DeviceInfo/index.tsx | 31 +--- src/app/settings/sync/WebRTC.tsx | 24 +-- .../SyncStatusInspector/DisableSync.tsx | 36 +++- .../SyncStatusInspector/EnableSync.tsx | 154 ++++++++++-------- src/features/SyncStatusInspector/index.tsx | 9 +- src/locales/default/common.ts | 5 +- src/locales/default/setting.ts | 2 +- .../global/slices/settings/selectors/sync.ts | 2 + 9 files changed, 163 insertions(+), 119 deletions(-) diff --git a/src/app/settings/sync/DeviceInfo/DeviceName.tsx b/src/app/settings/sync/DeviceInfo/DeviceName.tsx index 6152e4050e86..fd85acb67261 100644 --- a/src/app/settings/sync/DeviceInfo/DeviceName.tsx +++ b/src/app/settings/sync/DeviceInfo/DeviceName.tsx @@ -25,14 +25,14 @@ const DeviceName = memo(() => { }; return ( - -
{t('sync.device.deviceName.title')}
- + + {!deviceName && !editing && ( { @@ -53,6 +53,9 @@ const DeviceName = memo(() => { }} onEditingChange={setEditing} placeholder={t('sync.device.deviceName.placeholder')} + size={'large'} + style={{ maxWidth: 200 }} + type={'block'} value={deviceName} /> diff --git a/src/app/settings/sync/DeviceInfo/index.tsx b/src/app/settings/sync/DeviceInfo/index.tsx index db944d9ea430..00f7d6b8ae88 100644 --- a/src/app/settings/sync/DeviceInfo/index.tsx +++ b/src/app/settings/sync/DeviceInfo/index.tsx @@ -1,17 +1,15 @@ 'use client'; -import { Avatar, Icon } from '@lobehub/ui'; import { Typography } from 'antd'; import { createStyles } from 'antd-style'; -import { LucideLaptop, LucideSmartphone } from 'lucide-react'; import { rgba } from 'polished'; -import { PropsWithChildren, memo } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { BrowserIcon } from '@/components/BrowserIcon'; import { MAX_WIDTH } from '@/const/layoutTokens'; -import { BrowserIcon } from '../../../../components/BrowserIcon'; import SystemIcon from '../components/SystemIcon'; import Card from './Card'; import DeviceName from './DeviceName'; @@ -51,11 +49,11 @@ const useStyles = createStyles(({ css, cx, responsive, isDarkMode, token, stylis interface DeviceCardProps { browser?: string; - isMobile?: boolean; os?: string; } -const DeviceCard = memo>(({ browser, isMobile, os }) => { - const { styles, theme } = useStyles(); + +const DeviceCard = memo(({ browser, os }) => { + const { styles } = useStyles(); const { t } = useTranslation('setting'); return ( @@ -79,31 +77,14 @@ const DeviceCard = memo>(({ browser, isMobile horizontal padding={12} > + } title={os || t('sync.device.unknownOS')} /> } title={browser || t('sync.device.unknownBrowser')} /> - - } - background={theme.geekblue2} - shape={'square'} - /> - } - title={isMobile ? 'Mobile' : 'Desktop'} - /> - - { label: t('sync.webrtc.channelName.title'), name: ['sync', 'webrtc', 'channelName'], }, - { - children: ( - - ), - desc: t('sync.webrtc.channelPassword.desc'), - label: t('sync.webrtc.channelPassword.title'), - name: ['sync', 'webrtc', 'channelPassword'], - }, + // { + // children: ( + // + // ), + // desc: t('sync.webrtc.channelPassword.desc'), + // label: t('sync.webrtc.channelPassword.title'), + // name: ['sync', 'webrtc', 'channelPassword'], + // }, { children: !channelName ? ( @@ -87,7 +87,7 @@ const WebRTC = memo(() => { e.stopPropagation(); }} > - +
), title: ( diff --git a/src/features/SyncStatusInspector/DisableSync.tsx b/src/features/SyncStatusInspector/DisableSync.tsx index 3774d82fe9e7..ca20fc456f46 100644 --- a/src/features/SyncStatusInspector/DisableSync.tsx +++ b/src/features/SyncStatusInspector/DisableSync.tsx @@ -6,11 +6,24 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { useGlobalStore } from '@/store/global'; +import { syncSettingsSelectors } from '@/store/global/slices/settings/selectors'; + interface DisableSyncProps { noPopover?: boolean; } + const DisableSync = memo(({ noPopover }) => { const { t } = useTranslation('common'); + const [haveConfig, setSettings] = useGlobalStore((s) => [ + !!syncSettingsSelectors.webrtcConfig(s).channelName, + s.setSettings, + ]); + + const enableSync = () => { + setSettings({ sync: { webrtc: { enabled: true } } }); + }; + const tag = (
@@ -28,11 +41,24 @@ const DisableSync = memo(({ noPopover }) => { content={ {t('sync.disabled.desc')} - - - + {haveConfig ? ( + + + + + + + ) : ( + + + + )} } placement={'bottomLeft'} diff --git a/src/features/SyncStatusInspector/EnableSync.tsx b/src/features/SyncStatusInspector/EnableSync.tsx index 61468308c32a..14eb0581f96e 100644 --- a/src/features/SyncStatusInspector/EnableSync.tsx +++ b/src/features/SyncStatusInspector/EnableSync.tsx @@ -1,14 +1,14 @@ import { ActionIcon, Avatar, Icon } from '@lobehub/ui'; -import { Tag as ATag, Popover, Typography } from 'antd'; -import { useTheme } from 'antd-style'; +import { Divider, Popover, Switch, Tag, Typography } from 'antd'; +import { createStyles } from 'antd-style'; import isEqual from 'fast-deep-equal'; import { - LucideCloudCog, LucideCloudy, LucideLaptop, LucideRefreshCw, LucideRouter, LucideSmartphone, + SettingsIcon, } from 'lucide-react'; import Link from 'next/link'; import { memo } from 'react'; @@ -22,75 +22,65 @@ import { pathString } from '@/utils/url'; const { Text } = Typography; -const EllipsisChannelName = memo<{ text: string }>(({ text }) => { - const start = text.slice(0, 16); - const suffix = text.slice(-4).trim(); +const useStyles = createStyles(({ css, token, prefixCls }) => ({ + text: css` + max-width: 100%; + color: ${token.colorTextTertiary}; + .${prefixCls}-typography-copy { + color: ${token.colorTextTertiary}; + } + `, + title: css` + color: ${token.colorTextTertiary}; + `, +})); - return ( - - {start}... - - ); -}); +interface EnableSyncProps { + hiddenActions?: boolean; +} -const EnableSync = memo(() => { +const EnableSync = memo(({ hiddenActions }) => { const { t } = useTranslation('common'); - const [syncStatus, isSyncing, channelName] = useGlobalStore((s) => [ - s.syncStatus, - s.syncStatus === 'syncing', - syncSettingsSelectors.webrtcConfig(s).channelName, - s.setSettings, - ]); + + const { styles, theme } = useStyles(); + const [syncStatus, isSyncing, channelName, enableWebRTC, setSettings, refreshConnection] = + useGlobalStore((s) => [ + s.syncStatus, + s.syncStatus === 'syncing', + syncSettingsSelectors.webrtcChannelName(s), + syncSettingsSelectors.enableWebRTC(s), + s.setSettings, + s.refreshConnection, + ]); + const users = useGlobalStore((s) => s.syncAwareness, isEqual); - const refreshConnection = useGlobalStore((s) => s.refreshConnection); + const syncEvent = useSyncEvent(); - const theme = useTheme(); + const switchSync = (enabled: boolean) => { + setSettings({ sync: { webrtc: { enabled } } }); + }; return ( - - - {t('sync.title')} - - {t('sync.channel')}: - - + + + + {t('sync.channel')} + + {channelName} + - - - { - refreshConnection(syncEvent); - }} - title={t('sync.actions.settings')} - /> - - { - refreshConnection(syncEvent); - }} - title={t('sync.actions.sync')} - /> - - {/*
*/} - {/* {*/} - {/* setSettings({ sync: { channelName: e.target.value } });*/} - {/* }}*/} - {/* size={'small'}*/} - {/* value={channelName}*/} - {/* variant={'borderless'}*/} - {/* />*/} - {/*
*/}
+ {users.map((user) => ( @@ -110,9 +100,20 @@ const EnableSync = memo(() => { {user.name || user.id} {user.current && ( - - {t('sync.awareness.current')} - + + + {t('sync.awareness.current')} + + { + refreshConnection(syncEvent); + }} + size={'small'} + title={t('sync.actions.sync')} + /> + )} @@ -124,10 +125,33 @@ const EnableSync = memo(() => { } - open placement={'bottomLeft'} + title={ + + + + {t('sync.title')} + {!hiddenActions && ( + + )} + + {!hiddenActions && ( + + { + refreshConnection(syncEvent); + }} + // size={'small'} + title={t('sync.actions.settings')} + /> + + )} + + } > - { } > {t(`sync.status.${syncStatus}`)} - +
); }); diff --git a/src/features/SyncStatusInspector/index.tsx b/src/features/SyncStatusInspector/index.tsx index fd3575fe702e..3f8b9a7cc85d 100644 --- a/src/features/SyncStatusInspector/index.tsx +++ b/src/features/SyncStatusInspector/index.tsx @@ -6,12 +6,17 @@ import DisableSync from './DisableSync'; import EnableSync from './EnableSync'; interface SyncStatusTagProps { + hiddenActions?: boolean; hiddenEnableGuide?: boolean; } -const SyncStatusTag = memo(({ hiddenEnableGuide }) => { +const SyncStatusTag = memo(({ hiddenActions, hiddenEnableGuide }) => { const [enableSync] = useGlobalStore((s) => [s.syncEnabled]); - return enableSync ? : ; + return enableSync ? ( + + ) : ( + + ); }); export default SyncStatusTag; diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 21b5a8376ba9..1f81a0e8f6b4 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -135,10 +135,13 @@ export default { }, channel: '频道', disabled: { - actions: { settings: '配置云端同步' }, + actions: { enable: '开启云端同步', settings: '配置同步参数' }, desc: '当前会话数据仅存储于此浏览器中。如果你需要在多个设备间同步数据,请配置并开启云端同步。', title: '数据同步未开启', }, + enabled: { + title: '数据同步', + }, status: { disabled: '同步未开启', ready: '已连接', diff --git a/src/locales/default/setting.ts b/src/locales/default/setting.ts index b00b1fd9a2ae..465cc19bd1ba 100644 --- a/src/locales/default/setting.ts +++ b/src/locales/default/setting.ts @@ -465,7 +465,7 @@ export default { placeholder: '请输入同步频道密码', title: '同步频道密码', }, - desc: '实时、点对点的设备数据同步,此模式需确保设备同时在线时才可进行同步', + desc: '实时、点对点的数据通信,需设备同时在线才可同步', enabled: { invalid: '请填写同步频道名称后再开启', // desc: 'WebRTC 将使用此名创建同步频道,确保频道名称唯一', diff --git a/src/store/global/slices/settings/selectors/sync.ts b/src/store/global/slices/settings/selectors/sync.ts index 71d3288a5183..aae8e1f464a3 100644 --- a/src/store/global/slices/settings/selectors/sync.ts +++ b/src/store/global/slices/settings/selectors/sync.ts @@ -2,11 +2,13 @@ import { GlobalStore } from '../../../store'; import { currentSettings } from './settings'; const webrtcConfig = (s: GlobalStore) => currentSettings(s).sync.webrtc; +const webrtcChannelName = (s: GlobalStore) => webrtcConfig(s).channelName; const enableWebRTC = (s: GlobalStore) => webrtcConfig(s).enabled; const deviceName = (s: GlobalStore) => currentSettings(s).sync.deviceName; export const syncSettingsSelectors = { deviceName, enableWebRTC, + webrtcChannelName, webrtcConfig, }; From e89397fc9306d43fce870dda28b98f08dd3d29a5 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 20 Mar 2024 10:01:18 +0000 Subject: [PATCH 08/18] =?UTF-8?q?=E2=9C=A8=20feat:=20finish=20sync=20setti?= =?UTF-8?q?ngs=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/settings/sync/WebRTC.tsx | 22 +++--- src/app/settings/sync/page.tsx | 4 +- src/database/core/sync.ts | 73 +++++++++++++++---- .../SyncStatusInspector/EnableSync.tsx | 20 ++--- .../SyncStatusInspector/EnableTag.tsx | 66 +++++++++++++++++ src/locales/default/common.ts | 11 ++- src/services/config.ts | 2 +- .../global/slices/common/initialState.ts | 2 +- src/types/sync.ts | 11 ++- 9 files changed, 161 insertions(+), 50 deletions(-) create mode 100644 src/features/SyncStatusInspector/EnableTag.tsx diff --git a/src/app/settings/sync/WebRTC.tsx b/src/app/settings/sync/WebRTC.tsx index 95c58575a2c1..69a60afa4827 100644 --- a/src/app/settings/sync/WebRTC.tsx +++ b/src/app/settings/sync/WebRTC.tsx @@ -55,17 +55,17 @@ const WebRTC = memo(() => { label: t('sync.webrtc.channelName.title'), name: ['sync', 'webrtc', 'channelName'], }, - // { - // children: ( - // - // ), - // desc: t('sync.webrtc.channelPassword.desc'), - // label: t('sync.webrtc.channelPassword.title'), - // name: ['sync', 'webrtc', 'channelPassword'], - // }, + { + children: ( + + ), + desc: t('sync.webrtc.channelPassword.desc'), + label: t('sync.webrtc.channelPassword.title'), + name: ['sync', 'webrtc', 'channelPassword'], + }, { children: !channelName ? ( diff --git a/src/app/settings/sync/page.tsx b/src/app/settings/sync/page.tsx index 1d298cae58b1..b5f6320c613b 100644 --- a/src/app/settings/sync/page.tsx +++ b/src/app/settings/sync/page.tsx @@ -7,11 +7,11 @@ import PageTitle from './PageTitle'; import WebRTC from './WebRTC'; export default memo(() => { - const { os, browser, isMobile } = gerServerDeviceInfo(); + const { os, browser } = gerServerDeviceInfo(); return ( <> - + ); diff --git a/src/database/core/sync.ts b/src/database/core/sync.ts index 7f333fdc7b0f..aaba88a8dae4 100644 --- a/src/database/core/sync.ts +++ b/src/database/core/sync.ts @@ -6,11 +6,27 @@ import { OnAwarenessChange, OnSyncEvent, OnSyncStatusChange, + PeerSyncStatus, StartDataSyncParams, } from '@/types/sync'; import { LobeDBSchemaMap, LocalDBInstance } from './db'; +interface IWebsocketClient { + binaryType: 'arraybuffer' | 'blob' | null; + connect(): void; + connected: boolean; + connecting: boolean; + destroy(): void; + disconnect(): void; + lastMessageReceived: number; + send(message: any): void; + shouldConnect: boolean; + unsuccessfulReconnects: number; + url: string; + ws: WebSocket; +} + declare global { interface Window { __ONLY_USE_FOR_CLEANUP_IN_DEV?: WebrtcProvider | null; @@ -24,6 +40,8 @@ class DataSync { private syncParams!: StartDataSyncParams; private onAwarenessChange!: OnAwarenessChange; + private waitForConnecting: any; + transact(fn: (transaction: Transaction) => unknown) { this._ydoc?.transact(fn); } @@ -54,11 +72,14 @@ class DataSync { onAwarenessChange, signaling = 'wss://y-webrtc-signaling.lobehub.com', } = params; + // ====== 1. init yjs doc ====== // + await this.initYDoc(); console.log('[YJS] start to listen sync event...'); this.initYjsObserve(onSyncEvent, onSyncStatusChange); + // ====== 2. init webrtc provider ====== // console.log(`[WebRTC] init provider... room: ${channel.name}`); const { WebrtcProvider } = await import('y-webrtc'); @@ -68,48 +89,74 @@ class DataSync { signaling: [signaling], }); - // 只在开发时解决全局实例缓存问题 + // when fast refresh in dev, the provider will be cached in window + // so we need to clean it in destory if (process.env.NODE_ENV === 'development') { window.__ONLY_USE_FOR_CLEANUP_IN_DEV = this.provider; } - const provider = this.provider; - console.log(`[WebRTC] provider init success`); + // ====== 3. check signaling server connection ====== // + // 当本地设备正确连接到 WebRTC Provider 后,触发 status 事件 // 当开始连接,则开始监听事件 - provider.on('status', async ({ connected }) => { - console.log('[WebRTC] peer connected status:', connected); + this.provider.on('status', async ({ connected }) => { + console.log('[WebRTC] peer status:', connected); if (connected) { // this.initObserve(onSyncEvent, onSyncStatusChange); - onSyncStatusChange?.('ready'); + onSyncStatusChange?.(PeerSyncStatus.Connecting); } }); + // check the connection with signaling server + let connectionCheckCount = 0; + + this.waitForConnecting = setInterval(() => { + const signalingConnection: IWebsocketClient = this.provider!.signalingConns[0]; + + if (signalingConnection.connected) { + onSyncStatusChange?.(PeerSyncStatus.Ready); + clearInterval(this.waitForConnecting); + return; + } + + connectionCheckCount += 1; + + // check for 5 times, or make it failed + if (connectionCheckCount > 5) { + onSyncStatusChange?.(PeerSyncStatus.Unconnected); + clearInterval(this.waitForConnecting); + } + }, 2000); + + // ====== 4. handle data sync ====== // + // 当各方的数据均完成同步后,YJS 对象之间的数据已经一致时,触发 synced 事件 - provider.on('synced', async ({ synced }) => { + this.provider.on('synced', async ({ synced }) => { console.log('[WebRTC] peer sync status:', synced); if (synced) { console.groupCollapsed('[WebRTC] start to init yjs data...'); - onSyncStatusChange?.('syncing'); + onSyncStatusChange?.(PeerSyncStatus.Syncing); await this.initSync(); - onSyncStatusChange?.('synced'); + onSyncStatusChange?.(PeerSyncStatus.Synced); console.groupEnd(); console.log('[WebRTC] yjs data init success'); } else { console.log('[WebRTC] data not sync, try to reconnect in 1s...'); // await this.reconnect(params); setTimeout(() => { - onSyncStatusChange?.('syncing'); + onSyncStatusChange?.(PeerSyncStatus.Syncing); this.reconnect(params); }, 1000); } }); + // ====== 5. handle awareness ====== // + this.initAwareness({ onAwarenessChange, user }); - return provider; + return this.provider; }; manualSync = async () => { @@ -188,7 +235,7 @@ class DataSync { // 每次有变更时,都先清除之前的定时器(如果有的话),然后设置新的定时器 clearTimeout(debounceTimer); - onSyncStatusChange('syncing'); + onSyncStatusChange(PeerSyncStatus.Syncing); console.log(`[YJS] observe ${tableKey} changes:`, event.keysChanged.size); const pools = Array.from(event.keys).map(async ([id, payload]) => { @@ -219,7 +266,7 @@ class DataSync { // 设置定时器,2000ms 后更新状态为'synced' debounceTimer = setTimeout(() => { - onSyncStatusChange('synced'); + onSyncStatusChange(PeerSyncStatus.Synced); }, 2000); }); }; diff --git a/src/features/SyncStatusInspector/EnableSync.tsx b/src/features/SyncStatusInspector/EnableSync.tsx index 14eb0581f96e..e56c6dd01514 100644 --- a/src/features/SyncStatusInspector/EnableSync.tsx +++ b/src/features/SyncStatusInspector/EnableSync.tsx @@ -6,7 +6,6 @@ import { LucideCloudy, LucideLaptop, LucideRefreshCw, - LucideRouter, LucideSmartphone, SettingsIcon, } from 'lucide-react'; @@ -20,6 +19,8 @@ import { useGlobalStore } from '@/store/global'; import { syncSettingsSelectors } from '@/store/global/selectors'; import { pathString } from '@/utils/url'; +import EnableTag from './EnableTag'; + const { Text } = Typography; const useStyles = createStyles(({ css, token, prefixCls }) => ({ @@ -151,20 +152,9 @@ const EnableSync = memo(({ hiddenActions }) => { } > - - } - > - {t(`sync.status.${syncStatus}`)} - +
+ +
); }); diff --git a/src/features/SyncStatusInspector/EnableTag.tsx b/src/features/SyncStatusInspector/EnableTag.tsx new file mode 100644 index 000000000000..9602c2d60911 --- /dev/null +++ b/src/features/SyncStatusInspector/EnableTag.tsx @@ -0,0 +1,66 @@ +import { Icon, Tooltip } from '@lobehub/ui'; +import { Badge, Tag } from 'antd'; +import { LucideCloudy, LucideRefreshCw, LucideRouter, LucideWifiOff } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { PeerSyncStatus } from '@/types/sync'; + +const EnableTag = memo<{ isSyncing: boolean; status: PeerSyncStatus }>(({ status, isSyncing }) => { + const { t } = useTranslation('common'); + + switch (status) { + case PeerSyncStatus.Connecting: { + return ( + } + style={{ display: 'flex', gap: 4 }} + > + {t('sync.status.connecting')} + + ); + } + + case PeerSyncStatus.Synced: { + return ( + }> + {t('sync.status.synced')} + + ); + } + + case PeerSyncStatus.Ready: { + return ( + }> + {t('sync.status.ready')} + + ); + } + + case PeerSyncStatus.Syncing: { + return ( + } + > + {t('sync.status.syncing')} + + ); + } + + case PeerSyncStatus.Unconnected: { + return ( + + }> + {t('sync.status.unconnected')} + + + ); + } + } +}); + +export default EnableTag; diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 1f81a0e8f6b4..4bfa4a86993a 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -10,17 +10,14 @@ export default { }, about: '关于', advanceSettings: '高级设置', - agentMaxToken: '会话最大长度', - agentModel: '模型', - agentProfile: '助手信息', + appInitializing: 'LobeChat 启动中,请耐心等待...', - archive: '归档', + autoGenerate: '自动补全', autoGenerateTooltip: '基于提示词自动补全助手描述', cancel: '取消', changelog: '更新日志', close: '关闭', - confirmRemoveSessionItemAlert: '即将删除该助手,删除后该将无法找回,请确认你的操作', copy: '复制', copyFail: '复制失败', copySuccess: '复制成功', @@ -143,13 +140,15 @@ export default { title: '数据同步', }, status: { + connecting: '连接中', disabled: '同步未开启', ready: '已连接', synced: '已同步', syncing: '同步中', + unconnected: '连接失败', }, - title: '同步状态', + unconnected: { tip: '信令服务器连接失败,将无法建立点对点通信频道,请检查网络后重试' }, }, tab: { chat: '会话', diff --git a/src/services/config.ts b/src/services/config.ts index c03368c0442a..b3f00fcc3fce 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -42,7 +42,7 @@ class ConfigService { return topicService.batchCreateTopics(topics); }; importSessionGroups = async (sessionGroups: SessionGroupItem[]) => { - return sessionService.batchCreateSessionGroups(sessionGroups); + return sessionService.batchCreateSessionGroups(sessionGroups || []); }; importConfigState = async (config: ConfigFile): Promise => { diff --git a/src/store/global/slices/common/initialState.ts b/src/store/global/slices/common/initialState.ts index 72c31bf20e0e..3ee147c7bfb4 100644 --- a/src/store/global/slices/common/initialState.ts +++ b/src/store/global/slices/common/initialState.ts @@ -38,5 +38,5 @@ export const initialCommonState: GlobalCommonState = { sidebarKey: SidebarTabKey.Chat, syncAwareness: [], syncEnabled: false, - syncStatus: 'ready', + syncStatus: PeerSyncStatus.Disabled, }; diff --git a/src/types/sync.ts b/src/types/sync.ts index 8c89b092b28a..a07297f156ca 100644 --- a/src/types/sync.ts +++ b/src/types/sync.ts @@ -4,7 +4,16 @@ export type OnSyncEvent = (tableKey: keyof LobeDBSchemaMap) => void; export type OnSyncStatusChange = (status: PeerSyncStatus) => void; export type OnAwarenessChange = (state: SyncAwarenessState[]) => void; -export type PeerSyncStatus = 'syncing' | 'synced' | 'ready'; +// export type PeerSyncStatus = 'syncing' | 'synced' | 'ready' | 'unconnected'; + +export enum PeerSyncStatus { + Connecting = 'connecting', + Disabled = 'disabled', + Ready = 'ready', + Synced = 'synced', + Syncing = 'syncing', + Unconnected = 'unconnected', +} export interface StartDataSyncParams { channel: { From 93de89ea795233de1dcd918be748db9f048d9f80 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 20 Mar 2024 21:02:17 +0800 Subject: [PATCH 09/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20refactor?= =?UTF-8?q?=20the=20db=20to=20support=20yjs=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/database/core/model.ts | 83 +++++++++++++------ src/database/core/sync.ts | 42 +++++----- src/database/models/__tests__/message.test.ts | 1 - src/database/models/__tests__/plugin.test.ts | 7 +- src/database/models/message.ts | 67 +++++++++------ src/database/models/plugin.ts | 6 +- src/database/models/session.ts | 21 +++-- src/database/models/sessionGroup.ts | 19 +++-- src/database/models/topic.ts | 26 ++---- 9 files changed, 162 insertions(+), 110 deletions(-) diff --git a/src/database/core/model.ts b/src/database/core/model.ts index 92bb9f415e32..198729941991 100644 --- a/src/database/core/model.ts +++ b/src/database/core/model.ts @@ -1,11 +1,11 @@ import Dexie, { BulkError } from 'dexie'; import { ZodObject } from 'zod'; -import { DBBaseFieldsSchema } from '@/database/core/types/db'; import { nanoid } from '@/utils/uuid'; import { LocalDB, LocalDBInstance, LocalDBSchema } from './db'; import { dataSync } from './sync'; +import { DBBaseFieldsSchema } from './types/db'; export class BaseModel { protected readonly db: LocalDB; @@ -26,6 +26,8 @@ export class BaseModel string; + withSync?: boolean; } = {}, ): Promise<{ added: number; @@ -85,8 +88,8 @@ export class BaseModel { - const { idGenerator = nanoid, createWithNewId = false } = options; - const validatedData = []; + const { idGenerator = nanoid, createWithNewId = false, withSync = true } = options; + const validatedData: any[] = []; const errors = []; const skips: string[] = []; @@ -130,10 +133,15 @@ export class BaseModel { - await this.updateYMapItem(item.id); - }); - await Promise.all(pools); + + if (withSync) { + dataSync.transact(async () => { + const pools = validatedData.map(async (item) => { + await this.updateYMapItem(item.id); + }); + await Promise.all(pools); + }); + } return { added: validatedData.length, @@ -156,6 +164,35 @@ export class BaseModel { + keys.forEach((id) => { + this.yMap?.delete(id); + }); + }); + } + + protected async _clearWithSync() { + const result = await this.table.clear(); + // sync clear data to yjs data map + this.yMap?.clear(); + return result; + } + + // **************** Update *************** // + protected async _updateWithSync(id: string, data: Partial) { // we need to check whether the data is valid // pick data related schema from the full schema @@ -175,35 +212,31 @@ export class BaseModel { - keys.forEach((id) => { - this.yMap?.delete(id); + await dataSync.transact(() => { + items.forEach((items) => { + this.updateYMapItem((items as any).id); }); }); } - protected async _clearWithSync() { - const result = await this.table.clear(); - // sync clear data to yjs data map - this.yMap?.clear(); - return result; - } + // **************** Helper *************** // private updateYMapItem = async (id: string) => { const newData = await this.table.get(id); diff --git a/src/database/core/sync.ts b/src/database/core/sync.ts index aaba88a8dae4..6aa95d53e565 100644 --- a/src/database/core/sync.ts +++ b/src/database/core/sync.ts @@ -12,27 +12,6 @@ import { import { LobeDBSchemaMap, LocalDBInstance } from './db'; -interface IWebsocketClient { - binaryType: 'arraybuffer' | 'blob' | null; - connect(): void; - connected: boolean; - connecting: boolean; - destroy(): void; - disconnect(): void; - lastMessageReceived: number; - send(message: any): void; - shouldConnect: boolean; - unsuccessfulReconnects: number; - url: string; - ws: WebSocket; -} - -declare global { - interface Window { - __ONLY_USE_FOR_CLEANUP_IN_DEV?: WebrtcProvider | null; - } -} - class DataSync { private _ydoc: Doc | null = null; private provider: WebrtcProvider | null = null; @@ -328,3 +307,24 @@ class DataSync { } export const dataSync = new DataSync(); + +interface IWebsocketClient { + binaryType: 'arraybuffer' | 'blob' | null; + connect(): void; + connected: boolean; + connecting: boolean; + destroy(): void; + disconnect(): void; + lastMessageReceived: number; + send(message: any): void; + shouldConnect: boolean; + unsuccessfulReconnects: number; + url: string; + ws: WebSocket; +} + +declare global { + interface Window { + __ONLY_USE_FOR_CLEANUP_IN_DEV?: WebrtcProvider | null; + } +} diff --git a/src/database/models/__tests__/message.test.ts b/src/database/models/__tests__/message.test.ts index ca2885988ac9..4c6402495a71 100644 --- a/src/database/models/__tests__/message.test.ts +++ b/src/database/models/__tests__/message.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { DB_Message } from '@/database/schemas/message'; import { ChatMessage } from '@/types/message'; import { CreateMessageParams, MessageModel } from '../message'; diff --git a/src/database/models/__tests__/plugin.test.ts b/src/database/models/__tests__/plugin.test.ts index 236f16996dc8..ada338b5f978 100644 --- a/src/database/models/__tests__/plugin.test.ts +++ b/src/database/models/__tests__/plugin.test.ts @@ -59,11 +59,14 @@ describe('PluginModel', () => { describe('update', () => { it('should update a plugin', async () => { await PluginModel.create(pluginData); - const updatedPluginData: DB_Plugin = { ...pluginData, type: 'customPlugin' }; + const updatedPluginData: DB_Plugin = { + ...pluginData, + type: 'customPlugin', + }; await PluginModel.update(pluginData.identifier, updatedPluginData); const plugins = await PluginModel.getList(); expect(plugins).toHaveLength(1); - expect(plugins[0]).toEqual(updatedPluginData); + expect(plugins[0]).toEqual({ ...updatedPluginData, updatedAt: expect.any(Number) }); }); }); diff --git a/src/database/models/message.ts b/src/database/models/message.ts index e57bcce0afad..c1b7cf2d8c39 100644 --- a/src/database/models/message.ts +++ b/src/database/models/message.ts @@ -113,7 +113,7 @@ class _MessageModel extends BaseModel { async batchCreate(messages: ChatMessage[]) { const data: DB_Message[] = messages.map((m) => this.mapChatMessageToDBMessage(m)); - return this._batchAdd(data); + return this._batchAdd(data, { withSync: true }); } async duplicateMessages(messages: ChatMessage[]): Promise { @@ -123,28 +123,6 @@ class _MessageModel extends BaseModel { return duplicatedMessages; } - async createDuplicateMessages(messages: ChatMessage[]): Promise { - // 创建一个映射来存储原始消息ID和复制消息ID之间的关系 - const idMapping = new Map(); - - // 首先复制所有消息,并为每个复制的消息生成新的ID - const duplicatedMessages = messages.map((originalMessage) => { - const newId = nanoid(); - idMapping.set(originalMessage.id, newId); - - return { ...originalMessage, id: newId }; - }); - - // 更新 parentId 为复制后的新ID - for (const duplicatedMessage of duplicatedMessages) { - if (duplicatedMessage.parentId && idMapping.has(duplicatedMessage.parentId)) { - duplicatedMessage.parentId = idMapping.get(duplicatedMessage.parentId); - } - } - - return duplicatedMessages; - } - // **************** Delete *************** // async delete(id: string) { @@ -181,6 +159,25 @@ class _MessageModel extends BaseModel { return this._bulkDeleteWithSync(messageIds); } + async batchDeleteBySessionId(sessionId: string): Promise { + // If topicId is specified, use both assistantId and topicId as the filter criteria in the query. + // Otherwise, filter by assistantId and require that topicId is undefined. + const messageIds = await this.table.where('sessionId').equals(sessionId).primaryKeys(); + + // Use the bulkDelete method to delete all selected messages in bulk + return this._bulkDeleteWithSync(messageIds); + } + + /** + * Delete all messages associated with the topicId + * @param topicId + */ + async batchDeleteByTopicId(topicId: string): Promise { + const messageIds = await this.table.where('topicId').equals(topicId).primaryKeys(); + + return this._bulkDeleteWithSync(messageIds); + } + // **************** Update *************** // async update(id: string, data: DeepPartial) { @@ -211,13 +208,35 @@ class _MessageModel extends BaseModel { })); // Use the bulkPut method to update the messages in bulk - await this.table.bulkPut(updatedMessages); + await this._bulkPutWithSync(updatedMessages); return updatedMessages.length; } // **************** Helper *************** // + private async createDuplicateMessages(messages: ChatMessage[]): Promise { + // 创建一个映射来存储原始消息ID和复制消息ID之间的关系 + const idMapping = new Map(); + + // 首先复制所有消息,并为每个复制的消息生成新的ID + const duplicatedMessages = messages.map((originalMessage) => { + const newId = nanoid(); + idMapping.set(originalMessage.id, newId); + + return { ...originalMessage, id: newId }; + }); + + // 更新 parentId 为复制后的新ID + for (const duplicatedMessage of duplicatedMessages) { + if (duplicatedMessage.parentId && idMapping.has(duplicatedMessage.parentId)) { + duplicatedMessage.parentId = idMapping.get(duplicatedMessage.parentId); + } + } + + return duplicatedMessages; + } + private mapChatMessageToDBMessage(message: ChatMessage): DB_Message { const { extra, ...messageData } = message; diff --git a/src/database/models/plugin.ts b/src/database/models/plugin.ts index 23d4928b6ebd..862c4df77e37 100644 --- a/src/database/models/plugin.ts +++ b/src/database/models/plugin.ts @@ -27,7 +27,7 @@ class _PluginModel extends BaseModel { const old = await this.table.get(plugin.identifier); const dbPlugin = this.mapToDBPlugin(plugin); - return this.table.put(merge(old, dbPlugin), plugin.identifier); + return this._putWithSync(merge(old, dbPlugin), plugin.identifier); }; batchCreate = async (plugins: LobeTool[]) => { @@ -47,7 +47,9 @@ class _PluginModel extends BaseModel { // **************** Update *************** // update: (id: string, value: Partial) => Promise = async (id, value) => { - return this.table.update(id, value); + const { success } = await this._updateWithSync(id, value); + + return success; }; // **************** Helper *************** // diff --git a/src/database/models/session.ts b/src/database/models/session.ts index e79e51028b3d..b1e681e6df45 100644 --- a/src/database/models/session.ts +++ b/src/database/models/session.ts @@ -16,6 +16,9 @@ import { import { merge } from '@/utils/merge'; import { uuid } from '@/utils/uuid'; +import { MessageModel } from './message'; +import { TopicModel } from './topic'; + class _SessionModel extends BaseModel { constructor() { super('sessions', DB_SessionSchema); @@ -211,26 +214,22 @@ class _SessionModel extends BaseModel { async delete(id: string) { return this.db.transaction('rw', [this.table, this.db.topics, this.db.messages], async () => { // Delete all topics associated with the session - const topics = await this.db.topics.where('sessionId').equals(id).toArray(); - const topicIds = topics.map((topic) => topic.id); - if (topicIds.length > 0) { - await this.db.topics.bulkDelete(topicIds); - } + await TopicModel.batchDeleteBySessionId(id); // Delete all messages associated with the session - const messages = await this.db.messages.where('sessionId').equals(id).toArray(); - const messageIds = messages.map((message) => message.id); - if (messageIds.length > 0) { - await this.db.messages.bulkDelete(messageIds); - } + await MessageModel.batchDeleteBySessionId(id); // Finally, delete the session itself await this._deleteWithSync(id); }); } + async batchDelete(ids: string[]) { + return this._bulkDeleteWithSync(ids); + } + async clearTable() { - return this.table.clear(); + return this._clearWithSync(); } // **************** Update *************** // diff --git a/src/database/models/sessionGroup.ts b/src/database/models/sessionGroup.ts index ffce3c19e0cb..6339320f379e 100644 --- a/src/database/models/sessionGroup.ts +++ b/src/database/models/sessionGroup.ts @@ -3,6 +3,8 @@ import { DB_SessionGroup, DB_SessionGroupSchema } from '@/database/schemas/sessi import { SessionGroups } from '@/types/session'; import { nanoid } from '@/utils/uuid'; +import { SessionModel } from './session'; + class _SessionGroupModel extends BaseModel { constructor() { super('sessionGroups', DB_SessionGroupSchema); @@ -51,19 +53,24 @@ class _SessionGroupModel extends BaseModel { // **************** Delete *************** // async delete(id: string, removeGroupItem: boolean = false) { - this.db.sessions.toCollection().modify((session) => { + this.db.sessions.toCollection().modify(async (session) => { // update all session associated with the sessionGroup to default - if (session.group === id) session.group = 'default'; + if (session.group === id) { + await SessionModel.update(session.id, { group: 'default' }); + } }); + if (!removeGroupItem) { - return this.table.delete(id); + return this._deleteWithSync(id); } else { - return this.db.sessions.where('group').equals(id).delete(); + const sessionIds = await this.db.sessions.where('group').equals(id).primaryKeys(); + + return await SessionModel.batchDelete(sessionIds); } } async clear() { - this.table.clear(); + await this._clearWithSync(); } // **************** Update *************** // @@ -75,7 +82,7 @@ class _SessionGroupModel extends BaseModel { async updateOrder(sortMap: { id: string; sort: number }[]) { return this.db.transaction('rw', this.table, async () => { for (const { id, sort } of sortMap) { - await this.table.update(id, { sort }); + await this.update(id, { sort }); } }); } diff --git a/src/database/models/topic.ts b/src/database/models/topic.ts index 27bcf261d587..17530b7f88e6 100644 --- a/src/database/models/topic.ts +++ b/src/database/models/topic.ts @@ -123,10 +123,12 @@ class _TopicModel extends BaseModel { // add topicId to these messages if (messages) { - await this.db.messages.where('id').anyOf(messages).modify({ topicId: topic.id }); + await MessageModel.batchUpdate(messages, { topicId: topic.id }); } + return topic; } + async batchCreate(topics: CreateTopicParams[]) { return this._batchAdd(topics.map((t) => ({ ...t, favorite: t.favorite ? 1 : 0 }))); } @@ -164,14 +166,9 @@ class _TopicModel extends BaseModel { async delete(id: string) { return this.db.transaction('rw', [this.table, this.db.messages], async () => { // Delete all messages associated with the topic - const messages = await this.db.messages.where('topicId').equals(id).toArray(); - - if (messages.length > 0) { - const messageIds = messages.map((msg) => msg.id); - await this.db.messages.bulkDelete(messageIds); - } + await MessageModel.batchDeleteByTopicId(id); - await this.table.delete(id); + await this._deleteWithSync(id); }); } @@ -189,7 +186,7 @@ class _TopicModel extends BaseModel { const topicIds = await query.primaryKeys(); // Use the bulkDelete method to delete all selected messages in bulk - return this.table.bulkDelete(topicIds); + return this._bulkDeleteWithSync(topicIds); } /** * Deletes multiple topics and all messages associated with them in a transaction. @@ -199,20 +196,13 @@ class _TopicModel extends BaseModel { // Iterate over each topicId and delete related messages, then delete the topic itself for (const topicId of topicIds) { // Delete all messages associated with the topic - const messages = await this.db.messages.where('topicId').equals(topicId).toArray(); - if (messages.length > 0) { - const messageIds = messages.map((msg) => msg.id); - await this.db.messages.bulkDelete(messageIds); - } - - // Delete the topic - await this.table.delete(topicId); + await this.delete(topicId); } }); } async clearTable() { - return this.table.clear(); + return this._clearWithSync(); } // **************** Update *************** // From 9e7baa864da97c25d8d83e2aa33f89a64fd3cd1c Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 20 Mar 2024 22:21:03 +0800 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20disconnect=20when?= =?UTF-8?q?=20user=20disable=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/database/core/model.ts | 4 +- src/database/core/sync.ts | 12 ++--- src/database/models/message.ts | 2 +- src/database/models/topic.ts | 2 +- .../SyncStatusInspector/EnableSync.tsx | 46 ++++--------------- src/services/global.ts | 6 +++ src/store/global/slices/common/action.ts | 5 +- 7 files changed, 29 insertions(+), 48 deletions(-) diff --git a/src/database/core/model.ts b/src/database/core/model.ts index 198729941991..726d124f1ba6 100644 --- a/src/database/core/model.ts +++ b/src/database/core/model.ts @@ -135,11 +135,11 @@ export class BaseModel { + dataSync.transact(() => { const pools = validatedData.map(async (item) => { await this.updateYMapItem(item.id); }); - await Promise.all(pools); + Promise.all(pools); }); } diff --git a/src/database/core/sync.ts b/src/database/core/sync.ts index 6aa95d53e565..4cfd7bfb1657 100644 --- a/src/database/core/sync.ts +++ b/src/database/core/sync.ts @@ -149,6 +149,10 @@ class DataSync { await this.connect(params); }; + async disconnect() { + await this.cleanConnection(this.provider); + } + private initYDoc = async () => { if (typeof window === 'undefined') return; @@ -223,12 +227,8 @@ class DataSync { switch (payload.action) { case 'add': case 'update': { - const itemInTable = await table.get(id); - if (!itemInTable) { - await table.add(item, id); - } else { - await table.update(id, item); - } + await table.put(item, id); + break; } diff --git a/src/database/models/message.ts b/src/database/models/message.ts index c1b7cf2d8c39..5fbea8f8c40e 100644 --- a/src/database/models/message.ts +++ b/src/database/models/message.ts @@ -199,7 +199,7 @@ class _MessageModel extends BaseModel { */ async batchUpdate(messageIds: string[], updateFields: Partial): Promise { // Retrieve the messages by their IDs - const messagesToUpdate = await this.table.where(':id').anyOf(messageIds).toArray(); + const messagesToUpdate = await this.table.where('id').anyOf(messageIds).toArray(); // Update the specified fields of each message const updatedMessages = messagesToUpdate.map((message) => ({ diff --git a/src/database/models/topic.ts b/src/database/models/topic.ts index 17530b7f88e6..e47a8e740315 100644 --- a/src/database/models/topic.ts +++ b/src/database/models/topic.ts @@ -134,7 +134,7 @@ class _TopicModel extends BaseModel { } async duplicateTopic(topicId: string, newTitle?: string) { - return this.db.transaction('rw', this.db.topics, this.db.messages, async () => { + return this.db.transaction('rw', [this.db.topics, this.db.messages], async () => { // Step 1: get DB_Topic const topic = await this.findById(topicId); diff --git a/src/features/SyncStatusInspector/EnableSync.tsx b/src/features/SyncStatusInspector/EnableSync.tsx index e56c6dd01514..0cbfea858864 100644 --- a/src/features/SyncStatusInspector/EnableSync.tsx +++ b/src/features/SyncStatusInspector/EnableSync.tsx @@ -2,19 +2,12 @@ import { ActionIcon, Avatar, Icon } from '@lobehub/ui'; import { Divider, Popover, Switch, Tag, Typography } from 'antd'; import { createStyles } from 'antd-style'; import isEqual from 'fast-deep-equal'; -import { - LucideCloudy, - LucideLaptop, - LucideRefreshCw, - LucideSmartphone, - SettingsIcon, -} from 'lucide-react'; +import { LucideCloudy, LucideLaptop, LucideSmartphone, SettingsIcon } from 'lucide-react'; import Link from 'next/link'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; -import { useSyncEvent } from '@/hooks/useSyncData'; import { useGlobalStore } from '@/store/global'; import { syncSettingsSelectors } from '@/store/global/selectors'; import { pathString } from '@/utils/url'; @@ -44,20 +37,16 @@ const EnableSync = memo(({ hiddenActions }) => { const { t } = useTranslation('common'); const { styles, theme } = useStyles(); - const [syncStatus, isSyncing, channelName, enableWebRTC, setSettings, refreshConnection] = - useGlobalStore((s) => [ - s.syncStatus, - s.syncStatus === 'syncing', - syncSettingsSelectors.webrtcChannelName(s), - syncSettingsSelectors.enableWebRTC(s), - s.setSettings, - s.refreshConnection, - ]); + const [syncStatus, isSyncing, channelName, enableWebRTC, setSettings] = useGlobalStore((s) => [ + s.syncStatus, + s.syncStatus === 'syncing', + syncSettingsSelectors.webrtcChannelName(s), + syncSettingsSelectors.enableWebRTC(s), + s.setSettings, + ]); const users = useGlobalStore((s) => s.syncAwareness, isEqual); - const syncEvent = useSyncEvent(); - const switchSync = (enabled: boolean) => { setSettings({ sync: { webrtc: { enabled } } }); }; @@ -105,15 +94,6 @@ const EnableSync = memo(({ hiddenActions }) => { {t('sync.awareness.current')} - { - refreshConnection(syncEvent); - }} - size={'small'} - title={t('sync.actions.sync')} - /> )} @@ -138,15 +118,7 @@ const EnableSync = memo(({ hiddenActions }) => { {!hiddenActions && ( - { - refreshConnection(syncEvent); - }} - // size={'small'} - title={t('sync.actions.settings')} - /> + )} diff --git a/src/services/global.ts b/src/services/global.ts index f6a04c4d86bb..364a78610e79 100644 --- a/src/services/global.ts +++ b/src/services/global.ts @@ -37,6 +37,12 @@ class GlobalService { await dataSync.manualSync(); return true; }; + + disableSync = async () => { + await dataSync.disconnect(); + + return false; + }; } export const globalService = new GlobalService(); diff --git a/src/store/global/slices/common/action.ts b/src/store/global/slices/common/action.ts index 8e0dfcbf9dec..50fc10be6cd2 100644 --- a/src/store/global/slices/common/action.ts +++ b/src/store/global/slices/common/action.ts @@ -126,7 +126,10 @@ export const createCommonSlice: StateCreator< ['enableSync', userEnableSync, userId], async () => { // if user don't enable sync or no userId ,don't start sync - if (!userEnableSync || !userId) return false; + if (!userId) return false; + + // if user don't enable sync, stop sync + if (!userEnableSync) return globalService.disableSync(); return get().triggerEnableSync(userId, onEvent); }, From f763ac2f654d6cf8d33c4b884b3e929c5120df40 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 21 Mar 2024 00:05:20 +0800 Subject: [PATCH 11/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20refactor?= =?UTF-8?q?=20the=20console=20with=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 ++ src/database/core/sync.ts | 46 +++++++++++++++++++-------------------- src/services/global.ts | 8 ------- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 0348ec282892..cdd01d38c4b8 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "brotli-wasm": "^2", "chroma-js": "^2", "dayjs": "^1", + "debug": "^4", "dexie": "^3", "diff": "^5", "fast-deep-equal": "^3", @@ -167,6 +168,7 @@ "@testing-library/jest-dom": "^6", "@testing-library/react": "^14", "@types/chroma-js": "^2", + "@types/debug": "^4.1.12", "@types/diff": "^5", "@types/json-schema": "^7", "@types/lodash": "^4", diff --git a/src/database/core/sync.ts b/src/database/core/sync.ts index 4cfd7bfb1657..adc372368f01 100644 --- a/src/database/core/sync.ts +++ b/src/database/core/sync.ts @@ -1,3 +1,4 @@ +import Debug from 'debug'; import { throttle, uniqBy } from 'lodash-es'; import type { WebrtcProvider } from 'y-webrtc'; import type { Doc, Transaction } from 'yjs'; @@ -12,6 +13,8 @@ import { import { LobeDBSchemaMap, LocalDBInstance } from './db'; +const LOG_NAME_SPACE = 'DataSync'; + class DataSync { private _ydoc: Doc | null = null; private provider: WebrtcProvider | null = null; @@ -21,6 +24,8 @@ class DataSync { private waitForConnecting: any; + logger = Debug(LOG_NAME_SPACE); + transact(fn: (transaction: Transaction) => unknown) { this._ydoc?.transact(fn); } @@ -55,11 +60,11 @@ class DataSync { await this.initYDoc(); - console.log('[YJS] start to listen sync event...'); + this.logger('[YJS] start to listen sync event...'); this.initYjsObserve(onSyncEvent, onSyncStatusChange); // ====== 2. init webrtc provider ====== // - console.log(`[WebRTC] init provider... room: ${channel.name}`); + this.logger(`[WebRTC] init provider... room: ${channel.name}`); const { WebrtcProvider } = await import('y-webrtc'); // clients connected to the same room-name share document updates @@ -74,14 +79,14 @@ class DataSync { window.__ONLY_USE_FOR_CLEANUP_IN_DEV = this.provider; } - console.log(`[WebRTC] provider init success`); + this.logger(`[WebRTC] provider init success`); // ====== 3. check signaling server connection ====== // // 当本地设备正确连接到 WebRTC Provider 后,触发 status 事件 // 当开始连接,则开始监听事件 this.provider.on('status', async ({ connected }) => { - console.log('[WebRTC] peer status:', connected); + this.logger('[WebRTC] peer status:', connected); if (connected) { // this.initObserve(onSyncEvent, onSyncStatusChange); onSyncStatusChange?.(PeerSyncStatus.Connecting); @@ -113,16 +118,15 @@ class DataSync { // 当各方的数据均完成同步后,YJS 对象之间的数据已经一致时,触发 synced 事件 this.provider.on('synced', async ({ synced }) => { - console.log('[WebRTC] peer sync status:', synced); + this.logger('[WebRTC] peer sync status:', synced); if (synced) { - console.groupCollapsed('[WebRTC] start to init yjs data...'); + this.logger('[WebRTC] start to init yjs data...'); onSyncStatusChange?.(PeerSyncStatus.Syncing); await this.initSync(); onSyncStatusChange?.(PeerSyncStatus.Synced); - console.groupEnd(); - console.log('[WebRTC] yjs data init success'); + this.logger('[WebRTC] yjs data init success'); } else { - console.log('[WebRTC] data not sync, try to reconnect in 1s...'); + this.logger('[WebRTC] data not sync, try to reconnect in 1s...'); // await this.reconnect(params); setTimeout(() => { onSyncStatusChange?.(PeerSyncStatus.Syncing); @@ -138,11 +142,6 @@ class DataSync { return this.provider; }; - manualSync = async () => { - console.log('[WebRTC] try to manual init sync...'); - await this.reconnect(this.syncParams); - }; - reconnect = async (params: StartDataSyncParams) => { await this.cleanConnection(this.provider); @@ -156,30 +155,29 @@ class DataSync { private initYDoc = async () => { if (typeof window === 'undefined') return; - console.log('[YJS] init YDoc...'); + this.logger('[YJS] init YDoc...'); const { Doc } = await import('yjs'); this._ydoc = new Doc(); }; private async cleanConnection(provider: WebrtcProvider | null) { if (provider) { - console.groupCollapsed(`[WebRTC] clean Connection...`); - console.log(`[WebRTC] clean awareness...`); + this.logger(`[WebRTC] clean Connection...`); + this.logger(`[WebRTC] clean awareness...`); provider.awareness.destroy(); - console.log(`[WebRTC] clean room...`); + this.logger(`[WebRTC] clean room...`); provider.room?.disconnect(); provider.room?.destroy(); - console.log(`[WebRTC] clean provider...`); + this.logger(`[WebRTC] clean provider...`); provider.disconnect(); provider.destroy(); - console.log(`[WebRTC] clean yjs doc...`); + this.logger(`[WebRTC] clean yjs doc...`); this._ydoc?.destroy(); - console.groupEnd(); - console.log(`[WebRTC] -------------------`); + this.logger(`[WebRTC] -------------------`); } } @@ -220,7 +218,7 @@ class DataSync { onSyncStatusChange(PeerSyncStatus.Syncing); - console.log(`[YJS] observe ${tableKey} changes:`, event.keysChanged.size); + this.logger(`[YJS] observe ${tableKey} changes:`, event.keysChanged.size); const pools = Array.from(event.keys).map(async ([id, payload]) => { const item: any = yItemMap.get(id); @@ -277,7 +275,7 @@ class DataSync { }); } - console.log('[DB]:', tableKey, yItemMap?.size); + this.logger('[DB]:', tableKey, yItemMap?.size); }; private initAwareness = ({ user }: Pick) => { diff --git a/src/services/global.ts b/src/services/global.ts index 364a78610e79..8c643b38f245 100644 --- a/src/services/global.ts +++ b/src/services/global.ts @@ -30,14 +30,6 @@ class GlobalService { return true; }; - reconnect = async (params: StartDataSyncParams) => { - if (typeof window === 'undefined') return false; - - await dataSync.reconnect(params); - await dataSync.manualSync(); - return true; - }; - disableSync = async () => { await dataSync.disconnect(); From 1a1c22d89c48c6d96a141ae512bab5d7d77ae5eb Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 21 Mar 2024 00:33:39 +0800 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=9A=B8=20style:=20add=20experiment?= =?UTF-8?q?=20feature=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + .../settings/(desktop)/features/Header.tsx | 12 ++++++- src/app/settings/sync/Alert.tsx | 33 +++++++++++++++++++ src/app/settings/sync/WebRTC.tsx | 6 ++-- src/app/settings/sync/page.tsx | 2 ++ src/app/settings/sync/util.ts | 4 +++ src/locales/default/setting.ts | 4 +++ .../global/slices/preference/initialState.ts | 2 ++ .../global/slices/preference/selectors.ts | 3 ++ 9 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/app/settings/sync/Alert.tsx create mode 100644 src/app/settings/sync/util.ts diff --git a/package.json b/package.json index cdd01d38c4b8..97cf422812b4 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "polished": "^4", "posthog-js": "^1", "query-string": "^9", + "random-words": "^2.0.1", "react": "^18", "react-dom": "^18", "react-hotkeys-hook": "^4", diff --git a/src/app/settings/(desktop)/features/Header.tsx b/src/app/settings/(desktop)/features/Header.tsx index f4f95caa954a..a71df53c1c46 100644 --- a/src/app/settings/(desktop)/features/Header.tsx +++ b/src/app/settings/(desktop)/features/Header.tsx @@ -1,6 +1,8 @@ import { ChatHeader, ChatHeaderTitle } from '@lobehub/ui'; +import { Tag } from 'antd'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; import { SettingsTabs } from '@/store/global/initialState'; @@ -11,7 +13,15 @@ const Header = memo(({ activeTab }: { activeTab: SettingsTabs }) => { - + + {t(`tab.${activeTab}`)} + + {activeTab === SettingsTabs.Sync && {t('tab.experiment')}} + + } + />
} /> diff --git a/src/app/settings/sync/Alert.tsx b/src/app/settings/sync/Alert.tsx new file mode 100644 index 000000000000..6285889896bf --- /dev/null +++ b/src/app/settings/sync/Alert.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { Alert } from '@lobehub/ui'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { MAX_WIDTH } from '@/const/layoutTokens'; +import { useGlobalStore } from '@/store/global'; +import { preferenceSelectors } from '@/store/global/selectors'; + +const ExperimentAlert = () => { + const { t } = useTranslation('setting'); + const [showSyncAlert, updatePreference] = useGlobalStore((s) => [ + preferenceSelectors.showSyncAlert(s), + s.updatePreference, + ]); + return ( + showSyncAlert && ( + + { + updatePreference({ showSyncAlert: false }); + }} + type={'warning'} + /> + + ) + ); +}; + +export default ExperimentAlert; diff --git a/src/app/settings/sync/WebRTC.tsx b/src/app/settings/sync/WebRTC.tsx index 69a60afa4827..9aa1b785a15d 100644 --- a/src/app/settings/sync/WebRTC.tsx +++ b/src/app/settings/sync/WebRTC.tsx @@ -8,10 +8,10 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { generateRandomRoomName } from '@/app/settings/sync/util'; import { FORM_STYLE } from '@/const/layoutTokens'; import SyncStatusInspector from '@/features/SyncStatusInspector'; import { useGlobalStore } from '@/store/global'; -import { uuid } from '@/utils/uuid'; import { useSyncSettings } from '../hooks/useSyncSettings'; @@ -37,8 +37,8 @@ const WebRTC = memo(() => { { - const name = uuid(); + onClick={async () => { + const name = await generateRandomRoomName(); form.setFieldValue(['sync', 'webrtc', 'channelName'], name); form.submit(); }} diff --git a/src/app/settings/sync/page.tsx b/src/app/settings/sync/page.tsx index b5f6320c613b..468a48ab5489 100644 --- a/src/app/settings/sync/page.tsx +++ b/src/app/settings/sync/page.tsx @@ -2,6 +2,7 @@ import { memo } from 'react'; import { gerServerDeviceInfo } from '@/utils/responsive'; +import Alert from './Alert'; import DeviceCard from './DeviceInfo'; import PageTitle from './PageTitle'; import WebRTC from './WebRTC'; @@ -13,6 +14,7 @@ export default memo(() => { + ); }); diff --git a/src/app/settings/sync/util.ts b/src/app/settings/sync/util.ts new file mode 100644 index 000000000000..04317fd0ae28 --- /dev/null +++ b/src/app/settings/sync/util.ts @@ -0,0 +1,4 @@ +export const generateRandomRoomName = async () => { + const { generate } = await import('random-words'); + return (generate(3) as string[]).join('-'); +}; diff --git a/src/locales/default/setting.ts b/src/locales/default/setting.ts index 465cc19bd1ba..0691dedbaf58 100644 --- a/src/locales/default/setting.ts +++ b/src/locales/default/setting.ts @@ -453,6 +453,9 @@ export default { unknownBrowser: '未知浏览器', unknownOS: '未知系统', }, + warning: { + message: '本功能目前仍为实验性功能,可能存在预期外或不稳定的情况,如遇到问题请及时提交反馈。', + }, webrtc: { channelName: { desc: 'WebRTC 将使用此名创建同步频道,确保频道名称唯一', @@ -478,6 +481,7 @@ export default { about: '关于', agent: '默认助手', common: '通用设置', + experiment: '实验', llm: '语言模型', sync: '云端同步', tts: '语音服务', diff --git a/src/store/global/slices/preference/initialState.ts b/src/store/global/slices/preference/initialState.ts index 340236e0d537..35f950162be5 100644 --- a/src/store/global/slices/preference/initialState.ts +++ b/src/store/global/slices/preference/initialState.ts @@ -15,6 +15,7 @@ export interface GlobalPreference { showChatSideBar?: boolean; showSessionPanel?: boolean; + showSyncAlert: boolean; showSystemRole?: boolean; telemetry: boolean | null; /** @@ -40,6 +41,7 @@ export const initialPreferenceState: GlobalPreferenceState = { sessionsWidth: 320, showChatSideBar: true, showSessionPanel: true, + showSyncAlert: true, showSystemRole: false, telemetry: null, useCmdEnterToSend: false, diff --git a/src/store/global/slices/preference/selectors.ts b/src/store/global/slices/preference/selectors.ts index 56b487c350a9..53bc1f60986f 100644 --- a/src/store/global/slices/preference/selectors.ts +++ b/src/store/global/slices/preference/selectors.ts @@ -6,8 +6,11 @@ const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterT const userAllowTrace = (s: GlobalStore) => s.preference.telemetry; +const showSyncAlert = (s: GlobalStore) => s.preference.showSyncAlert; + export const preferenceSelectors = { sessionGroupKeys, + showSyncAlert, useCmdEnterToSend, userAllowTrace, }; From 375c42d737d717fceaa7a943b59b3c822ced6f38 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 21 Mar 2024 04:52:00 +0000 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=93=B1=20style:=20fix=20with=20mobi?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/(mobile)/features/SessionHeader.tsx | 13 +++- src/app/settings/sync/Alert.tsx | 18 +++-- src/app/settings/sync/DeviceInfo/Card.tsx | 6 +- src/app/settings/sync/DeviceInfo/index.tsx | 76 ++++++++++++------- src/app/settings/sync/WebRTC.tsx | 1 + src/app/settings/sync/page.tsx | 7 +- .../SyncStatusInspector/DisableSync.tsx | 8 +- .../SyncStatusInspector/EnableSync.tsx | 6 +- src/features/SyncStatusInspector/index.tsx | 21 +++-- .../global/slices/preference/initialState.ts | 5 +- .../global/slices/preference/selectors.ts | 4 +- 11 files changed, 105 insertions(+), 60 deletions(-) diff --git a/src/app/chat/(mobile)/features/SessionHeader.tsx b/src/app/chat/(mobile)/features/SessionHeader.tsx index f0aa2ca6b18f..ae7bca1f1b41 100644 --- a/src/app/chat/(mobile)/features/SessionHeader.tsx +++ b/src/app/chat/(mobile)/features/SessionHeader.tsx @@ -3,8 +3,10 @@ import { createStyles } from 'antd-style'; import { MessageSquarePlus } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens'; +import SyncStatusInspector from '@/features/SyncStatusInspector'; import { useGlobalStore } from '@/store/global'; import { commonSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; @@ -25,11 +27,14 @@ const Header = memo(() => { const avatar = useGlobalStore(commonSelectors.userAvatar); return ( } left={ -
router.push('/settings')} style={{ marginLeft: 8 }}> - {avatar ? : } -
+ +
router.push('/settings')}> + {avatar ? : } +
+ + +
} right={ { +interface ExperimentAlertProps { + mobile?: boolean; +} +const ExperimentAlert = memo(({ mobile }) => { const { t } = useTranslation('setting'); - const [showSyncAlert, updatePreference] = useGlobalStore((s) => [ - preferenceSelectors.showSyncAlert(s), + const [hideSyncAlert, updatePreference] = useGlobalStore((s) => [ + preferenceSelectors.hideSyncAlert(s), s.updatePreference, ]); + return ( - showSyncAlert && ( + !hideSyncAlert && ( { - updatePreference({ showSyncAlert: false }); + updatePreference({ hideSyncAlert: true }); }} type={'warning'} /> ) ); -}; +}); export default ExperimentAlert; diff --git a/src/app/settings/sync/DeviceInfo/Card.tsx b/src/app/settings/sync/DeviceInfo/Card.tsx index 927901db3faa..7320a1728110 100644 --- a/src/app/settings/sync/DeviceInfo/Card.tsx +++ b/src/app/settings/sync/DeviceInfo/Card.tsx @@ -2,10 +2,14 @@ import { createStyles } from 'antd-style'; import { ReactNode, memo } from 'react'; import { Center, Flexbox } from 'react-layout-kit'; -const useStyles = createStyles(({ css, token }) => ({ +const useStyles = createStyles(({ css, token, responsive }) => ({ container: css` background: ${token.colorFillTertiary}; border-radius: 12px; + + .${responsive.mobile} { + width: 100%; + } `, icon: css` width: 40px; diff --git a/src/app/settings/sync/DeviceInfo/index.tsx b/src/app/settings/sync/DeviceInfo/index.tsx index 00f7d6b8ae88..5bbcfae2b80c 100644 --- a/src/app/settings/sync/DeviceInfo/index.tsx +++ b/src/app/settings/sync/DeviceInfo/index.tsx @@ -19,17 +19,36 @@ const useStyles = createStyles(({ css, cx, responsive, isDarkMode, token, stylis flex-direction: row; ${responsive.mobile} { flex-direction: column; + width: 100%; } `, container: css` + position: relative; + width: 100%; border-radius: ${token.borderRadiusLG}px; `, content: cx( stylish.blurStrong, css` z-index: 2; + + flex-direction: row; + justify-content: space-between; + + height: 88px; + padding: 12px; + background: ${rgba(token.colorBgContainer, isDarkMode ? 0.7 : 1)}; border-radius: ${token.borderRadiusLG - 1}px; + + ${responsive.mobile} { + flex-direction: column; + gap: 16px; + align-items: flex-start; + + width: 100%; + padding: 8px; + } `, ), glow: cx( @@ -45,6 +64,12 @@ const useStyles = createStyles(({ css, cx, responsive, isDarkMode, token, stylis animation-duration: 10s; `, ), + wrapper: css` + ${responsive.mobile} { + padding-block: 8px; + padding-inline: 4px; + } + `, })); interface DeviceCardProps { @@ -58,40 +83,33 @@ const DeviceCard = memo(({ browser, os }) => { return ( - -
- {t('sync.device.title')} -
-
- - - - } title={os || t('sync.device.unknownOS')} /> - } - title={browser || t('sync.device.unknownBrowser')} - /> + + +
+ {t('sync.device.title')} +
+
+ + + + } title={os || t('sync.device.unknownOS')} /> + } + title={browser || t('sync.device.unknownBrowser')} + /> + + - ); }); diff --git a/src/app/settings/sync/WebRTC.tsx b/src/app/settings/sync/WebRTC.tsx index 9aa1b785a15d..4b60231141f3 100644 --- a/src/app/settings/sync/WebRTC.tsx +++ b/src/app/settings/sync/WebRTC.tsx @@ -40,6 +40,7 @@ const WebRTC = memo(() => { onClick={async () => { const name = await generateRandomRoomName(); form.setFieldValue(['sync', 'webrtc', 'channelName'], name); + form.setFieldValue(['sync', 'webrtc', 'enabled'], false); form.submit(); }} size={'small'} diff --git a/src/app/settings/sync/page.tsx b/src/app/settings/sync/page.tsx index 468a48ab5489..8a9dc483a4ad 100644 --- a/src/app/settings/sync/page.tsx +++ b/src/app/settings/sync/page.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; -import { gerServerDeviceInfo } from '@/utils/responsive'; +import { gerServerDeviceInfo, isMobileDevice } from '@/utils/responsive'; import Alert from './Alert'; import DeviceCard from './DeviceInfo'; @@ -9,12 +9,15 @@ import WebRTC from './WebRTC'; export default memo(() => { const { os, browser } = gerServerDeviceInfo(); + const isMobile = isMobileDevice(); + return ( <> + {isMobile && } - + {!isMobile && } ); }); diff --git a/src/features/SyncStatusInspector/DisableSync.tsx b/src/features/SyncStatusInspector/DisableSync.tsx index ca20fc456f46..76a7f5c911c5 100644 --- a/src/features/SyncStatusInspector/DisableSync.tsx +++ b/src/features/SyncStatusInspector/DisableSync.tsx @@ -1,5 +1,6 @@ import { Icon, Tag } from '@lobehub/ui'; import { Badge, Button, Popover } from 'antd'; +import { TooltipPlacement } from 'antd/es/tooltip'; import { LucideCloudCog, LucideCloudy } from 'lucide-react'; import Link from 'next/link'; import { memo } from 'react'; @@ -7,13 +8,14 @@ import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; import { useGlobalStore } from '@/store/global'; -import { syncSettingsSelectors } from '@/store/global/slices/settings/selectors'; +import { syncSettingsSelectors } from '@/store/global/selectors'; interface DisableSyncProps { noPopover?: boolean; + placement?: TooltipPlacement; } -const DisableSync = memo(({ noPopover }) => { +const DisableSync = memo(({ noPopover, placement = 'bottomLeft' }) => { const { t } = useTranslation('common'); const [haveConfig, setSettings] = useGlobalStore((s) => [ !!syncSettingsSelectors.webrtcConfig(s).channelName, @@ -61,7 +63,7 @@ const DisableSync = memo(({ noPopover }) => { )}
} - placement={'bottomLeft'} + placement={placement} title={ diff --git a/src/features/SyncStatusInspector/EnableSync.tsx b/src/features/SyncStatusInspector/EnableSync.tsx index 0cbfea858864..21f201a66704 100644 --- a/src/features/SyncStatusInspector/EnableSync.tsx +++ b/src/features/SyncStatusInspector/EnableSync.tsx @@ -1,6 +1,7 @@ import { ActionIcon, Avatar, Icon } from '@lobehub/ui'; import { Divider, Popover, Switch, Tag, Typography } from 'antd'; import { createStyles } from 'antd-style'; +import { TooltipPlacement } from 'antd/es/tooltip'; import isEqual from 'fast-deep-equal'; import { LucideCloudy, LucideLaptop, LucideSmartphone, SettingsIcon } from 'lucide-react'; import Link from 'next/link'; @@ -31,9 +32,10 @@ const useStyles = createStyles(({ css, token, prefixCls }) => ({ interface EnableSyncProps { hiddenActions?: boolean; + placement?: TooltipPlacement; } -const EnableSync = memo(({ hiddenActions }) => { +const EnableSync = memo(({ hiddenActions, placement = 'bottomLeft' }) => { const { t } = useTranslation('common'); const { styles, theme } = useStyles(); @@ -106,7 +108,7 @@ const EnableSync = memo(({ hiddenActions }) => {
} - placement={'bottomLeft'} + placement={placement} title={ diff --git a/src/features/SyncStatusInspector/index.tsx b/src/features/SyncStatusInspector/index.tsx index 3f8b9a7cc85d..5de666c3bc3b 100644 --- a/src/features/SyncStatusInspector/index.tsx +++ b/src/features/SyncStatusInspector/index.tsx @@ -1,3 +1,4 @@ +import { TooltipPlacement } from 'antd/es/tooltip'; import { memo } from 'react'; import { useGlobalStore } from '@/store/global'; @@ -8,15 +9,19 @@ import EnableSync from './EnableSync'; interface SyncStatusTagProps { hiddenActions?: boolean; hiddenEnableGuide?: boolean; + placement?: TooltipPlacement; } -const SyncStatusTag = memo(({ hiddenActions, hiddenEnableGuide }) => { - const [enableSync] = useGlobalStore((s) => [s.syncEnabled]); - return enableSync ? ( - - ) : ( - - ); -}); +const SyncStatusTag = memo( + ({ hiddenActions, placement, hiddenEnableGuide }) => { + const [enableSync] = useGlobalStore((s) => [s.syncEnabled]); + + return enableSync ? ( + + ) : ( + + ); + }, +); export default SyncStatusTag; diff --git a/src/store/global/slices/preference/initialState.ts b/src/store/global/slices/preference/initialState.ts index 35f950162be5..3a50d968df74 100644 --- a/src/store/global/slices/preference/initialState.ts +++ b/src/store/global/slices/preference/initialState.ts @@ -9,13 +9,13 @@ export interface GlobalPreference { // which sessionGroup should expand expandSessionGroupKeys: SessionGroupId[]; guide?: Guide; + hideSyncAlert?: boolean; inputHeight: number; mobileShowTopic?: boolean; - sessionsWidth: number; + sessionsWidth: number; showChatSideBar?: boolean; showSessionPanel?: boolean; - showSyncAlert: boolean; showSystemRole?: boolean; telemetry: boolean | null; /** @@ -41,7 +41,6 @@ export const initialPreferenceState: GlobalPreferenceState = { sessionsWidth: 320, showChatSideBar: true, showSessionPanel: true, - showSyncAlert: true, showSystemRole: false, telemetry: null, useCmdEnterToSend: false, diff --git a/src/store/global/slices/preference/selectors.ts b/src/store/global/slices/preference/selectors.ts index 53bc1f60986f..145719263579 100644 --- a/src/store/global/slices/preference/selectors.ts +++ b/src/store/global/slices/preference/selectors.ts @@ -6,11 +6,11 @@ const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterT const userAllowTrace = (s: GlobalStore) => s.preference.telemetry; -const showSyncAlert = (s: GlobalStore) => s.preference.showSyncAlert; +const hideSyncAlert = (s: GlobalStore) => s.preference.hideSyncAlert; export const preferenceSelectors = { + hideSyncAlert, sessionGroupKeys, - showSyncAlert, useCmdEnterToSend, userAllowTrace, }; From dc1c984cf35502ab722b70667401267cb65b3381 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 21 Mar 2024 05:48:40 +0000 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=8C=90=20chore:=20add=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ar/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/ar/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/de-DE/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/de-DE/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/en-US/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/en-US/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/es-ES/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/es-ES/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/fr-FR/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/fr-FR/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/it-IT/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/it-IT/setting.json | 38 ++++++++++++++++++++++++++++++++++++ locales/ja-JP/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/ja-JP/setting.json | 38 ++++++++++++++++++++++++++++++++++++ locales/ko-KR/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/ko-KR/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/nl-NL/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/nl-NL/setting.json | 38 ++++++++++++++++++++++++++++++++++++ locales/pl-PL/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/pl-PL/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/pt-BR/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/pt-BR/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/ru-RU/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/ru-RU/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/tr-TR/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/tr-TR/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/vi-VN/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/vi-VN/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/zh-CN/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/zh-CN/setting.json | 36 ++++++++++++++++++++++++++++++++++ locales/zh-TW/common.json | 40 ++++++++++++++++++++++++++++++++------ locales/zh-TW/setting.json | 36 ++++++++++++++++++++++++++++++++++ 32 files changed, 1126 insertions(+), 96 deletions(-) diff --git a/locales/ar/common.json b/locales/ar/common.json index 6325cffc90af..83756d12e27f 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -10,17 +10,12 @@ }, "about": "حول", "advanceSettings": "إعدادات متقدمة", - "agentMaxToken": "أقصى طول للجلسة", - "agentModel": "النموذج", - "agentProfile": "ملف المساعد", "appInitializing": "جاري تهيئة LobeChat، يرجى الانتظار ...", - "archive": "أرشيف", "autoGenerate": "توليد تلقائي", "autoGenerateTooltip": "إكمال تلقائي بناءً على الكلمات المقترحة لوصف المساعد", "cancel": "إلغاء", "changelog": "سجل التغييرات", "close": "إغلاق", - "confirmRemoveSessionItemAlert": "سيتم حذف هذا المساعد قريبًا، وبمجرد الحذف لن يمكن استعادته، يرجى تأكيد الإجراء", "copy": "نسخ", "copyFail": "فشل في النسخ", "copySuccess": "تم النسخ بنجاح", @@ -128,6 +123,39 @@ "setting": "الإعدادات", "share": "مشاركة", "stop": "إيقاف", + "sync": { + "actions": { + "settings": "إعدادات المزامنة", + "sync": "مزامنة فورية" + }, + "awareness": { + "current": "الجهاز الحالي" + }, + "channel": "القناة", + "disabled": { + "actions": { + "enable": "تمكين المزامنة السحابية", + "settings": "تكوين معلمات المزامنة" + }, + "desc": "بيانات الجلسة الحالية تُخزن فقط في هذا المتصفح. إذا كنت بحاجة إلى مزامنة البيانات بين عدة أجهزة، يرجى تكوين وتمكين المزامنة السحابية.", + "title": "لم يتم تشغيل مزامنة البيانات" + }, + "enabled": { + "title": "مزامنة البيانات" + }, + "status": { + "connecting": "جار الاتصال", + "disabled": "مزامنة غير مفعلة", + "ready": "متصل", + "synced": "تمت المزامنة", + "syncing": "جار المزامنة", + "unconnected": "فشل الاتصال" + }, + "title": "حالة المزامنة", + "unconnected": { + "tip": "فشل اتصال خادم الإشارة، لن يتمكن من إنشاء قناة اتصال نقطية، يرجى التحقق من الشبكة وإعادة المحاولة" + } + }, "tab": { "chat": "الدردشة", "market": "الاكتشاف", @@ -147,4 +175,4 @@ "hasNew": "يوجد تحديث متاح", "newVersion": "هناك إصدار جديد متاح: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/ar/setting.json b/locales/ar/setting.json index 15f79e1ce7b9..771506d479c8 100644 --- a/locales/ar/setting.json +++ b/locales/ar/setting.json @@ -440,11 +440,47 @@ "placeholder": "الرجاء إدخال معرف المساعد، يجب أن يكون فريدًا، مثل تطوير الويب", "tooltips": "مشاركة في سوق المساعدين" }, + "sync": { + "device": { + "deviceName": { + "hint": "أضف اسمًا للتعرف بشكل أفضل", + "placeholder": "الرجاء إدخال اسم الجهاز", + "title": "اسم الجهاز" + }, + "title": "معلومات الجهاز", + "unknownBrowser": "متصفح غير معروف", + "unknownOS": "نظام التشغيل غير معروف" + }, + "warning": { + "message": "هذه الميزة لا تزال تحت التجربة وقد تكون هناك حالات غير متوقعة أو غير مستقرة، في حال واجهت مشكلة، يرجى تقديم ردود فعل في الوقت المناسب." + }, + "webrtc": { + "channelName": { + "desc": "سيستخدم WebRTC هذا الاسم لإنشاء قناة مزامنة، يرجى التأكد من فرادة اسم القناة", + "placeholder": "الرجاء إدخال اسم قناة المزامنة", + "shuffle": "توليف عشوائي", + "title": "اسم قناة المزامنة" + }, + "channelPassword": { + "desc": "إضافة كلمة مرور لضمان خصوصية القناة، يمكن للأجهزة الانضمام إلى القناة فقط عند إدخال كلمة المرور الصحيحة", + "placeholder": "الرجاء إدخال كلمة مرور قناة المزامنة", + "title": "كلمة مرور قناة المزامنة" + }, + "desc": "اتصال البيانات النقطي الفوري يتطلب تواجد الأجهزة معًا للمزامنة", + "enabled": { + "invalid": "الرجاء إدخال اسم قناة المزامنة قبل تشغيلها", + "title": "تمكين المزامنة" + }, + "title": "WebRTC مزامنة" + } + }, "tab": { "about": "حول", "agent": "المساعد الافتراضي", "common": "إعدادات عامة", + "experiment": "تجربة", "llm": "نموذج اللغة", + "sync": "مزامنة السحابة", "tts": "خدمة الكلام" }, "tools": { diff --git a/locales/de-DE/common.json b/locales/de-DE/common.json index 32424cc60c3f..20aed9478cef 100644 --- a/locales/de-DE/common.json +++ b/locales/de-DE/common.json @@ -10,17 +10,12 @@ }, "about": "Über", "advanceSettings": "Erweiterte Einstellungen", - "agentMaxToken": "Maximale Sitzungslänge", - "agentModel": "Modell", - "agentProfile": "Assistentenprofil", "appInitializing": "LobeChat wird initialisiert. Bitte haben Sie einen Moment Geduld...", - "archive": "Archiv", "autoGenerate": "Automatisch generieren", "autoGenerateTooltip": "Assistentenbeschreibung automatisch auf Basis von Vorschlägen vervollständigen", "cancel": "Abbrechen", "changelog": "Änderungsprotokoll", "close": "Schließen", - "confirmRemoveSessionItemAlert": "Möchten Sie diesen Assistenten wirklich löschen? Nach dem Löschen kann er nicht wiederhergestellt werden. Bitte bestätigen Sie Ihre Aktion.", "copy": "Kopieren", "copyFail": "Kopieren fehlgeschlagen", "copySuccess": "Kopieren erfolgreich", @@ -128,6 +123,39 @@ "setting": "Einstellung", "share": "Teilen", "stop": "Stoppen", + "sync": { + "actions": { + "settings": "同步设置", + "sync": "立即同步" + }, + "awareness": { + "current": "当前设备" + }, + "channel": "频道", + "disabled": { + "actions": { + "enable": "开启云端同步", + "settings": "配置同步参数" + }, + "desc": "当前会话数据仅存储于此浏览器中。如果你需要在多个设备间同步数据,请配置并开启云端同步。", + "title": "数据同步未开启" + }, + "enabled": { + "title": "数据同步" + }, + "status": { + "connecting": "连接中", + "disabled": "同步未开启", + "ready": "已连接", + "synced": "已同步", + "syncing": "同步中", + "unconnected": "连接失败" + }, + "title": "同步状态", + "unconnected": { + "tip": "信令服务器连接失败,将无法建立点对点通信频道,请检查网络后重试" + } + }, "tab": { "chat": "Chat", "market": "Entdecken", @@ -147,4 +175,4 @@ "hasNew": "Neue Version verfügbar", "newVersion": "Neue Version verfügbar: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/de-DE/setting.json b/locales/de-DE/setting.json index 58a6548a99f7..57d5b5485f73 100644 --- a/locales/de-DE/setting.json +++ b/locales/de-DE/setting.json @@ -440,11 +440,47 @@ "placeholder": "Geben Sie die Kennung des Assistenten ein, die eindeutig sein muss, z. B. Web-Entwicklung", "tooltips": "Auf dem Assistentenmarkt teilen" }, + "sync": { + "device": { + "deviceName": { + "hint": "Fügen Sie einen Namen hinzu, um das Gerät zu identifizieren", + "placeholder": "Geben Sie den Gerätenamen ein", + "title": "Gerätename" + }, + "title": "Geräteinformationen", + "unknownBrowser": "Unbekannter Browser", + "unknownOS": "Unbekanntes Betriebssystem" + }, + "warning": { + "message": "Diese Funktion ist derzeit experimentell und kann unerwartete oder instabile Situationen aufweisen. Bitte geben Sie bei Problemen rechtzeitig Feedback ab." + }, + "webrtc": { + "channelName": { + "desc": "WebRTC verwendet diesen Namen, um einen Synchronisierungskanal zu erstellen. Stellen Sie sicher, dass der Kanalname eindeutig ist", + "placeholder": "Geben Sie den Synchronisierungskanalnamen ein", + "shuffle": "Zufällige Generierung", + "title": "Synchronisierungskanalname" + }, + "channelPassword": { + "desc": "Fügen Sie ein Passwort hinzu, um die Vertraulichkeit des Kanals zu gewährleisten. Nur wenn das Passwort korrekt ist, kann das Gerät dem Kanal beitreten", + "placeholder": "Geben Sie das Synchronisierungskennwort ein", + "title": "Synchronisierungskennwort" + }, + "desc": "Echtzeit, Punkt-zu-Punkt-Datenkommunikation, bei der die Geräte gleichzeitig online sein müssen, um synchronisiert zu werden", + "enabled": { + "invalid": "Bitte geben Sie zuerst den Synchronisierungskanalnamen ein, bevor Sie die Synchronisierung aktivieren", + "title": "Synchronisierung aktivieren" + }, + "title": "WebRTC-Synchronisierung" + } + }, "tab": { "about": "Über", "agent": "Standard-Assistent", "common": "Allgemeine Einstellungen", + "experiment": "Experiment", "llm": "Sprachmodell", + "sync": "Cloud-Synchronisierung", "tts": "Sprachdienste" }, "tools": { diff --git a/locales/en-US/common.json b/locales/en-US/common.json index f4e4e33d0227..7b892783237e 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -10,17 +10,12 @@ }, "about": "About", "advanceSettings": "Advanced Settings", - "agentMaxToken": "Max Session Length", - "agentModel": "Model", - "agentProfile": "Agent Profile", "appInitializing": "LobeChat is initializing, please wait...", - "archive": "Archive", "autoGenerate": "Auto Generate", "autoGenerateTooltip": "Auto-generate agent description based on prompts", "cancel": "Cancel", "changelog": "Changelog", "close": "Close", - "confirmRemoveSessionItemAlert": "You are about to delete this agent. Once deleted, it cannot be recovered. Please confirm your action.", "copy": "Copy", "copyFail": "Copy failed", "copySuccess": "Copied successfully", @@ -128,6 +123,39 @@ "setting": "Settings", "share": "Share", "stop": "Stop", + "sync": { + "actions": { + "settings": "Sync Settings", + "sync": "Sync Now" + }, + "awareness": { + "current": "Current Device" + }, + "channel": "Channel", + "disabled": { + "actions": { + "enable": "Enable Cloud Sync", + "settings": "Sync Settings" + }, + "desc": "Current session data is only stored in this browser. If you need to sync data across multiple devices, please configure and enable cloud sync.", + "title": "Data Sync Disabled" + }, + "enabled": { + "title": "Data Sync Enabled" + }, + "status": { + "connecting": "Connecting", + "disabled": "Sync Disabled", + "ready": "Connected", + "synced": "Synced", + "syncing": "Syncing", + "unconnected": "Connection Failed" + }, + "title": "Sync Status", + "unconnected": { + "tip": "Signaling server connection failed, and peer-to-peer communication channel cannot be established. Please check the network and try again." + } + }, "tab": { "chat": "Chat", "market": "Discover", @@ -147,4 +175,4 @@ "hasNew": "New update available", "newVersion": "New version available: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/en-US/setting.json b/locales/en-US/setting.json index 3d64008a651d..fa0c31690fe9 100644 --- a/locales/en-US/setting.json +++ b/locales/en-US/setting.json @@ -440,11 +440,47 @@ "placeholder": "Enter a unique identifier for the agent, e.g. web-development", "tooltips": "Share to the agent marketplace" }, + "sync": { + "device": { + "deviceName": { + "hint": "Add a name for easy identification", + "placeholder": "Enter device name", + "title": "Device Name" + }, + "title": "Device Information", + "unknownBrowser": "Unknown Browser", + "unknownOS": "Unknown OS" + }, + "warning": { + "message": "This feature is currently experimental and may have unexpected or unstable behavior. If you encounter any issues, please submit feedback promptly." + }, + "webrtc": { + "channelName": { + "desc": "WebRTC will use this name to create a sync channel. Ensure the channel name is unique.", + "placeholder": "Enter sync channel name", + "shuffle": "Generate Randomly", + "title": "Sync Channel Name" + }, + "channelPassword": { + "desc": "Add a password to ensure channel privacy. Only devices with the correct password can join the channel.", + "placeholder": "Enter sync channel password", + "title": "Sync Channel Password" + }, + "desc": "Real-time, peer-to-peer data communication requires all devices to be online for synchronization.", + "enabled": { + "invalid": "Please enter a sync channel name before enabling", + "title": "Enable Sync" + }, + "title": "WebRTC Sync" + } + }, "tab": { "about": "About", "agent": "Default Agent", "common": "Common Settings", + "experiment": "Experiment", "llm": "Language Model", + "sync": "Cloud Sync", "tts": "Text-to-Speech" }, "tools": { diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 40eda49e3bcb..2e705306c9c9 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -10,17 +10,12 @@ }, "about": "Acerca de", "advanceSettings": "Configuración avanzada", - "agentMaxToken": "Máximo de tokens de sesión", - "agentModel": "Modelo", - "agentProfile": "Perfil del asistente", "appInitializing": "LobeChat está inicializando, por favor espere...", - "archive": "Archivar", "autoGenerate": "Generación automática", "autoGenerateTooltip": "Completar automáticamente la descripción del asistente basándose en las sugerencias", "cancel": "Cancelar", "changelog": "Registro de cambios", "close": "Cerrar", - "confirmRemoveSessionItemAlert": "Estás a punto de eliminar este asistente. Una vez eliminado, no se podrá recuperar. Por favor, confirma tu acción", "copy": "Copiar", "copyFail": "Fallo al copiar", "copySuccess": "¡Copia exitosa!", @@ -128,6 +123,39 @@ "setting": "Configuración", "share": "Compartir", "stop": "Detener", + "sync": { + "actions": { + "settings": "Configuración de sincronización", + "sync": "Sincronizar ahora" + }, + "awareness": { + "current": "Dispositivo actual" + }, + "channel": "Canal", + "disabled": { + "actions": { + "enable": "Habilitar sincronización en la nube", + "settings": "Configurar parámetros de sincronización" + }, + "desc": "Los datos de esta sesión se almacenan solo en este navegador. Si necesitas sincronizar datos entre varios dispositivos, configura y habilita la sincronización en la nube.", + "title": "Sincronización de datos deshabilitada" + }, + "enabled": { + "title": "Sincronización de datos" + }, + "status": { + "connecting": "Conectando", + "disabled": "Sincronización deshabilitada", + "ready": "Listo", + "synced": "Sincronizado", + "syncing": "Sincronizando", + "unconnected": "Sin conexión" + }, + "title": "Estado de sincronización", + "unconnected": { + "tip": "Fallo al conectar con el servidor de señal. No se podrá establecer un canal de comunicación punto a punto. Por favor, verifica la red e inténtalo de nuevo." + } + }, "tab": { "chat": "Chat", "market": "Descubrir", @@ -147,4 +175,4 @@ "hasNew": "Hay una nueva actualización disponible", "newVersion": "Nueva versión disponible: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/es-ES/setting.json b/locales/es-ES/setting.json index 3bfcf43c6d97..2416f3032660 100644 --- a/locales/es-ES/setting.json +++ b/locales/es-ES/setting.json @@ -440,11 +440,47 @@ "placeholder": "Ingrese el identificador único del asistente, por ejemplo desarrollo-web", "tooltips": "Compartir en el mercado de asistentes" }, + "sync": { + "device": { + "deviceName": { + "hint": "Agrega un nombre para identificar el dispositivo", + "placeholder": "Introduce el nombre del dispositivo", + "title": "Nombre del dispositivo" + }, + "title": "Información del dispositivo", + "unknownBrowser": "Navegador desconocido", + "unknownOS": "Sistema operativo desconocido" + }, + "warning": { + "message": "Esta función todavía está en fase experimental y puede presentar situaciones inesperadas o inestables. Si encuentras algún problema, por favor envía tus comentarios de inmediato." + }, + "webrtc": { + "channelName": { + "desc": "WebRTC utilizará este nombre para crear un canal de sincronización. Asegúrate de que el nombre del canal sea único", + "placeholder": "Introduce el nombre del canal de sincronización", + "shuffle": "Generar aleatoriamente", + "title": "Nombre del canal de sincronización" + }, + "channelPassword": { + "desc": "Agrega una contraseña para garantizar la privacidad del canal. Solo los dispositivos con la contraseña correcta podrán unirse al canal", + "placeholder": "Introduce la contraseña del canal de sincronización", + "title": "Contraseña del canal de sincronización" + }, + "desc": "Comunicación de datos en tiempo real y punto a punto. Los dispositivos deben estar en línea simultáneamente para sincronizarse", + "enabled": { + "invalid": "Por favor, introduce el nombre del canal de sincronización antes de activar", + "title": "Activar sincronización" + }, + "title": "Sincronización WebRTC" + } + }, "tab": { "about": "Acerca de", "agent": "Asistente predeterminado", "common": "Configuración común", + "experiment": "Experimento", "llm": "Modelo de lenguaje", + "sync": "Sincronización en la nube", "tts": "Servicio de voz" }, "tools": { diff --git a/locales/fr-FR/common.json b/locales/fr-FR/common.json index e0334287bc46..2fda6b8c6ed7 100644 --- a/locales/fr-FR/common.json +++ b/locales/fr-FR/common.json @@ -10,17 +10,12 @@ }, "about": "À propos", "advanceSettings": "Paramètres avancés", - "agentMaxToken": "Longueur maximale de la session", - "agentModel": "Modèle", - "agentProfile": "Profil de l'agent", "appInitializing": "LobeChat est en cours de démarrage, veuillez patienter...", - "archive": "Archiver", "autoGenerate": "Générer automatiquement", "autoGenerateTooltip": "Générer automatiquement la description de l'agent basée sur les suggestions", "cancel": "Annuler", "changelog": "Journal des modifications", "close": "Fermer", - "confirmRemoveSessionItemAlert": "Vous êtes sur le point de supprimer cet agent. Une fois supprimé, il ne pourra pas être récupéré. Veuillez confirmer votre action.", "copy": "Copier", "copyFail": "Échec de la copie", "copySuccess": "Copie réussie", @@ -128,6 +123,39 @@ "setting": "Paramètre", "share": "Partager", "stop": "Arrêter", + "sync": { + "actions": { + "settings": "Paramètres de synchronisation", + "sync": "Synchroniser maintenant" + }, + "awareness": { + "current": "Appareil actuel" + }, + "channel": "Canal", + "disabled": { + "actions": { + "enable": "Activer la synchronisation cloud", + "settings": "Paramètres de configuration" + }, + "desc": "Les données de cette session sont uniquement stockées dans ce navigateur. Si vous avez besoin de synchroniser les données entre plusieurs appareils, veuillez configurer et activer la synchronisation cloud.", + "title": "La synchronisation des données n'est pas activée" + }, + "enabled": { + "title": "Synchronisation des données" + }, + "status": { + "connecting": "Connexion en cours", + "disabled": "Synchronisation désactivée", + "ready": "Connecté", + "synced": "Synchronisé", + "syncing": "Synchronisation en cours", + "unconnected": "Échec de la connexion" + }, + "title": "État de synchronisation", + "unconnected": { + "tip": "Échec de la connexion au serveur de signalisation. Impossible d'établir un canal de communication peer-to-peer. Veuillez vérifier votre réseau et réessayer." + } + }, "tab": { "chat": "Conversation", "market": "Découvrir", @@ -147,4 +175,4 @@ "hasNew": "Nouvelle mise à jour disponible", "newVersion": "Nouvelle version disponible : {{version}}" } -} \ No newline at end of file +} diff --git a/locales/fr-FR/setting.json b/locales/fr-FR/setting.json index a7472533683c..b69a34932bd6 100644 --- a/locales/fr-FR/setting.json +++ b/locales/fr-FR/setting.json @@ -440,11 +440,47 @@ "placeholder": "Veuillez entrer l'identifiant de l'agent, qui doit être unique, par exemple développement-web", "tooltips": "Partager sur le marché des agents" }, + "sync": { + "device": { + "deviceName": { + "hint": "Ajoutez un nom pour l'identifier", + "placeholder": "Entrez le nom de l'appareil", + "title": "Nom de l'appareil" + }, + "title": "Informations sur l'appareil", + "unknownBrowser": "Navigateur inconnu", + "unknownOS": "Système d'exploitation inconnu" + }, + "warning": { + "message": "Cette fonctionnalité est actuellement expérimentale et peut présenter des comportements inattendus ou instables. En cas de problème, veuillez soumettre vos commentaires rapidement." + }, + "webrtc": { + "channelName": { + "desc": "WebRTC utilisera ce nom pour créer un canal de synchronisation. Assurez-vous que le nom du canal est unique", + "placeholder": "Entrez le nom du canal de synchronisation", + "shuffle": "Générer aléatoirement", + "title": "Nom du canal de synchronisation" + }, + "channelPassword": { + "desc": "Ajoutez un mot de passe pour assurer la confidentialité du canal. Seuls les appareils avec le bon mot de passe pourront rejoindre le canal", + "placeholder": "Entrez le mot de passe du canal de synchronisation", + "title": "Mot de passe du canal de synchronisation" + }, + "desc": "Communication de données en temps réel et en pair-à-pair. Les appareils doivent être en ligne simultanément pour se synchroniser", + "enabled": { + "invalid": "Veuillez entrer un nom de canal de synchronisation avant d'activer", + "title": "Activer la synchronisation" + }, + "title": "Synchronisation WebRTC" + } + }, "tab": { "about": "À propos", "agent": "Agent par défaut", "common": "Paramètres généraux", + "experiment": "Expérience", "llm": "Modèle de langue", + "sync": "Synchronisation cloud", "tts": "Service vocal" }, "tools": { diff --git a/locales/it-IT/common.json b/locales/it-IT/common.json index 430aaa986d50..e34929bf8680 100644 --- a/locales/it-IT/common.json +++ b/locales/it-IT/common.json @@ -10,17 +10,12 @@ }, "about": "Informazioni", "advanceSettings": "Impostazioni avanzate", - "agentMaxToken": "Massima lunghezza della sessione", - "agentModel": "Modello", - "agentProfile": "Profilo assistente", "appInitializing": "LobeChat inizializzazione in corso, attendere prego...", - "archive": "Archivio", "autoGenerate": "Generazione automatica", "autoGenerateTooltip": "Completamento automatico basato su suggerimenti", "cancel": "Annulla", "changelog": "Registro modifiche", "close": "Chiudi", - "confirmRemoveSessionItemAlert": "Stai per eliminare questo assistente. Una volta eliminato, non sarà possibile recuperarlo. Confermi l'operazione?", "copy": "Copia", "copyFail": "Copia non riuscita", "copySuccess": "Copia riuscita", @@ -128,6 +123,39 @@ "setting": "Impostazioni", "share": "Condividi", "stop": "Ferma", + "sync": { + "actions": { + "settings": "Impostazioni di sincronizzazione", + "sync": "Sincronizza ora" + }, + "awareness": { + "current": "Dispositivo corrente" + }, + "channel": "Canale", + "disabled": { + "actions": { + "enable": "Abilita la sincronizzazione cloud", + "settings": "Configura le impostazioni di sincronizzazione" + }, + "desc": "I dati della sessione corrente sono memorizzati solo in questo browser. Se hai bisogno di sincronizzare i dati tra più dispositivi, configura e abilita la sincronizzazione cloud.", + "title": "Sincronizzazione dati disabilitata" + }, + "enabled": { + "title": "Sincronizzazione dati" + }, + "status": { + "connecting": "Connessione in corso", + "disabled": "Sincronizzazione disabilitata", + "ready": "Pronto", + "synced": "Sincronizzato", + "syncing": "Sincronizzazione in corso", + "unconnected": "Connessione non riuscita" + }, + "title": "Stato di sincronizzazione", + "unconnected": { + "tip": "Connessione al server di segnalazione non riuscita. Impossibile stabilire un canale di comunicazione punto a punto. Controlla la rete e riprova." + } + }, "tab": { "chat": "Chat", "market": "Scopri", @@ -147,4 +175,4 @@ "hasNew": "Nuovo aggiornamento disponibile", "newVersion": "Nuova versione disponibile: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/it-IT/setting.json b/locales/it-IT/setting.json index 4991c7f76c80..4390ce64532a 100644 --- a/locales/it-IT/setting.json +++ b/locales/it-IT/setting.json @@ -440,6 +440,44 @@ "placeholder": "Inserisci l'identificatore dell'assistente, deve essere univoco, ad esempio sviluppo-web", "tooltips": "Condividi sul mercato degli assistenti" }, + "sync": { + "device": { + "deviceName": { + "hint": "Aggiungi un nome per identificare il dispositivo", + "placeholder": "Inserisci il nome del dispositivo", + "title": "Nome del dispositivo" + }, + "title": "Informazioni sul dispositivo", + "unknownBrowser": "Browser sconosciuto", + "unknownOS": "Sistema operativo sconosciuto" + }, + "tab": { + "experiment": "Esperimento", + "sync": "Sincronizzazione cloud" + }, + "warning": { + "message": "Questa funzione è attualmente sperimentale e potrebbe comportare comportamenti imprevisti o instabili. In caso di problemi, invia tempestivamente un feedback.", + "webrtc": { + "channelName": { + "desc": "WebRTC utilizzerà questo nome per creare un canale di sincronizzazione. Assicurati che il nome del canale sia univoco", + "placeholder": "Inserisci il nome del canale di sincronizzazione", + "shuffle": "Genera casuale", + "title": "Nome del canale di sincronizzazione" + }, + "channelPassword": { + "desc": "Aggiungi una password per garantire la privacy del canale. Solo con la password corretta i dispositivi potranno unirsi al canale", + "placeholder": "Inserisci la password del canale di sincronizzazione", + "title": "Password del canale di sincronizzazione" + }, + "desc": "Comunicazione dati in tempo reale e punto-punto. I dispositivi devono essere online contemporaneamente per sincronizzarsi", + "enabled": { + "invalid": "Inserisci prima il nome del canale di sincronizzazione per abilitare", + "title": "Abilita la sincronizzazione" + }, + "title": "Sincronizzazione WebRTC" + } + } + }, "tab": { "about": "Informazioni", "agent": "Assistente predefinito", diff --git a/locales/ja-JP/common.json b/locales/ja-JP/common.json index e50ba34da8a3..0bc745c40989 100644 --- a/locales/ja-JP/common.json +++ b/locales/ja-JP/common.json @@ -10,17 +10,12 @@ }, "about": "概要", "advanceSettings": "高度な設定", - "agentMaxToken": "エージェントの最大トークン数", - "agentModel": "モデル", - "agentProfile": "エージェントプロフィール", "appInitializing": "LobeChatを初期化中です。しばらくお待ちください...", - "archive": "アーカイブ", "autoGenerate": "自動生成", "autoGenerateTooltip": "ヒントに基づいてエージェントの説明を自動生成します", "cancel": "キャンセル", "changelog": "変更履歴", "close": "閉じる", - "confirmRemoveSessionItemAlert": "このエージェントを削除します。削除後は元に戻すことはできません。操作を確認してください。", "copy": "コピー", "copyFail": "コピーに失敗しました", "copySuccess": "コピーが成功しました", @@ -128,6 +123,39 @@ "setting": "設定", "share": "共有", "stop": "停止", + "sync": { + "actions": { + "settings": "同期設定", + "sync": "即時同期" + }, + "awareness": { + "current": "現在のデバイス" + }, + "channel": "チャンネル", + "disabled": { + "actions": { + "enable": "クラウド同期を有効にする", + "settings": "同期設定" + }, + "desc": "現在のセッションデータはこのブラウザにのみ保存されます。複数のデバイス間でデータを同期するには、クラウド同期を設定して有効にしてください。", + "title": "データ同期が無効です" + }, + "enabled": { + "title": "データ同期" + }, + "status": { + "connecting": "接続中", + "disabled": "同期が無効です", + "ready": "接続済み", + "synced": "同期済み", + "syncing": "同期中", + "unconnected": "接続失敗" + }, + "title": "同期状態", + "unconnected": { + "tip": "シグナリングサーバーに接続できません。ピア間通信チャンネルを確立できません。ネットワークを確認して再試行してください。" + } + }, "tab": { "chat": "チャット", "market": "探す", @@ -147,4 +175,4 @@ "hasNew": "利用可能な更新があります", "newVersion": "新しいバージョンが利用可能です:{{version}}" } -} \ No newline at end of file +} diff --git a/locales/ja-JP/setting.json b/locales/ja-JP/setting.json index cf1dfa12b8a3..6a842ff4486d 100644 --- a/locales/ja-JP/setting.json +++ b/locales/ja-JP/setting.json @@ -440,6 +440,44 @@ "placeholder": "エージェントの識別子を入力してください。一意である必要があります。例:web-development", "tooltips": "エージェントマーケットに共有" }, + "sync": { + "device": { + "deviceName": { + "hint": "識別のために名前を追加", + "placeholder": "デバイス名を入力", + "title": "デバイス名" + }, + "title": "デバイス情報", + "unknownBrowser": "不明なブラウザ", + "unknownOS": "不明なOS" + }, + "tab": { + "experiment": "実験", + "sync": "クラウド同期" + }, + "warning": { + "message": "この機能は現在実験的なものであり、予期しない不安定な状況が発生する可能性があります。問題が発生した場合はフィードバックを提出してください。", + "webrtc": { + "channelName": { + "desc": "WebRTC はこの名前で同期チャネルを作成します。チャネル名が一意であることを確認してください。", + "placeholder": "同期チャネル名を入力", + "shuffle": "ランダム生成", + "title": "同期チャネル名" + }, + "channelPassword": { + "desc": "チャネルのプライバシーを保護するためにパスワードを追加し、デバイスがチャネルに参加できるのはパスワードが正しい場合のみです。", + "placeholder": "同期チャネルのパスワードを入力", + "title": "同期チャネルのパスワード" + }, + "desc": "リアルタイムでピアツーピアのデータ通信であり、デバイスが同時にオンラインである必要があります。", + "enabled": { + "invalid": "同期チャネル名を入力してから有効にしてください", + "title": "同期を有効にする" + }, + "title": "WebRTC 同期" + } + } + }, "tab": { "about": "について", "agent": "デフォルトエージェント", diff --git a/locales/ko-KR/common.json b/locales/ko-KR/common.json index 11bb25bb9e49..941541d4a4de 100644 --- a/locales/ko-KR/common.json +++ b/locales/ko-KR/common.json @@ -10,17 +10,12 @@ }, "about": "소개", "advanceSettings": "고급 설정", - "agentMaxToken": "최대 대화 길이", - "agentModel": "모델", - "agentProfile": "에이전트 프로필", "appInitializing": "LobeChat이 초기화 중입니다. 잠시 기다려주세요...", - "archive": "보관", "autoGenerate": "자동 생성", "autoGenerateTooltip": "힌트 단어를 기반으로 에이전트 설명을 자동으로 완성합니다", "cancel": "취소", "changelog": "변경 로그", "close": "닫기", - "confirmRemoveSessionItemAlert": "이 에이전트를 삭제하려고 합니다. 삭제 후에는 복구할 수 없습니다. 작업을 확인하십시오.", "copy": "복사", "copyFail": "복사 실패", "copySuccess": "복사 성공", @@ -128,6 +123,39 @@ "setting": "설정", "share": "공유", "stop": "중지", + "sync": { + "actions": { + "settings": "동기화 설정", + "sync": "즉시 동기화" + }, + "awareness": { + "current": "현재 장치" + }, + "channel": "채널", + "disabled": { + "actions": { + "enable": "클라우드 동기화 활성화", + "settings": "동기화 설정 구성" + }, + "desc": "현재 세션 데이터는 이 브라우저에만 저장됩니다. 여러 장치 간에 데이터를 동기화해야 하는 경우 클라우드 동기화를 구성하고 활성화하세요.", + "title": "데이터 동기화가 비활성화됨" + }, + "enabled": { + "title": "데이터 동기화" + }, + "status": { + "connecting": "연결 중", + "disabled": "동기화가 비활성화됨", + "ready": "연결됨", + "synced": "동기화됨", + "syncing": "동기화 중", + "unconnected": "연결 실패" + }, + "title": "동기화 상태", + "unconnected": { + "tip": "시그널 서버 연결 실패로 인해 피어 투 피어 통신 채널을 설정할 수 없습니다. 네트워크를 확인한 후 다시 시도하세요." + } + }, "tab": { "chat": "채팅", "market": "발견", @@ -147,4 +175,4 @@ "hasNew": "사용 가능한 업데이트가 있습니다", "newVersion": "새 버전 사용 가능: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/ko-KR/setting.json b/locales/ko-KR/setting.json index dc197e1185ac..da45e72000a5 100644 --- a/locales/ko-KR/setting.json +++ b/locales/ko-KR/setting.json @@ -440,11 +440,47 @@ "placeholder": "에이전트 식별자를 입력하세요. 고유해야 하며, 예: 웹 개발", "tooltips": "에이전트 마켓에 공유" }, + "sync": { + "device": { + "deviceName": { + "hint": "추가 이름을 입력하여 식별할 수 있게 합니다", + "placeholder": "장치 이름을 입력하세요", + "title": "장치 이름" + }, + "title": "장치 정보", + "unknownBrowser": "알 수 없는 브라우저", + "unknownOS": "알 수 없는 OS" + }, + "warning": { + "message": "이 기능은 현재 실험적인 기능으로 예기치 않거나 불안정한 상황이 발생할 수 있으며 문제가 발생하면 즉시 피드백을 제출해 주세요." + }, + "webrtc": { + "channelName": { + "desc": "WebRTC는 이 이름을 사용하여 동기화 채널을 생성하며 채널 이름이 고유한지 확인하세요", + "placeholder": "동기화 채널 이름을 입력하세요", + "shuffle": "랜덤 생성", + "title": "동기화 채널 이름" + }, + "channelPassword": { + "desc": "채널의 개인 정보를 보호하기 위해 비밀번호를 추가하고 장치가 채널에 참여하려면 올바른 비밀번호여야 합니다", + "placeholder": "동기화 채널 비밀번호를 입력하세요", + "title": "동기화 채널 비밀번호" + }, + "desc": "실시간, 피어 투 피어 데이터 통신으로 장치가 동시에 온라인 상태여야만 동기화할 수 있습니다", + "enabled": { + "invalid": "동기화 채널 이름을 입력한 후에 활성화하세요", + "title": "동기화 활성화" + }, + "title": "WebRTC 동기화" + } + }, "tab": { "about": "소개", "agent": "기본 에이전트", "common": "일반 설정", + "experiment": "실험", "llm": "언어 모델", + "sync": "클라우드 동기화", "tts": "음성 서비스" }, "tools": { diff --git a/locales/nl-NL/common.json b/locales/nl-NL/common.json index eba607b52206..3d69b708cfa2 100644 --- a/locales/nl-NL/common.json +++ b/locales/nl-NL/common.json @@ -10,17 +10,12 @@ }, "about": "Over", "advanceSettings": "Geavanceerde instellingen", - "agentMaxToken": "Maximale sessielengte", - "agentModel": "Model", - "agentProfile": "Assistentprofiel", "appInitializing": "LobeChat wordt geïnitialiseerd, even geduld a.u.b...", - "archive": "Archief", "autoGenerate": "Automatisch genereren", "autoGenerateTooltip": "Automatisch assistentbeschrijving genereren op basis van suggesties", "cancel": "Annuleren", "changelog": "Wijzigingslogboek", "close": "Sluiten", - "confirmRemoveSessionItemAlert": "U staat op het punt deze assistent te verwijderen. Na verwijdering kan deze niet worden hersteld. Weet u zeker dat u door wilt gaan?", "copy": "Kopiëren", "copyFail": "Kopiëren mislukt", "copySuccess": "Kopiëren gelukt", @@ -128,6 +123,39 @@ "setting": "设置", "share": "分享", "stop": "停止", + "sync": { + "actions": { + "settings": "同步设置", + "sync": "立即同步" + }, + "awareness": { + "current": "当前设备" + }, + "channel": "频道", + "disabled": { + "actions": { + "enable": "开启云端同步", + "settings": "配置同步参数" + }, + "desc": "当前会话数据仅存储于此浏览器中。如果你需要在多个设备间同步数据,请配置并开启云端同步。", + "title": "数据同步未开启" + }, + "enabled": { + "title": "数据同步" + }, + "status": { + "connecting": "连接中", + "disabled": "同步未开启", + "ready": "已连接", + "synced": "已同步", + "syncing": "同步中", + "unconnected": "连接失败" + }, + "title": "同步状态", + "unconnected": { + "tip": "信令服务器连接失败,将无法建立点对点通信频道,请检查网络后重试" + } + }, "tab": { "chat": "会话", "market": "发现", @@ -147,4 +175,4 @@ "hasNew": "有可用更新", "newVersion": "有新版本可用:{{version}}" } -} \ No newline at end of file +} diff --git a/locales/nl-NL/setting.json b/locales/nl-NL/setting.json index 75ef766768b6..a640dfac135a 100644 --- a/locales/nl-NL/setting.json +++ b/locales/nl-NL/setting.json @@ -440,6 +440,44 @@ "placeholder": "Voer de identificatie van de assistent in, deze moet uniek zijn, bijvoorbeeld web-ontwikkeling", "tooltips": "Delen op de assistentenmarkt" }, + "sync": { + "device": { + "deviceName": { + "hint": "添加名称以便于识别", + "placeholder": "Voer de apparaatnaam in", + "title": "Apparaatnaam" + }, + "title": "Apparaatinformatie", + "unknownBrowser": "Onbekende browser", + "unknownOS": "Onbekend besturingssysteem" + }, + "tab": { + "experiment": "Experiment", + "sync": "Cloudsynchronisatie" + }, + "warning": { + "message": "Deze functie is momenteel experimenteel en kan onverwachte of onstabiele situaties veroorzaken. Als u problemen ondervindt, geef dan tijdig feedback.", + "webrtc": { + "channelName": { + "desc": "WebRTC zal dit gebruiken om een synchronisatiekanaal te maken. Zorg ervoor dat de kanaalnaam uniek is", + "placeholder": "Voer de synchronisatiekanaalnaam in", + "shuffle": "Willekeurig genereren", + "title": "Synchronisatiekanaalnaam" + }, + "channelPassword": { + "desc": "Voeg een wachtwoord toe om de privacy van het kanaal te waarborgen. Alleen met het juiste wachtwoord kan het apparaat aan het kanaal deelnemen", + "placeholder": "Voer het synchronisatiekanaalwachtwoord in", + "title": "Synchronisatiekanaalwachtwoord" + }, + "desc": "Realtime, point-to-point datacommunicatie. Apparaten moeten tegelijkertijd online zijn om te synchroniseren", + "enabled": { + "invalid": "Schakel de synchronisatie in nadat u de synchronisatiekanaalnaam heeft ingevoerd", + "title": "Synchronisatie inschakelen" + }, + "title": "WebRTC-synchronisatie" + } + } + }, "tab": { "about": "Over", "agent": "Standaardassistent", diff --git a/locales/pl-PL/common.json b/locales/pl-PL/common.json index 315674735a55..0d0440985c99 100644 --- a/locales/pl-PL/common.json +++ b/locales/pl-PL/common.json @@ -10,17 +10,12 @@ }, "about": "O nas", "advanceSettings": "Zaawansowane ustawienia", - "agentMaxToken": "Maksymalna długość sesji", - "agentModel": "Model", - "agentProfile": "Profil asystenta", "appInitializing": "LobeChat inicjuje, proszę czekać...", - "archive": "Archiwum", "autoGenerate": "Automatyczne generowanie", "autoGenerateTooltip": "Automatyczne uzupełnianie opisu asystenta na podstawie sugestii", "cancel": "Anuluj", "changelog": "Dziennik zmian", "close": "Zamknij", - "confirmRemoveSessionItemAlert": "Czy na pewno chcesz usunąć tego asystenta? Po usunięciu nie będzie możliwe jego odzyskanie.", "copy": "Kopiuj", "copyFail": "Nie udało się skopiować", "copySuccess": "Skopiowano pomyślnie", @@ -128,6 +123,39 @@ "setting": "Ustawienia", "share": "Udostępnij", "stop": "Zatrzymaj", + "sync": { + "actions": { + "settings": "Ustawienia synchronizacji", + "sync": "Synchronizuj teraz" + }, + "awareness": { + "current": "Bieżące urządzenie" + }, + "channel": "Kanał", + "disabled": { + "actions": { + "enable": "Włącz synchronizację chmurową", + "settings": "Konfiguruj parametry synchronizacji" + }, + "desc": "Dane bieżącej sesji są przechowywane tylko w tej przeglądarce. Jeśli chcesz synchronizować dane między wieloma urządzeniami, skonfiguruj i włącz synchronizację chmurową.", + "title": "Synchronizacja danych wyłączona" + }, + "enabled": { + "title": "Synchronizacja danych" + }, + "status": { + "connecting": "Łączenie", + "disabled": "Synchronizacja wyłączona", + "ready": "Gotowy", + "synced": "Synchronizacja zakończona", + "syncing": "Synchronizacja w toku", + "unconnected": "Brak połączenia" + }, + "title": "Stan synchronizacji", + "unconnected": { + "tip": "Błąd połączenia z serwerem sygnalizacyjnym, nie można nawiązać kanału komunikacyjnego punkt-punkt, sprawdź sieć i spróbuj ponownie" + } + }, "tab": { "chat": "Czat", "market": "Odkrywaj", @@ -147,4 +175,4 @@ "hasNew": "Dostępna jest nowa aktualizacja", "newVersion": "Dostępna jest nowa wersja: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/pl-PL/setting.json b/locales/pl-PL/setting.json index 0453a49013aa..c84b069f499d 100644 --- a/locales/pl-PL/setting.json +++ b/locales/pl-PL/setting.json @@ -440,11 +440,47 @@ "placeholder": "Wprowadź identyfikator asystenta, musi być unikalny, na przykład web-development", "tooltips": "Udostępnij na rynku asystentów" }, + "sync": { + "device": { + "deviceName": { + "hint": "添加名称以便于识别", + "placeholder": "请输入设备名称", + "title": "设备名称" + }, + "title": "设备信息", + "unknownBrowser": "未知浏览器", + "unknownOS": "未知系统" + }, + "warning": { + "message": "本功能目前仍为实验性功能,可能存在预期外或不稳定的情况,如遇到问题请及时提交反馈。" + }, + "webrtc": { + "channelName": { + "desc": "WebRTC 将使用此名创建同步频道,确保频道名称唯一", + "placeholder": "请输入同步频道名称", + "shuffle": "随机生成", + "title": "同步频道名称" + }, + "channelPassword": { + "desc": "添加密码确保频道私密性,只有密码正确时,设备才可加入频道", + "placeholder": "请输入同步频道密码", + "title": "同步频道密码" + }, + "desc": "实时、点对点的数据通信,需设备同时在线才可同步", + "enabled": { + "invalid": "请填写同步频道名称后再开启", + "title": "开启同步" + }, + "title": "WebRTC 同步" + } + }, "tab": { "about": "O nas", "agent": "Domyślny asystent", "common": "Ustawienia ogólne", + "experiment": "实验", "llm": "Model językowy", + "sync": "云端同步", "tts": "Usługa głosowa" }, "tools": { diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 76f1512e6856..1b2ebb350e3b 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -10,17 +10,12 @@ }, "about": "Sobre", "advanceSettings": "Configurações avançadas", - "agentMaxToken": "Máximo de tokens da sessão", - "agentModel": "Modelo", - "agentProfile": "Perfil do assistente", "appInitializing": "LobeChat inicializando, por favor aguarde...", - "archive": "Arquivar", "autoGenerate": "Auto completar", "autoGenerateTooltip": "Auto completar descrição do assistente com base em sugestões", "cancel": "Cancelar", "changelog": "Registro de alterações", "close": "Fechar", - "confirmRemoveSessionItemAlert": "Você está prestes a excluir este assistente. Após a exclusão, não será possível recuperá-lo. Por favor, confirme sua ação.", "copy": "Copiar", "copyFail": "Falha ao copiar", "copySuccess": "Cópia bem-sucedida", @@ -128,6 +123,39 @@ "setting": "Configuração", "share": "Compartilhar", "stop": "Parar", + "sync": { + "actions": { + "settings": "Configurações de Sincronização", + "sync": "Sincronizar Agora" + }, + "awareness": { + "current": "Dispositivo Atual" + }, + "channel": "Canal", + "disabled": { + "actions": { + "enable": "Habilitar Sincronização na Nuvem", + "settings": "Configurar Parâmetros de Sincronização" + }, + "desc": "Os dados da sessão atual são armazenados apenas neste navegador. Se você precisa sincronizar os dados entre vários dispositivos, configure e habilite a sincronização na nuvem.", + "title": "Sincronização de Dados Desativada" + }, + "enabled": { + "title": "Sincronização de Dados" + }, + "status": { + "connecting": "Conectando", + "disabled": "Sincronização Desativada", + "ready": "Conectado", + "synced": "Sincronizado", + "syncing": "Sincronizando", + "unconnected": "Falha na Conexão" + }, + "title": "Status de Sincronização", + "unconnected": { + "tip": "Falha na conexão com o servidor de sinalização. Não será possível estabelecer um canal de comunicação ponto a ponto. Verifique a rede e tente novamente." + } + }, "tab": { "chat": "Chat", "market": "Descobrir", @@ -147,4 +175,4 @@ "hasNew": "Nova atualização disponível", "newVersion": "Nova versão disponível: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/pt-BR/setting.json b/locales/pt-BR/setting.json index f5db91a7082f..47b6af1aecd3 100644 --- a/locales/pt-BR/setting.json +++ b/locales/pt-BR/setting.json @@ -440,11 +440,47 @@ "placeholder": "Insira o identificador único do assistente, como por exemplo, desenvolvimento-web", "tooltips": "Compartilhar no mercado de assistentes" }, + "sync": { + "device": { + "deviceName": { + "hint": "Adicione um nome para facilitar a identificação", + "placeholder": "Insira o nome do dispositivo", + "title": "Nome do dispositivo" + }, + "title": "Informações do dispositivo", + "unknownBrowser": "Navegador desconhecido", + "unknownOS": "Sistema operacional desconhecido" + }, + "warning": { + "message": "Esta função ainda é experimental e pode apresentar comportamento inesperado ou instável. Se encontrar problemas, envie um feedback imediatamente." + }, + "webrtc": { + "channelName": { + "desc": "O WebRTC usará este nome para criar um canal de sincronização. Certifique-se de que o nome do canal seja único", + "placeholder": "Insira o nome do canal de sincronização", + "shuffle": "Gerar aleatoriamente", + "title": "Nome do canal de sincronização" + }, + "channelPassword": { + "desc": "Adicione uma senha para garantir a privacidade do canal. Apenas com a senha correta os dispositivos poderão ingressar no canal", + "placeholder": "Insira a senha do canal de sincronização", + "title": "Senha do canal de sincronização" + }, + "desc": "Comunicação de dados em tempo real ponto a ponto. Os dispositivos precisam estar online simultaneamente para sincronizar", + "enabled": { + "invalid": "Por favor, insira o nome do canal de sincronização antes de ativar", + "title": "Ativar sincronização" + }, + "title": "Sincronização WebRTC" + } + }, "tab": { "about": "Sobre", "agent": "Assistente Padrão", "common": "Configurações Comuns", + "experiment": "Experimento", "llm": "Modelo de Linguagem", + "sync": "Sincronização na nuvem", "tts": "Serviço de Voz" }, "tools": { diff --git a/locales/ru-RU/common.json b/locales/ru-RU/common.json index 0f819d09a611..64bc747e0c56 100644 --- a/locales/ru-RU/common.json +++ b/locales/ru-RU/common.json @@ -10,17 +10,12 @@ }, "about": "О нас", "advanceSettings": "Расширенные настройки", - "agentMaxToken": "Максимальная длина сессии", - "agentModel": "Модель", - "agentProfile": "Профиль агента", "appInitializing": "LobeChat запускается, пожалуйста, подождите…", - "archive": "Архив", "autoGenerate": "Автозаполнение", "autoGenerateTooltip": "Автоматическое дополнение описания агента на основе подсказок", "cancel": "Отмена", "changelog": "История изменений", "close": "Закрыть", - "confirmRemoveSessionItemAlert": "Вы собираетесь удалить этого агента. После удаления его будет невозможно восстановить. Подтвердите ваше действие", "copy": "Копировать", "copyFail": "Не удалось скопировать", "copySuccess": "Успешно скопировано", @@ -128,6 +123,39 @@ "setting": "Настройка", "share": "Поделиться", "stop": "Остановить", + "sync": { + "actions": { + "settings": "Настройки синхронизации", + "sync": "Синхронизировать сейчас" + }, + "awareness": { + "current": "Текущее устройство" + }, + "channel": "Канал", + "disabled": { + "actions": { + "enable": "Включить облачную синхронизацию", + "settings": "Настройки синхронизации" + }, + "desc": "Данные текущей сессии хранятся только в этом браузере. Если вам нужно синхронизировать данные между несколькими устройствами, настройте и включите облачную синхронизацию.", + "title": "Синхронизация данных не включена" + }, + "enabled": { + "title": "Синхронизация данных" + }, + "status": { + "connecting": "Подключение", + "disabled": "Синхронизация не включена", + "ready": "Готово", + "synced": "Синхронизировано", + "syncing": "Синхронизация", + "unconnected": "Соединение не установлено" + }, + "title": "Статус синхронизации", + "unconnected": { + "tip": "Не удалось подключиться к серверу сигнализации. Невозможно установить канал для прямого обмена сообщениями. Пожалуйста, проверьте сеть и повторите попытку" + } + }, "tab": { "chat": "Чат", "market": "Обзор", @@ -147,4 +175,4 @@ "hasNew": "Доступно обновление", "newVersion": "Доступна новая версия: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/ru-RU/setting.json b/locales/ru-RU/setting.json index 073c79ca738b..79cbc1ac198a 100644 --- a/locales/ru-RU/setting.json +++ b/locales/ru-RU/setting.json @@ -440,11 +440,47 @@ "placeholder": "Введите уникальный идентификатор агента, например, 'web-development'", "tooltips": "Поделиться в магазине агентов" }, + "sync": { + "device": { + "deviceName": { + "hint": "添加名称以便于识别", + "placeholder": "请输入设备名称", + "title": "设备名称" + }, + "title": "设备信息", + "unknownBrowser": "未知浏览器", + "unknownOS": "未知系统" + }, + "warning": { + "message": "本功能目前仍为实验性功能,可能存在预期外或不稳定的情况,如遇到问题请及时提交反馈。" + }, + "webrtc": { + "channelName": { + "desc": "WebRTC 将使用此名创建同步频道,确保频道名称唯一", + "placeholder": "请输入同步频道名称", + "shuffle": "随机生成", + "title": "同步频道名称" + }, + "channelPassword": { + "desc": "添加密码确保频道私密性,只有密码正确时,设备才可加入频道", + "placeholder": "请输入同步频道密码", + "title": "同步频道密码" + }, + "desc": "实时、点对点的数据通信,需设备同时在线才可同步", + "enabled": { + "invalid": "请填写同步频道名称后再开启", + "title": "开启同步" + }, + "title": "WebRTC 同步" + } + }, "tab": { "about": "О нас", "agent": "Помощник по умолчанию", "common": "Общие настройки", + "experiment": "实验", "llm": "Языковая модель", + "sync": "云端同步", "tts": "Голосовые услуги" }, "tools": { diff --git a/locales/tr-TR/common.json b/locales/tr-TR/common.json index 8374526c129e..8aa7fb0ca618 100644 --- a/locales/tr-TR/common.json +++ b/locales/tr-TR/common.json @@ -10,17 +10,12 @@ }, "about": "Hakkında", "advanceSettings": "Gelişmiş Ayarlar", - "agentMaxToken": "Maksimum Oturum Süresi", - "agentModel": "Model", - "agentProfile": "Asistan Profili", "appInitializing": "LobeChat başlatılıyor, lütfen bekleyin...", - "archive": "Arşiv", "autoGenerate": "Otomatik Oluştur", "autoGenerateTooltip": "Auto-generate agent description based on prompts", "cancel": "İptal", "changelog": "Changelog", "close": "Kapat", - "confirmRemoveSessionItemAlert": "Bu asistanı silmek üzeresiniz. Silindikten sonra geri alınamaz. Lütfen eyleminizi onaylayın.", "copy": "Kopyala", "copyFail": "Kopyalama başarısız oldu", "copySuccess": "Kopyalama Başarılı", @@ -128,6 +123,39 @@ "setting": "Ayarlar", "share": "Paylaş", "stop": "Dur", + "sync": { + "actions": { + "settings": "Senkronizasyon Ayarları", + "sync": "Hemen Senkronize Et" + }, + "awareness": { + "current": "Mevcut Cihaz" + }, + "channel": "Kanal", + "disabled": { + "actions": { + "enable": "Bulut Senkronizasyonunu Etkinleştir", + "settings": "Senkronizasyon Ayarlarını Yapılandır" + }, + "desc": "Mevcut oturum verileri sadece bu tarayıcıda depolanır. Verilerinizi birden fazla cihaz arasında senkronize etmeniz gerekiyorsa bulut senkronizasyonunu yapılandırın ve etkinleştirin.", + "title": "Veri Senkronizasyonu Devre Dışı" + }, + "enabled": { + "title": "Veri Senkronizasyonu" + }, + "status": { + "connecting": "Bağlanılıyor", + "disabled": "Senkronizasyon Devre Dışı", + "ready": "Bağlandı", + "synced": "Senkronize Edildi", + "syncing": "Senkronize Ediliyor", + "unconnected": "Bağlantı Başarısız" + }, + "title": "Senkronizasyon Durumu", + "unconnected": { + "tip": "Sinyal sunucusuna bağlantı başarısız oldu, noktadan noktaya iletişim kanalı kurulamayabilir, lütfen ağı kontrol edip tekrar deneyin" + } + }, "tab": { "chat": "Chat", "market": "Keşfet", @@ -147,4 +175,4 @@ "hasNew": "Yeni güncelleme mevcut", "newVersion": "Yeni sürüm mevcut: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/tr-TR/setting.json b/locales/tr-TR/setting.json index 854ac540aebc..393953d042f0 100644 --- a/locales/tr-TR/setting.json +++ b/locales/tr-TR/setting.json @@ -440,11 +440,47 @@ "placeholder": "Asistan için benzersiz bir kimlik girin, örneğin web-geliştirme", "tooltips": "Asistan pazarına paylaşın" }, + "sync": { + "device": { + "deviceName": { + "hint": "添加名称以便于识别", + "placeholder": "请输入设备名称", + "title": "设备名称" + }, + "title": "设备信息", + "unknownBrowser": "未知浏览器", + "unknownOS": "未知系统" + }, + "warning": { + "message": "本功能目前仍为实验性功能,可能存在预期外或不稳定的情况,如遇到问题请及时提交反馈。" + }, + "webrtc": { + "channelName": { + "desc": "WebRTC 将使用此名创建同步频道,确保频道名称唯一", + "placeholder": "请输入同步频道名称", + "shuffle": "随机生成", + "title": "同步频道名称" + }, + "channelPassword": { + "desc": "添加密码确保频道私密性,只有密码正确时,设备才可加入频道", + "placeholder": "请输入同步频道密码", + "title": "同步频道密码" + }, + "desc": "实时、点对点的数据通信,需设备同时在线才可同步", + "enabled": { + "invalid": "请填写同步频道名称后再开启", + "title": "开启同步" + }, + "title": "WebRTC 同步" + } + }, "tab": { "about": "Hakkında", "agent": "Varsayılan Asistan", "common": "Genel Ayarlar", + "experiment": "实验", "llm": "Modeller", + "sync": "云端同步", "tts": "Metin Seslendirme" }, "tools": { diff --git a/locales/vi-VN/common.json b/locales/vi-VN/common.json index 8004337c9873..804d0b85d350 100644 --- a/locales/vi-VN/common.json +++ b/locales/vi-VN/common.json @@ -10,17 +10,12 @@ }, "about": "Giới thiệu", "advanceSettings": "Cài đặt nâng cao", - "agentMaxToken": "Số ký tự tối đa của phiên", - "agentModel": "Mô hình", - "agentProfile": "Hồ sơ trợ lý", "appInitializing": "LobeChat đang khởi động, vui lòng chờ...", - "archive": "Lưu trữ", "autoGenerate": "Tự động tạo", "autoGenerateTooltip": "Tự động hoàn thành mô tả trợ lý dựa trên từ gợi ý", "cancel": "Hủy", "changelog": "Nhật ký cập nhật", "close": "Đóng", - "confirmRemoveSessionItemAlert": "Bạn sắp xóa trợ lý này. Sau khi xóa, bạn sẽ không thể khôi phục. Vui lòng xác nhận hành động của bạn", "copy": "Sao chép", "copyFail": "Sao chép thất bại", "copySuccess": "Sao chép thành công", @@ -128,6 +123,39 @@ "setting": "Cài đặt", "share": "Chia sẻ", "stop": "Dừng", + "sync": { + "actions": { + "settings": "Cài đặt đồng bộ hóa", + "sync": "Đồng bộ ngay" + }, + "awareness": { + "current": "Thiết bị hiện tại" + }, + "channel": "Kênh", + "disabled": { + "actions": { + "enable": "Bật đồng bộ hóa đám mây", + "settings": "Cấu hình tham số đồng bộ hóa" + }, + "desc": "Dữ liệu phiên hiện tại chỉ lưu trữ trong trình duyệt này. Nếu bạn cần đồng bộ dữ liệu qua nhiều thiết bị, vui lòng cấu hình và bật đồng bộ hóa đám mây.", + "title": "Dữ liệu chưa được đồng bộ hóa" + }, + "enabled": { + "title": "Đồng bộ dữ liệu" + }, + "status": { + "connecting": "Đang kết nối", + "disabled": "Đồng bộ hóa chưa được bật", + "ready": "Đã kết nối", + "synced": "Đã đồng bộ", + "syncing": "Đang đồng bộ", + "unconnected": "Kết nối không thành công" + }, + "title": "Trạng thái đồng bộ hóa", + "unconnected": { + "tip": "Kết nối đến máy chủ tín hiệu thất bại, không thể thiết lập kênh truyền thông điểm-điểm, vui lòng kiểm tra lại mạng và thử lại" + } + }, "tab": { "chat": "Trò chuyện", "market": "Thị trường", @@ -147,4 +175,4 @@ "hasNew": "Có bản cập nhật mới", "newVersion": "Có phiên bản mới: {{version}}" } -} \ No newline at end of file +} diff --git a/locales/vi-VN/setting.json b/locales/vi-VN/setting.json index e43cc9b004dc..76fac0628fb6 100644 --- a/locales/vi-VN/setting.json +++ b/locales/vi-VN/setting.json @@ -440,11 +440,47 @@ "placeholder": "Vui lòng nhập nhận dạng trợ lý, cần phải duy nhất, ví dụ như phát triển web", "tooltips": "Chia sẻ lên thị trường trợ lý" }, + "sync": { + "device": { + "deviceName": { + "hint": "Thêm tên để dễ nhận biết", + "placeholder": "Nhập tên thiết bị", + "title": "Tên thiết bị" + }, + "title": "Thông tin thiết bị", + "unknownBrowser": "Trình duyệt không xác định", + "unknownOS": "Hệ điều hành không xác định" + }, + "warning": { + "message": "Chức năng này hiện vẫn đang trong thử nghiệm, có thể gặp phải tình huống ngoài dự kiến hoặc không ổn định, nếu gặp vấn đề vui lòng gửi phản hồi ngay lập tức." + }, + "webrtc": { + "channelName": { + "desc": "WebRTC sẽ sử dụng tên này để tạo kênh đồng bộ, đảm bảo tên kênh là duy nhất", + "placeholder": "Nhập tên kênh đồng bộ", + "shuffle": "Tạo ngẫu nhiên", + "title": "Tên kênh đồng bộ" + }, + "channelPassword": { + "desc": "Thêm mật khẩu để đảm bảo tính riêng tư của kênh, chỉ khi mật khẩu đúng, thiết bị mới có thể tham gia kênh", + "placeholder": "Nhập mật khẩu kênh đồng bộ", + "title": "Mật khẩu kênh đồng bộ" + }, + "desc": "Truyền thông dữ liệu thời gian thực, điểm-điểm, cần thiết bị cùng online mới có thể đồng bộ", + "enabled": { + "invalid": "Vui lòng nhập tên kênh đồng bộ trước khi bật", + "title": "Bật đồng bộ" + }, + "title": "WebRTC Đồng bộ" + } + }, "tab": { "about": "Về chúng tôi", "agent": "Trợ lý mặc định", "common": "Cài đặt chung", + "experiment": "Thử nghiệm", "llm": "Mô hình ngôn ngữ", + "sync": "Đồng bộ trên đám mây", "tts": "Dịch vụ giọng nói" }, "tools": { diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json index ccd7ef45c08b..cad4529f58ae 100644 --- a/locales/zh-CN/common.json +++ b/locales/zh-CN/common.json @@ -10,17 +10,12 @@ }, "about": "关于", "advanceSettings": "高级设置", - "agentMaxToken": "会话最大长度", - "agentModel": "模型", - "agentProfile": "助手信息", "appInitializing": "LobeChat 启动中,请耐心等待...", - "archive": "归档", "autoGenerate": "自动补全", "autoGenerateTooltip": "基于提示词自动补全助手描述", "cancel": "取消", "changelog": "更新日志", "close": "关闭", - "confirmRemoveSessionItemAlert": "即将删除该助手,删除后该将无法找回,请确认你的操作", "copy": "复制", "copyFail": "复制失败", "copySuccess": "复制成功", @@ -128,6 +123,39 @@ "setting": "设置", "share": "分享", "stop": "停止", + "sync": { + "actions": { + "settings": "同步设置", + "sync": "立即同步" + }, + "awareness": { + "current": "当前设备" + }, + "channel": "频道", + "disabled": { + "actions": { + "enable": "开启云端同步", + "settings": "配置同步参数" + }, + "desc": "当前会话数据仅存储于此浏览器中。如果你需要在多个设备间同步数据,请配置并开启云端同步。", + "title": "数据同步未开启" + }, + "enabled": { + "title": "数据同步" + }, + "status": { + "connecting": "连接中", + "disabled": "同步未开启", + "ready": "已连接", + "synced": "已同步", + "syncing": "同步中", + "unconnected": "连接失败" + }, + "title": "同步状态", + "unconnected": { + "tip": "信令服务器连接失败,将无法建立点对点通信频道,请检查网络后重试" + } + }, "tab": { "chat": "会话", "market": "发现", @@ -147,4 +175,4 @@ "hasNew": "有可用更新", "newVersion": "有新版本可用:{{version}}" } -} \ No newline at end of file +} diff --git a/locales/zh-CN/setting.json b/locales/zh-CN/setting.json index e24a040248a8..063a1cdf343f 100644 --- a/locales/zh-CN/setting.json +++ b/locales/zh-CN/setting.json @@ -440,11 +440,47 @@ "placeholder": "请输入助手的标识符,需要是唯一的,比如 web-development", "tooltips": "分享到助手市场" }, + "sync": { + "device": { + "deviceName": { + "hint": "添加名称以便于识别", + "placeholder": "请输入设备名称", + "title": "设备名称" + }, + "title": "设备信息", + "unknownBrowser": "未知浏览器", + "unknownOS": "未知系统" + }, + "warning": { + "message": "本功能目前仍为实验性功能,可能存在预期外或不稳定的情况,如遇到问题请及时提交反馈。" + }, + "webrtc": { + "channelName": { + "desc": "WebRTC 将使用此名创建同步频道,确保频道名称唯一", + "placeholder": "请输入同步频道名称", + "shuffle": "随机生成", + "title": "同步频道名称" + }, + "channelPassword": { + "desc": "添加密码确保频道私密性,只有密码正确时,设备才可加入频道", + "placeholder": "请输入同步频道密码", + "title": "同步频道密码" + }, + "desc": "实时、点对点的数据通信,需设备同时在线才可同步", + "enabled": { + "invalid": "请填写同步频道名称后再开启", + "title": "开启同步" + }, + "title": "WebRTC 同步" + } + }, "tab": { "about": "关于", "agent": "默认助手", "common": "通用设置", + "experiment": "实验", "llm": "语言模型", + "sync": "云端同步", "tts": "语音服务" }, "tools": { diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index c2f015ecdfb3..e752f8b249b4 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -10,17 +10,12 @@ }, "about": "關於", "advanceSettings": "進階設定", - "agentMaxToken": "助手最大標記", - "agentModel": "模型", - "agentProfile": "助手資訊", "appInitializing": "LobeChat 初始化中,請耐心等候...", - "archive": "歸檔", "autoGenerate": "自動生成", "autoGenerateTooltip": "基於提示詞自動生成助手描述", "cancel": "取消", "changelog": "變更日誌", "close": "關閉", - "confirmRemoveSessionItemAlert": "即將刪除此助手,刪除後將無法復原,請確認您的操作", "copy": "複製", "copyFail": "複製失敗", "copySuccess": "複製成功", @@ -128,6 +123,39 @@ "setting": "設定", "share": "分享", "stop": "停止", + "sync": { + "actions": { + "settings": "同步設定", + "sync": "立即同步" + }, + "awareness": { + "current": "當前裝置" + }, + "channel": "頻道", + "disabled": { + "actions": { + "enable": "開啟雲端同步", + "settings": "配置同步參數" + }, + "desc": "當前會話數據僅存儲於此瀏覽器中。如果你需要在多個裝置間同步數據,請配置並開啟雲端同步。", + "title": "數據同步未開啟" + }, + "enabled": { + "title": "數據同步" + }, + "status": { + "connecting": "連接中", + "disabled": "同步未開啟", + "ready": "已連接", + "synced": "已同步", + "syncing": "同步中", + "unconnected": "連接失敗" + }, + "title": "同步狀態", + "unconnected": { + "tip": "信令伺服器連接失敗,將無法建立點對點通訊頻道,請檢查網路後重試" + } + }, "tab": { "chat": "對話", "market": "發現", @@ -147,4 +175,4 @@ "hasNew": "有可用更新", "newVersion": "有新版本可用:{{version}}" } -} \ No newline at end of file +} diff --git a/locales/zh-TW/setting.json b/locales/zh-TW/setting.json index da5efd0f3f99..df0102e241d8 100644 --- a/locales/zh-TW/setting.json +++ b/locales/zh-TW/setting.json @@ -440,11 +440,47 @@ "placeholder": "請輸入助手的標識符,需要是唯一的,比如 web-development", "tooltips": "分享到助手市場" }, + "sync": { + "device": { + "deviceName": { + "hint": "添加名稱以便於識別", + "placeholder": "請輸入裝置名稱", + "title": "裝置名稱" + }, + "title": "裝置資訊", + "unknownBrowser": "未知瀏覽器", + "unknownOS": "未知系統" + }, + "warning": { + "message": "本功能目前仍為實驗性功能,可能存在預期外或不穩定的情況,如遇到問題請及時提交反饋。" + }, + "webrtc": { + "channelName": { + "desc": "WebRTC 將使用此名創建同步頻道,確保頻道名稱唯一", + "placeholder": "請輸入同步頻道名稱", + "shuffle": "隨機生成", + "title": "同步頻道名稱" + }, + "channelPassword": { + "desc": "添加密碼確保頻道私密性,只有密碼正確時,裝置才可加入頻道", + "placeholder": "請輸入同步頻道密碼", + "title": "同步頻道密碼" + }, + "desc": "實時、點對點的數據通信,需裝置同時在線才可同步", + "enabled": { + "invalid": "請填寫同步頻道名稱後再開啟", + "title": "開啟同步" + }, + "title": "WebRTC 同步" + } + }, "tab": { "about": "關於", "agent": "默認助手", "common": "通用設置", + "experiment": "實驗", "llm": "語言模型", + "sync": "雲端同步", "tts": "語音服務" }, "tools": { From 1d605c6d2538260b472150379536ddc6bf471fef Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Thu, 21 Mar 2024 06:39:51 +0000 Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=92=84=20style:=20improve=20random?= =?UTF-8?q?=20name=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/sync/WebRTC/ChannelNameInput.tsx | 46 +++++++++++++++++++ .../sync/{WebRTC.tsx => WebRTC/index.tsx} | 30 ++---------- .../{ => components}/SyncSwitch/index.css | 0 .../{ => components}/SyncSwitch/index.tsx | 0 4 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 src/app/settings/sync/WebRTC/ChannelNameInput.tsx rename src/app/settings/sync/{WebRTC.tsx => WebRTC/index.tsx} (71%) rename src/app/settings/sync/{ => components}/SyncSwitch/index.css (100%) rename src/app/settings/sync/{ => components}/SyncSwitch/index.tsx (100%) diff --git a/src/app/settings/sync/WebRTC/ChannelNameInput.tsx b/src/app/settings/sync/WebRTC/ChannelNameInput.tsx new file mode 100644 index 000000000000..b47c4c011241 --- /dev/null +++ b/src/app/settings/sync/WebRTC/ChannelNameInput.tsx @@ -0,0 +1,46 @@ +import { ActionIcon } from '@lobehub/ui'; +import { Input, InputProps } from 'antd'; +import { FormInstance } from 'antd/es/form/hooks/useForm'; +import { LucideDices } from 'lucide-react'; +import { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { generateRandomRoomName } from '@/app/settings/sync/util'; + +interface ChannelNameInputProps extends Omit { + form: FormInstance; +} + +const ChannelNameInput = memo(({ form, ...res }) => { + const { t } = useTranslation('setting'); + const [loading, setLoading] = useState(false); + + return ( + { + setLoading(true); + const name = await generateRandomRoomName(); + setLoading(false); + form.setFieldValue(['sync', 'webrtc', 'channelName'], name); + form.setFieldValue(['sync', 'webrtc', 'enabled'], false); + form.submit(); + }} + size={'small'} + style={{ + marginRight: -4, + }} + title={t('sync.webrtc.channelName.shuffle')} + /> + } + {...res} + /> + ); +}); + +export default ChannelNameInput; diff --git a/src/app/settings/sync/WebRTC.tsx b/src/app/settings/sync/WebRTC/index.tsx similarity index 71% rename from src/app/settings/sync/WebRTC.tsx rename to src/app/settings/sync/WebRTC/index.tsx index 4b60231141f3..76319e33813f 100644 --- a/src/app/settings/sync/WebRTC.tsx +++ b/src/app/settings/sync/WebRTC/index.tsx @@ -1,19 +1,18 @@ 'use client'; import { SiWebrtc } from '@icons-pack/react-simple-icons'; -import { ActionIcon, Form, type ItemGroup, Tooltip } from '@lobehub/ui'; +import { Form, type ItemGroup, Tooltip } from '@lobehub/ui'; import { Form as AntForm, Input, Switch, Typography } from 'antd'; -import { LucideDices } from 'lucide-react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; -import { generateRandomRoomName } from '@/app/settings/sync/util'; import { FORM_STYLE } from '@/const/layoutTokens'; import SyncStatusInspector from '@/features/SyncStatusInspector'; import { useGlobalStore } from '@/store/global'; -import { useSyncSettings } from '../hooks/useSyncSettings'; +import { useSyncSettings } from '../../hooks/useSyncSettings'; +import ChannelNameInput from './ChannelNameInput'; type SettingItemGroup = ItemGroup; @@ -30,28 +29,7 @@ const WebRTC = memo(() => { const config: SettingItemGroup = { children: [ { - children: ( - { - const name = await generateRandomRoomName(); - form.setFieldValue(['sync', 'webrtc', 'channelName'], name); - form.setFieldValue(['sync', 'webrtc', 'enabled'], false); - form.submit(); - }} - size={'small'} - style={{ - marginRight: -4, - }} - title={t('sync.webrtc.channelName.shuffle')} - /> - } - /> - ), + children: , desc: t('sync.webrtc.channelName.desc'), label: t('sync.webrtc.channelName.title'), name: ['sync', 'webrtc', 'channelName'], diff --git a/src/app/settings/sync/SyncSwitch/index.css b/src/app/settings/sync/components/SyncSwitch/index.css similarity index 100% rename from src/app/settings/sync/SyncSwitch/index.css rename to src/app/settings/sync/components/SyncSwitch/index.css diff --git a/src/app/settings/sync/SyncSwitch/index.tsx b/src/app/settings/sync/components/SyncSwitch/index.tsx similarity index 100% rename from src/app/settings/sync/SyncSwitch/index.tsx rename to src/app/settings/sync/components/SyncSwitch/index.tsx From 0542a403833dfc17c73af908eaf941c4f81fd95a Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 21 Mar 2024 19:25:55 +0800 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=9A=B8=20style:=20add=20default=20D?= =?UTF-8?q?evice=20Name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/global/slices/common/action.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/store/global/slices/common/action.ts b/src/store/global/slices/common/action.ts index 50fc10be6cd2..43edf7dfb145 100644 --- a/src/store/global/slices/common/action.ts +++ b/src/store/global/slices/common/action.ts @@ -11,7 +11,7 @@ import { messageService } from '@/services/message'; import { UserConfig, userService } from '@/services/user'; import type { GlobalStore } from '@/store/global'; import type { GlobalServerConfig, GlobalSettings } from '@/types/settings'; -import { OnSyncEvent } from '@/types/sync'; +import { OnSyncEvent, PeerSyncStatus } from '@/types/sync'; import { merge } from '@/utils/merge'; import { browserInfo } from '@/utils/platform'; import { setNamespace } from '@/utils/storeDebug'; @@ -74,6 +74,9 @@ export const createCommonSlice: StateCreator< const name = syncSettingsSelectors.deviceName(get()); + const defaultUserName = `My ${browserInfo.browser} (${browserInfo.os})`; + + set({ syncStatus: PeerSyncStatus.Connecting }); return globalService.enabledSync({ channel: { name: sync.channelName, @@ -87,7 +90,12 @@ export const createCommonSlice: StateCreator< set({ syncStatus: status }); }, signaling: sync.signaling, - user: { id: userId, name: name, ...browserInfo }, + user: { + id: userId, + // if user don't set the name, use default name + name: name || defaultUserName, + ...browserInfo, + }, }); }, updateAvatar: async (avatar) => { From 60252b98a179fec85441018aa693b8a418883187 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Thu, 21 Mar 2024 19:32:11 +0800 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=9A=B8=20style:=20add=20mobile=20ex?= =?UTF-8?q?periment=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/settings/(mobile)/features/Header/index.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/settings/(mobile)/features/Header/index.tsx b/src/app/settings/(mobile)/features/Header/index.tsx index fff67916fe86..2e2e4870bb96 100644 --- a/src/app/settings/(mobile)/features/Header/index.tsx +++ b/src/app/settings/(mobile)/features/Header/index.tsx @@ -1,7 +1,9 @@ import { MobileNavBar, MobileNavBarTitle } from '@lobehub/ui'; +import { Tag } from 'antd'; import { useRouter } from 'next/navigation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; import { SettingsTabs } from '@/store/global/initialState'; @@ -16,7 +18,16 @@ const Header = memo(({ activeTab }) => { return ( } + center={ + + {t(`tab.${activeTab}`)} + {activeTab === SettingsTabs.Sync && {t('tab.experiment')}} + + } + /> + } onBackClick={() => router.push('/settings')} showBackButton /> From bd76f4e76e9c24d18b204f6ef8a468bb274e645f Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Fri, 22 Mar 2024 05:48:11 +0000 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=9A=A8=20ci:=20fix=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/database/models/session.ts | 2 +- src/database/models/sessionGroup.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a9eddc117cc7..78122e4a9058 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "i18n": "npm run workflow:i18n && lobe-i18n", "i18n:docs": "lobe-i18n md && npm run workflow:docs", "lint": "npm run lint:ts && npm run lint:style && npm run type-check && npm run lint:circular", - "lint:circular": "dpdm src/**/*.ts --warning false --tree false --exit-code circular:1 -T true", + "lint:circular": "dpdm src/**/*.ts --warning false --tree false --exit-code circular:1 -T true --skip-dynamic-imports circular", "lint:md": "remark . --quiet --frail --output", "lint:mdx": "eslint \"{contributing,docs}/**/*.mdx\" --fix", "lint:style": "stylelint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix", diff --git a/src/database/models/session.ts b/src/database/models/session.ts index b1e681e6df45..c045a09e27a2 100644 --- a/src/database/models/session.ts +++ b/src/database/models/session.ts @@ -3,7 +3,6 @@ import { DeepPartial } from 'utility-types'; import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session'; import { BaseModel } from '@/database/core'; import { DBModel } from '@/database/core/types/db'; -import { SessionGroupModel } from '@/database/models/sessionGroup'; import { DB_Session, DB_SessionSchema } from '@/database/schemas/session'; import { LobeAgentConfig } from '@/types/agent'; import { @@ -17,6 +16,7 @@ import { merge } from '@/utils/merge'; import { uuid } from '@/utils/uuid'; import { MessageModel } from './message'; +import { SessionGroupModel } from './sessionGroup'; import { TopicModel } from './topic'; class _SessionModel extends BaseModel { diff --git a/src/database/models/sessionGroup.ts b/src/database/models/sessionGroup.ts index 6339320f379e..69fcf2d5778f 100644 --- a/src/database/models/sessionGroup.ts +++ b/src/database/models/sessionGroup.ts @@ -3,8 +3,6 @@ import { DB_SessionGroup, DB_SessionGroupSchema } from '@/database/schemas/sessi import { SessionGroups } from '@/types/session'; import { nanoid } from '@/utils/uuid'; -import { SessionModel } from './session'; - class _SessionGroupModel extends BaseModel { constructor() { super('sessionGroups', DB_SessionGroupSchema); @@ -53,6 +51,7 @@ class _SessionGroupModel extends BaseModel { // **************** Delete *************** // async delete(id: string, removeGroupItem: boolean = false) { + const { SessionModel } = await import('./session'); this.db.sessions.toCollection().modify(async (session) => { // update all session associated with the sessionGroup to default if (session.group === id) {