From 6c68115482298933f0e231c3dde49f2a848d8b83 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Tue, 17 Dec 2024 04:03:21 +0000 Subject: [PATCH 01/16] =?UTF-8?q?=E2=9C=A8=20feat:=20pglite=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- scripts/migrateClientDB/compile-migrations.ts | 14 + .../features/ChatList/Client.tsx | 25 + .../@conversation/features/ChatList/index.tsx | 4 +- src/app/(main)/chat/_layout/Desktop/index.tsx | 2 + .../chat/features/EnableClientDB/Failed.tsx | 82 ++ .../chat/features/EnableClientDB/Idle.tsx | 111 ++ .../chat/features/EnableClientDB/Modal.tsx | 63 + .../chat/features/EnableClientDB/const.ts | 15 + .../chat/features/EnableClientDB/index.tsx | 18 + src/app/loading/Content.tsx | 10 +- src/app/loading/InitClientDB.tsx | 25 + src/components/DataStyleModal/index.tsx | 15 +- src/database/client/db.test.ts | 169 +++ src/database/client/db.ts | 216 ++++ src/database/client/dbForTest.ts | 35 + src/database/client/migrations.json | 289 +++++ src/features/InitClientDB/InitProgress.tsx | 132 ++ src/features/InitClientDB/index.tsx | 24 + src/locales/default/common.ts | 2 +- src/server/routers/lambda/file.ts | 4 +- src/services/baseClientService/index.ts | 19 + src/services/debug.ts | 66 +- src/services/file/ClientS3/index.test.ts | 115 ++ src/services/file/ClientS3/index.ts | 58 + src/services/file/client.test.ts | 226 ++-- src/services/file/client.ts | 58 +- src/services/file/type.ts | 7 +- .../import/__tests__/fixtures/messages.json | 1101 +++++++++++++++++ src/services/import/client.test.ts | 998 +++++++++++++++ src/services/import/client.ts | 82 +- src/services/message/client.test.ts | 350 +++--- src/services/message/client.ts | 74 +- src/services/message/index.test.ts | 48 - src/services/message/server.ts | 8 +- src/services/message/type.ts | 7 +- src/services/plugin/client.test.ts | 123 +- src/services/plugin/client.ts | 31 +- src/services/session/client.test.ts | 449 ++++--- src/services/session/client.ts | 103 +- src/services/session/type.ts | 15 +- src/services/topic/client.test.ts | 187 ++- src/services/topic/client.ts | 54 +- src/services/topic/index.ts | 1 - src/services/upload.ts | 24 +- src/services/user/client.test.ts | 60 +- src/services/user/client.ts | 66 +- .../chat/slices/builtinTool/action.test.ts | 16 +- src/store/file/slices/upload/action.ts | 100 +- src/store/global/actions/clientDb.ts | 45 + src/store/global/initialState.ts | 12 + src/store/global/selectors.ts | 24 +- src/store/global/store.ts | 4 +- .../slices/sessionGroup/reducer.test.ts | 12 +- src/types/clientDB.ts | 17 + src/types/files/upload.ts | 10 +- src/types/global.d.ts | 9 + src/types/meta.ts | 9 - src/types/session/sessionGroup.ts | 6 +- 59 files changed, 4755 insertions(+), 1098 deletions(-) create mode 100644 scripts/migrateClientDB/compile-migrations.ts create mode 100644 src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx create mode 100644 src/app/(main)/chat/features/EnableClientDB/Failed.tsx create mode 100644 src/app/(main)/chat/features/EnableClientDB/Idle.tsx create mode 100644 src/app/(main)/chat/features/EnableClientDB/Modal.tsx create mode 100644 src/app/(main)/chat/features/EnableClientDB/const.ts create mode 100644 src/app/(main)/chat/features/EnableClientDB/index.tsx create mode 100644 src/app/loading/InitClientDB.tsx create mode 100644 src/database/client/db.test.ts create mode 100644 src/database/client/db.ts create mode 100644 src/database/client/dbForTest.ts create mode 100644 src/database/client/migrations.json create mode 100644 src/features/InitClientDB/InitProgress.tsx create mode 100644 src/features/InitClientDB/index.tsx create mode 100644 src/services/baseClientService/index.ts create mode 100644 src/services/file/ClientS3/index.test.ts create mode 100644 src/services/file/ClientS3/index.ts create mode 100644 src/services/import/__tests__/fixtures/messages.json create mode 100644 src/services/import/client.test.ts delete mode 100644 src/services/message/index.test.ts create mode 100644 src/store/global/actions/clientDb.ts create mode 100644 src/types/clientDB.ts diff --git a/package.json b/package.json index d16eb9c0865c..01bcefb0be14 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts", "build:analyze": "ANALYZE=true next build", "build:docker": "DOCKER=true next build && npm run build-sitemap", - "db:generate": "drizzle-kit generate", + "db:generate": "drizzle-kit generate && npm run db:generate-client", + "db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts", "db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts", "db:push": "drizzle-kit push", "db:push-test": "NODE_ENV=test drizzle-kit push", @@ -117,6 +118,7 @@ "@clerk/themes": "^2.1.37", "@codesandbox/sandpack-react": "^2.19.9", "@cyntler/react-doc-viewer": "^1.17.0", + "@electric-sql/pglite": "^0.2.14", "@google/generative-ai": "^0.21.0", "@huggingface/inference": "^2.8.1", "@icons-pack/react-simple-icons": "9.6.0", diff --git a/scripts/migrateClientDB/compile-migrations.ts b/scripts/migrateClientDB/compile-migrations.ts new file mode 100644 index 000000000000..c33e9dff5fb1 --- /dev/null +++ b/scripts/migrateClientDB/compile-migrations.ts @@ -0,0 +1,14 @@ +import { readMigrationFiles } from 'drizzle-orm/migrator'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const dbBase = join(__dirname, '../../src/database'); +const migrationsFolder = join(dbBase, './migrations'); +const migrations = readMigrationFiles({ migrationsFolder: migrationsFolder }); + +writeFileSync( + join(dbBase, './client/migrations.json'), + JSON.stringify(migrations, null, 2), // null, 2 adds indentation for better readability +); + +console.log('🏁 client migrations.json compiled!'); diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx new file mode 100644 index 000000000000..a9c328bc6407 --- /dev/null +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx @@ -0,0 +1,25 @@ +'use client'; + +import React, { memo } from 'react'; + +import InitClientDB from '@/features/InitClientDB'; +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; + +import Content from './Content'; + +interface ListProps { + mobile?: boolean; +} + +const Client = memo(({ mobile }) => { + const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); + + if (isPgliteNotInited) return ; + + return ; +}); + +Client.displayName = 'ChatListClient'; + +export default Client; diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx index 57c561cab0b2..82571c40f349 100644 --- a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx @@ -3,7 +3,7 @@ import { Flexbox } from 'react-layout-kit'; import { SkeletonList } from '@/features/Conversation'; -const Content = lazy(() => import('./Content')); +const Client = lazy(() => import('./Client')); interface ChatListProps { mobile?: boolean; @@ -20,7 +20,7 @@ const ChatList = ({ mobile }: ChatListProps) => ( width={'100%'} > }> - + ); diff --git a/src/app/(main)/chat/_layout/Desktop/index.tsx b/src/app/(main)/chat/_layout/Desktop/index.tsx index 84296bd8f87e..72e1d54950ae 100644 --- a/src/app/(main)/chat/_layout/Desktop/index.tsx +++ b/src/app/(main)/chat/_layout/Desktop/index.tsx @@ -1,5 +1,6 @@ import { Flexbox } from 'react-layout-kit'; +import EnableClientDB from '../../features/EnableClientDB'; import Migration from '../../features/Migration'; import { LayoutProps } from '../type'; import SessionPanel from './SessionPanel'; @@ -19,6 +20,7 @@ const Layout = ({ children, session }: LayoutProps) => { + {/* ↓ cloud slot ↓ */} {/* ↑ cloud slot ↑ */} diff --git a/src/app/(main)/chat/features/EnableClientDB/Failed.tsx b/src/app/(main)/chat/features/EnableClientDB/Failed.tsx new file mode 100644 index 000000000000..eb40760adb53 --- /dev/null +++ b/src/app/(main)/chat/features/EnableClientDB/Failed.tsx @@ -0,0 +1,82 @@ +import { Alert, Highlighter, Icon } from '@lobehub/ui'; +import { Button, Result } from 'antd'; +import { ShieldAlert } from 'lucide-react'; +import Link from 'next/link'; +import { memo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; +import Balancer from 'react-wrap-balancer'; + +import { GITHUB_ISSUES } from '@/const/url'; +import { githubService } from '@/services/github'; +import { useGlobalStore } from '@/store/global'; + +import { MigrationError } from './const'; + +interface FailedProps { + error?: MigrationError; +} +const Failed = memo(({ error }) => { + const { t } = useTranslation('common'); + + const [initializeClientDB] = useGlobalStore((s) => [s.initializeClientDB]); + + return ( + + {!!error && ( + + {JSON.stringify(error)} + + } + message={error.message} + style={{ flex: 1 }} + type={'error'} + /> + )} + + + + + } + icon={} + status={'error'} + style={{ paddingBlock: 24, width: 450 }} + subTitle={ + + + 非常抱歉,数据库升级过程发生异常。请重试升级,或 + { + e.preventDefault(); + githubService.submitDBV1UpgradeError(1, error!); + }} + target="_blank" + > + 提交问题 + + 我们将会第一时间帮你排查问题。 + + + } + title={t('clientDB.error.title')} + /> + ); +}); + +export default Failed; diff --git a/src/app/(main)/chat/features/EnableClientDB/Idle.tsx b/src/app/(main)/chat/features/EnableClientDB/Idle.tsx new file mode 100644 index 000000000000..596e9e7a1769 --- /dev/null +++ b/src/app/(main)/chat/features/EnableClientDB/Idle.tsx @@ -0,0 +1,111 @@ +import { Icon } from '@lobehub/ui'; +import { Button } from 'antd'; +import { createStyles } from 'antd-style'; +import { Database, SearchCheck, Zap } from 'lucide-react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { useGlobalStore } from '@/store/global'; + +const useStyles = createStyles(({ css, token, isDarkMode, responsive }) => ({ + desc: css` + width: 280px; + color: ${token.colorTextSecondary}; + + ${responsive.mobile} { + line-height: ${token.lineHeight}; + } + `, + hint: css` + font-size: ${token.fontSizeSM}px; + color: ${token.colorTextTertiary}; + text-align: center; + `, + icon: css` + color: ${isDarkMode ? token.blue : token.geekblue}; + `, + iconCtn: css` + width: 72px; + height: 72px; + background: ${isDarkMode ? token.blue1 : token.geekblue1}; + border-radius: 50%; + `, + intro: css` + ${responsive.mobile} { + width: 350px; + margin-block-start: 24px; + line-height: ${token.lineHeight}; + } + `, + + title: css` + margin-block-end: 0; + font-size: ${token.fontSizeLG}px; + font-weight: bold; + `, +})); + +const Idle = () => { + const { t } = useTranslation('common'); + const { styles } = useStyles(); + const [initializeClientDB] = useGlobalStore((s) => [s.initializeClientDB]); + const [loading, setLoading] = useState(false); + const features = [ + { + avatar: Database, + desc: '顶级数据库Postgres', + title: 'Pglite XXXX', + }, + { + avatar: Zap, + desc: 'AI Native 知识库', + title: '支持原生知识库对话', + }, + { + avatar: SearchCheck, + desc: '大容量', + title: '高达 2G 的存储空间', + }, + ]; + + return ( +
+ + + {t('clientDB.modal.desc')} + + + + {features.map((item) => ( + +
+ +
+ +

{item.title}

+

{item.desc}

+
+
+ ))} +
+ + + + + +
+ ); +}; + +export default Idle; diff --git a/src/app/(main)/chat/features/EnableClientDB/Modal.tsx b/src/app/(main)/chat/features/EnableClientDB/Modal.tsx new file mode 100644 index 000000000000..afb2ed979cf7 --- /dev/null +++ b/src/app/(main)/chat/features/EnableClientDB/Modal.tsx @@ -0,0 +1,63 @@ +import { Icon } from '@lobehub/ui'; +import { Button, Result } from 'antd'; +import { CheckCircle, CpuIcon } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import DataStyleModal from '@/components/DataStyleModal'; +import InitProgress from '@/features/InitClientDB/InitProgress'; +import { useGlobalStore } from '@/store/global'; +import { DatabaseLoadingState } from '@/types/clientDB'; + +import Failed from './Failed'; +import Idle from './Idle'; + +interface EnableClientDBModalProps { + open: boolean; +} + +const EnableClientDBModal = memo(({ open }) => { + const { t } = useTranslation('common'); + + const [initClientDBStage, markPgliteEnabled] = useGlobalStore((s) => [ + s.initClientDBStage, + s.markPgliteEnabled, + ]); + + const renderContent = () => { + if (initClientDBStage === DatabaseLoadingState.Idle) return ; + + if (initClientDBStage === DatabaseLoadingState.Ready) + return ( + + {t('clientDB.ready.button')} + + } + icon={} + status={'success'} + style={{ paddingBlock: 24, paddingTop: 0 }} + subTitle={t('clientDB.ready.desc')} + title={t('clientDB.ready.title')} + /> + ); + + if (initClientDBStage === DatabaseLoadingState.Error) return ; + + return ; + }; + + return ( + + {renderContent()} + + ); +}); + +export default EnableClientDBModal; diff --git a/src/app/(main)/chat/features/EnableClientDB/const.ts b/src/app/(main)/chat/features/EnableClientDB/const.ts new file mode 100644 index 000000000000..2006283d4407 --- /dev/null +++ b/src/app/(main)/chat/features/EnableClientDB/const.ts @@ -0,0 +1,15 @@ +export const MIGRATE_KEY = 'migrated'; +export enum UpgradeStatus { + START, + UPGRADING, + UPGRADED, + UPGRADE_FAILED, +} + +export const V1DB_NAME = 'LobeHub'; +export const V1DB_TABLE_NAME = 'LOBE_CHAT'; + +export interface MigrationError { + message: string; + stack: string; +} diff --git a/src/app/(main)/chat/features/EnableClientDB/index.tsx b/src/app/(main)/chat/features/EnableClientDB/index.tsx new file mode 100644 index 000000000000..dd9eff20066b --- /dev/null +++ b/src/app/(main)/chat/features/EnableClientDB/index.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { Spin } from 'antd'; +import dynamic from 'next/dynamic'; +import { memo } from 'react'; + +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; + +const Modal = dynamic(() => import('./Modal'), { loading: () => , ssr: false }); + +const Migration = memo(() => { + const showModal = useGlobalStore(systemStatusSelectors.isPgliteNotEnabled); + + return showModal && ; +}); + +export default Migration; diff --git a/src/app/loading/Content.tsx b/src/app/loading/Content.tsx index 63a9775e8b1f..d679a0ce1511 100644 --- a/src/app/loading/Content.tsx +++ b/src/app/loading/Content.tsx @@ -2,6 +2,10 @@ import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; import FullscreenLoading from '@/components/FullscreenLoading'; +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; + +import InitClientDB from './InitClientDB'; import { AppLoadingStage } from './type'; @@ -10,8 +14,12 @@ interface ContentProps { } const Content = memo(({ loadingStage }) => { const { t } = useTranslation('common'); + const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); - return ; + return isPgliteNotInited ? ( + + ) : ( + ); }); export default Content; diff --git a/src/app/loading/InitClientDB.tsx b/src/app/loading/InitClientDB.tsx new file mode 100644 index 000000000000..2d83f589e63f --- /dev/null +++ b/src/app/loading/InitClientDB.tsx @@ -0,0 +1,25 @@ +import { memo } from 'react'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { ProductLogo } from '@/components/Branding'; +import InitProgress from '@/features/InitClientDB/InitProgress'; +import { useGlobalStore } from '@/store/global'; + +const InitClientDB = memo(() => { + const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); + + useInitClientDB(); + + return ( + +
+ +
+ +
+
+
+ ); +}); + +export default InitClientDB; diff --git a/src/components/DataStyleModal/index.tsx b/src/components/DataStyleModal/index.tsx index d42e8669f471..afa995ff1e95 100644 --- a/src/components/DataStyleModal/index.tsx +++ b/src/components/DataStyleModal/index.tsx @@ -26,6 +26,7 @@ const useStyles = createStyles(({ css, token, prefixCls, isDarkMode }) => ({ interface DataStyleModalProps { children: ReactNode; + hideTitle?: boolean; icon: LucideIcon; onOpenChange?: (open: boolean) => void; open: boolean; @@ -34,7 +35,7 @@ interface DataStyleModalProps { } const DataStyleModal = memo( - ({ icon, onOpenChange, title, open, children, width = 550 }) => { + ({ icon, onOpenChange, title, open, children, width = 550, hideTitle }) => { const { styles } = useStyles(); return ( @@ -48,10 +49,14 @@ const DataStyleModal = memo( footer={null} open={open} title={ - - - {title} - + !hideTitle ? ( + + + {title} + + ) : ( +
+ ) } width={width} > diff --git a/src/database/client/db.test.ts b/src/database/client/db.test.ts new file mode 100644 index 000000000000..33996f04849f --- /dev/null +++ b/src/database/client/db.test.ts @@ -0,0 +1,169 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB'; + +import { DatabaseManager } from './db'; + +// Mock 所有外部依赖 +vi.mock('@electric-sql/pglite', () => ({ + default: vi.fn(), + PGlite: vi.fn(), +})); + +vi.mock('@electric-sql/pglite/vector', () => ({ + default: vi.fn(), + vector: vi.fn(), +})); + +vi.mock('drizzle-orm/pglite', () => ({ + drizzle: vi.fn(() => ({ + dialect: { + migrate: vi.fn().mockResolvedValue(undefined), + }, + })), +})); + +let manager: DatabaseManager; +let progressEvents: ClientDBLoadingProgress[] = []; +let stateChanges: DatabaseLoadingState[] = []; + +let callbacks = { + onProgress: vi.fn((progress: ClientDBLoadingProgress) => { + progressEvents.push(progress); + }), + onStateChange: vi.fn((state: DatabaseLoadingState) => { + stateChanges.push(state); + }), +}; + +beforeEach(() => { + vi.clearAllMocks(); + progressEvents = []; + stateChanges = []; + + callbacks = { + onProgress: vi.fn((progress: ClientDBLoadingProgress) => { + progressEvents.push(progress); + }), + onStateChange: vi.fn((state: DatabaseLoadingState) => { + stateChanges.push(state); + }), + }; + // @ts-expect-error + DatabaseManager['instance'] = undefined; + manager = DatabaseManager.getInstance(); +}); + +describe('DatabaseManager', () => { + describe('Callback Handling', () => { + it('should properly track loading states', async () => { + await manager.initialize(callbacks); + + // 验证状态转换顺序 + expect(stateChanges).toEqual([ + DatabaseLoadingState.Initializing, + DatabaseLoadingState.LoadingDependencies, + DatabaseLoadingState.LoadingWasm, + DatabaseLoadingState.Migrating, + DatabaseLoadingState.Ready, + ]); + }); + + it('should report dependencies loading progress', async () => { + await manager.initialize(callbacks); + + // 验证依赖加载进度回调 + const dependencyProgress = progressEvents.filter((e) => e.phase === 'dependencies'); + expect(dependencyProgress.length).toBeGreaterThan(0); + expect(dependencyProgress[dependencyProgress.length - 1]).toEqual( + expect.objectContaining({ + phase: 'dependencies', + progress: 100, + costTime: expect.any(Number), + }), + ); + }); + + it('should report WASM loading progress', async () => { + await manager.initialize(callbacks); + + // 验证 WASM 加载进度回调 + const wasmProgress = progressEvents.filter((e) => e.phase === 'wasm'); + // expect(wasmProgress.length).toBeGreaterThan(0); + expect(wasmProgress[wasmProgress.length - 1]).toEqual( + expect.objectContaining({ + phase: 'wasm', + progress: 100, + costTime: expect.any(Number), + }), + ); + }); + + it('should handle initialization errors', async () => { + // 模拟加载失败 + vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error')); + + await expect(manager.initialize(callbacks)).rejects.toThrow(); + expect(stateChanges).toContain(DatabaseLoadingState.Error); + }); + + it('should only initialize once when called multiple times', async () => { + const firstInit = manager.initialize(callbacks); + const secondInit = manager.initialize(callbacks); + + await Promise.all([firstInit, secondInit]); + + // 验证回调只被调用一次 + const readyStateCount = stateChanges.filter( + (state) => state === DatabaseLoadingState.Ready, + ).length; + expect(readyStateCount).toBe(1); + }); + }); + + describe('Progress Calculation', () => { + it('should report progress between 0 and 100', async () => { + await manager.initialize(callbacks); + + // 验证所有进度值都在有效范围内 + progressEvents.forEach((event) => { + expect(event.progress).toBeGreaterThanOrEqual(0); + expect(event.progress).toBeLessThanOrEqual(100); + }); + }); + + it('should include timing information', async () => { + await manager.initialize(callbacks); + + // 验证最终进度回调包含耗时信息 + const finalProgress = progressEvents[progressEvents.length - 1]; + expect(finalProgress.costTime).toBeGreaterThan(0); + }); + }); + + describe('Error Handling', () => { + it('should handle missing callbacks gracefully', async () => { + // 测试没有提供回调的情况 + await expect(manager.initialize()).resolves.toBeDefined(); + }); + + it('should handle partial callbacks', async () => { + // 只提供部分回调 + await expect(manager.initialize({ onProgress: callbacks.onProgress })).resolves.toBeDefined(); + await expect( + manager.initialize({ onStateChange: callbacks.onStateChange }), + ).resolves.toBeDefined(); + }); + }); + + describe('Database Access', () => { + it('should throw error when accessing database before initialization', () => { + expect(() => manager.db).toThrow('Database not initialized'); + }); + + it('should provide access to database after initialization', async () => { + await manager.initialize(); + expect(() => manager.db).not.toThrow(); + }); + }); +}); diff --git a/src/database/client/db.ts b/src/database/client/db.ts new file mode 100644 index 000000000000..d920c93b98c9 --- /dev/null +++ b/src/database/client/db.ts @@ -0,0 +1,216 @@ +import type { PgliteDatabase } from 'drizzle-orm/pglite'; + +import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB'; + +import * as schema from '../schemas'; +import migrations from './migrations.json'; + +type DrizzleInstance = PgliteDatabase; + +export interface DatabaseLoadingCallbacks { + onProgress?: (progress: ClientDBLoadingProgress) => void; + onStateChange?: (state: DatabaseLoadingState) => void; +} + +export class DatabaseManager { + private static instance: DatabaseManager; + private dbInstance: DrizzleInstance | null = null; + private initPromise: Promise | null = null; + private callbacks?: DatabaseLoadingCallbacks; + private isLocalDBSchemaSynced = false; + + // CDN 配置 + private static WASM_CDN_URL = + 'https://registry.npmmirror.com/@electric-sql/pglite/0.2.15/files/dist/postgres.wasm'; + + private constructor() {} + + static getInstance() { + if (!DatabaseManager.instance) { + DatabaseManager.instance = new DatabaseManager(); + } + return DatabaseManager.instance; + } + + // 加载并编译 WASM 模块 + private async loadWasmModule(): Promise { + const start = Date.now(); + this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingWasm); + + const response = await fetch(DatabaseManager.WASM_CDN_URL); + + const contentLength = Number(response.headers.get('Content-Length')) || 0; + const reader = response.body?.getReader(); + + if (!reader) throw new Error('Failed to start WASM download'); + + let receivedLength = 0; + const chunks: Uint8Array[] = []; + + // 读取数据流 + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read(); + + if (done) break; + + chunks.push(value); + receivedLength += value.length; + + // 计算并报告进度 + const progress = Math.min(Math.round((receivedLength / contentLength) * 100), 100); + this.callbacks?.onProgress?.({ + phase: 'wasm', + progress, + }); + } + + // 合并数据块 + const wasmBytes = new Uint8Array(receivedLength); + let position = 0; + for (const chunk of chunks) { + wasmBytes.set(chunk, position); + position += chunk.length; + } + + this.callbacks?.onProgress?.({ + costTime: Date.now() - start, + phase: 'wasm', + progress: 100, + }); + + // 编译 WASM 模块 + return WebAssembly.compile(wasmBytes); + } + + // 异步加载 PGlite 相关依赖 + private async loadDependencies() { + const start = Date.now(); + this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingDependencies); + + const imports = [ + import('@electric-sql/pglite').then((m) => ({ default: m.PGlite })), + import('@electric-sql/pglite/vector'), + import('drizzle-orm/pglite'), + import('@electric-sql/pglite'), + ]; + + let loaded = 0; + const results = await Promise.all( + imports.map(async (importPromise) => { + const result = await importPromise; + loaded += 1; + + // 计算加载进度 + this.callbacks?.onProgress?.({ + phase: 'dependencies', + progress: Math.min(Math.round((loaded / imports.length) * 100), 100), + }); + return result; + }), + ); + + this.callbacks?.onProgress?.({ + costTime: Date.now() - start, + phase: 'dependencies', + progress: 100, + }); + + // @ts-ignore + const [{ default: PGlite }, { vector }, { drizzle }, { IdbFs, MemoryFS }] = results; + + return { IdbFs, MemoryFS, PGlite, drizzle, vector }; + } + + // 数据库迁移方法 + private async migrate(skipMultiRun = false): Promise { + if (this.isLocalDBSchemaSynced && skipMultiRun) return this.db; + + const start = Date.now(); + try { + this.callbacks?.onStateChange?.(DatabaseLoadingState.Migrating); + + // refs: https://github.com/drizzle-team/drizzle-orm/discussions/2532 + // @ts-expect-error + await this.db.dialect.migrate(migrations, this.db.session, {}); + this.isLocalDBSchemaSynced = true; + + console.info(`✅ Local database ready in ${Date.now() - start}ms`); + } catch (cause) { + console.error('❌ Local database schema migration failed', cause); + throw cause; + } + + return this.db; + } + + // 初始化数据库 + async initialize(callbacks?: DatabaseLoadingCallbacks): Promise { + if (this.initPromise) return this.initPromise; + + this.callbacks = callbacks; + + this.initPromise = (async () => { + try { + if (this.dbInstance) return this.dbInstance; + + // 初始化数据库 + this.callbacks?.onStateChange?.(DatabaseLoadingState.Initializing); + + // 加载依赖 + const { PGlite, vector, drizzle, IdbFs, MemoryFS } = await this.loadDependencies(); + + // 加载并编译 WASM 模块 + const wasmModule = await this.loadWasmModule(); + + const db = new PGlite({ + extensions: { vector }, + fs: typeof window === 'undefined' ? new MemoryFS('lobechat') : new IdbFs('lobechat'), + relaxedDurability: true, + wasmModule, + }); + + this.dbInstance = drizzle({ client: db, schema }); + + await this.migrate(true); + + this.callbacks?.onStateChange?.(DatabaseLoadingState.Ready); + + return this.dbInstance as DrizzleInstance; + } catch (error) { + this.initPromise = null; + this.callbacks?.onStateChange?.(DatabaseLoadingState.Error); + throw error; + } + })(); + + return this.initPromise; + } + + // 获取数据库实例 + get db(): DrizzleInstance { + if (!this.dbInstance) { + throw new Error('Database not initialized. Please call initialize() first.'); + } + return this.dbInstance; + } + + // 创建代理对象 + createProxy(): DrizzleInstance { + return new Proxy({} as DrizzleInstance, { + get: (target, prop) => { + return this.db[prop as keyof DrizzleInstance]; + }, + }); + } +} + +// 导出单例 +const dbManager = DatabaseManager.getInstance(); + +// 保持原有的 clientDB 导出不变 +export const clientDB = dbManager.createProxy(); + +// 导出初始化方法,供应用启动时使用 +export const initializeDB = (callbacks?: DatabaseLoadingCallbacks) => + dbManager.initialize(callbacks); diff --git a/src/database/client/dbForTest.ts b/src/database/client/dbForTest.ts new file mode 100644 index 000000000000..93271b53b7cc --- /dev/null +++ b/src/database/client/dbForTest.ts @@ -0,0 +1,35 @@ +import { IdbFs, PGlite } from '@electric-sql/pglite'; +import { vector } from '@electric-sql/pglite/vector'; +import { drizzle } from 'drizzle-orm/pglite'; + +import * as schema from '../schemas'; +import migrations from './migrations.json'; + +const client = new PGlite({ + extensions: { vector }, + fs: new IdbFs('lobechat'), + relaxedDurability: true, +}); + +export const clientDB = drizzle({ client, schema }); + +//prevent multiple schema migrations to be run +let isLocalDBSchemaSynced = false; + +export const migrate = async (skipMultiRun = false) => { + if (isLocalDBSchemaSynced && skipMultiRun) return; + + const start = Date.now(); + try { + // @ts-expect-error + await clientDB.dialect.migrate(migrations, clientDB.session, {}); + isLocalDBSchemaSynced = true; + + console.info(`✅ Local database ready in ${Date.now() - start}ms`); + } catch (cause) { + console.error('❌ Local database schema migration failed', cause); + throw cause; + } + + return clientDB; +}; diff --git a/src/database/client/migrations.json b/src/database/client/migrations.json new file mode 100644 index 000000000000..f6600bba1d51 --- /dev/null +++ b/src/database/client/migrations.json @@ -0,0 +1,289 @@ +[ + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"agents\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"slug\" varchar(100),\n\t\"title\" text,\n\t\"description\" text,\n\t\"tags\" jsonb DEFAULT '[]'::jsonb,\n\t\"avatar\" text,\n\t\"background_color\" text,\n\t\"plugins\" jsonb DEFAULT '[]'::jsonb,\n\t\"user_id\" text NOT NULL,\n\t\"chat_config\" jsonb,\n\t\"few_shots\" jsonb,\n\t\"model\" text,\n\t\"params\" jsonb DEFAULT '{}'::jsonb,\n\t\"provider\" text,\n\t\"system_role\" text,\n\t\"tts\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_slug_unique\" UNIQUE(\"slug\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_tags\" (\n\t\"agent_id\" text NOT NULL,\n\t\"tag_id\" integer NOT NULL,\n\tCONSTRAINT \"agents_tags_agent_id_tag_id_pk\" PRIMARY KEY(\"agent_id\",\"tag_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_to_sessions\" (\n\t\"agent_id\" text NOT NULL,\n\t\"session_id\" text NOT NULL,\n\tCONSTRAINT \"agents_to_sessions_agent_id_session_id_pk\" PRIMARY KEY(\"agent_id\",\"session_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"file_type\" varchar(255) NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"size\" integer NOT NULL,\n\t\"url\" text NOT NULL,\n\t\"metadata\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_agents\" (\n\t\"file_id\" text NOT NULL,\n\t\"agent_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_agents_file_id_agent_id_pk\" PRIMARY KEY(\"file_id\",\"agent_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_messages\" (\n\t\"file_id\" text NOT NULL,\n\t\"message_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_messages_file_id_message_id_pk\" PRIMARY KEY(\"file_id\",\"message_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_sessions\" (\n\t\"file_id\" text NOT NULL,\n\t\"session_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_sessions_file_id_session_id_pk\" PRIMARY KEY(\"file_id\",\"session_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_installed_plugins\" (\n\t\"user_id\" text NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"type\" text NOT NULL,\n\t\"manifest\" jsonb,\n\t\"settings\" jsonb,\n\t\"custom_params\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"user_installed_plugins_user_id_identifier_pk\" PRIMARY KEY(\"user_id\",\"identifier\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"market\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"agent_id\" text,\n\t\"plugin_id\" integer,\n\t\"type\" text NOT NULL,\n\t\"view\" integer DEFAULT 0,\n\t\"like\" integer DEFAULT 0,\n\t\"used\" integer DEFAULT 0,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_plugins\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"tool_call_id\" text,\n\t\"type\" text DEFAULT 'default',\n\t\"api_name\" text,\n\t\"arguments\" text,\n\t\"identifier\" text,\n\t\"state\" jsonb,\n\t\"error\" jsonb\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_tts\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"content_md5\" text,\n\t\"file_id\" text,\n\t\"voice\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_translates\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"content\" text,\n\t\"from\" text,\n\t\"to\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"messages\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"role\" text NOT NULL,\n\t\"content\" text,\n\t\"model\" text,\n\t\"provider\" text,\n\t\"favorite\" boolean DEFAULT false,\n\t\"error\" jsonb,\n\t\"tools\" jsonb,\n\t\"trace_id\" text,\n\t\"observation_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"session_id\" text,\n\t\"topic_id\" text,\n\t\"parent_id\" text,\n\t\"quota_id\" text,\n\t\"agent_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"plugins\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"title\" text NOT NULL,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"author\" text,\n\t\"manifest\" text NOT NULL,\n\t\"locale\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"plugins_identifier_unique\" UNIQUE(\"identifier\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"plugins_tags\" (\n\t\"plugin_id\" integer NOT NULL,\n\t\"tag_id\" integer NOT NULL,\n\tCONSTRAINT \"plugins_tags_plugin_id_tag_id_pk\" PRIMARY KEY(\"plugin_id\",\"tag_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"session_groups\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"sort\" integer,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"sessions\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"slug\" varchar(100) NOT NULL,\n\t\"title\" text,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"background_color\" text,\n\t\"type\" text DEFAULT 'agent',\n\t\"user_id\" text NOT NULL,\n\t\"group_id\" text,\n\t\"pinned\" boolean DEFAULT false,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"tags\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"slug\" text NOT NULL,\n\t\"name\" text,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"tags_slug_unique\" UNIQUE(\"slug\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"topics\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"session_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"favorite\" boolean DEFAULT false,\n\t\"title\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_settings\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"tts\" jsonb,\n\t\"key_vaults\" text,\n\t\"general\" jsonb,\n\t\"language_model\" jsonb,\n\t\"system_agent\" jsonb,\n\t\"default_agent\" jsonb,\n\t\"tool\" jsonb\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"users\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"username\" text,\n\t\"email\" text,\n\t\"avatar\" text,\n\t\"phone\" text,\n\t\"first_name\" text,\n\t\"last_name\" text,\n\t\"is_onboarded\" boolean DEFAULT false,\n\t\"clerk_created_at\" timestamp with time zone,\n\t\"preference\" jsonb DEFAULT '{\"guide\":{\"moveSettingsToAvatar\":true,\"topic\":true},\"telemetry\":null,\"useCmdEnterToSend\":false}'::jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"key\" text,\n\tCONSTRAINT \"users_username_unique\" UNIQUE(\"username\")\n);\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents\" ADD CONSTRAINT \"agents_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_tags\" ADD CONSTRAINT \"agents_tags_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_tags\" ADD CONSTRAINT \"agents_tags_tag_id_tags_id_fk\" FOREIGN KEY (\"tag_id\") REFERENCES \"public\".\"tags\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_to_sessions\" ADD CONSTRAINT \"agents_to_sessions_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_to_sessions\" ADD CONSTRAINT \"agents_to_sessions_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_agents\" ADD CONSTRAINT \"files_to_agents_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_agents\" ADD CONSTRAINT \"files_to_agents_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_messages\" ADD CONSTRAINT \"files_to_messages_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_messages\" ADD CONSTRAINT \"files_to_messages_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_sessions\" ADD CONSTRAINT \"files_to_sessions_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_sessions\" ADD CONSTRAINT \"files_to_sessions_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_installed_plugins\" ADD CONSTRAINT \"user_installed_plugins_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_plugin_id_plugins_id_fk\" FOREIGN KEY (\"plugin_id\") REFERENCES \"public\".\"plugins\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_plugins\" ADD CONSTRAINT \"message_plugins_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_tts\" ADD CONSTRAINT \"message_tts_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_tts\" ADD CONSTRAINT \"message_tts_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_translates\" ADD CONSTRAINT \"message_translates_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_topic_id_topics_id_fk\" FOREIGN KEY (\"topic_id\") REFERENCES \"public\".\"topics\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_parent_id_messages_id_fk\" FOREIGN KEY (\"parent_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_quota_id_messages_id_fk\" FOREIGN KEY (\"quota_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"plugins_tags\" ADD CONSTRAINT \"plugins_tags_plugin_id_plugins_id_fk\" FOREIGN KEY (\"plugin_id\") REFERENCES \"public\".\"plugins\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"plugins_tags\" ADD CONSTRAINT \"plugins_tags_tag_id_tags_id_fk\" FOREIGN KEY (\"tag_id\") REFERENCES \"public\".\"tags\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_groups_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_group_id_session_groups_id_fk\" FOREIGN KEY (\"group_id\") REFERENCES \"public\".\"session_groups\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"tags\" ADD CONSTRAINT \"tags_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"topics\" ADD CONSTRAINT \"topics_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"topics\" ADD CONSTRAINT \"topics_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_settings\" ADD CONSTRAINT \"user_settings_id_users_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nCREATE INDEX IF NOT EXISTS \"messages_created_at_idx\" ON \"messages\" (\"created_at\");", + "\nCREATE UNIQUE INDEX IF NOT EXISTS \"slug_user_id_unique\" ON \"sessions\" (\"slug\",\"user_id\");\n" + ], + "bps": true, + "folderMillis": 1716982944425, + "hash": "1513c1da50dc083fc0bd9783fe88c60e4fa80b60db645aa87bfda54332252c65" + }, + { + "sql": [ + "ALTER TABLE \"messages\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"session_groups\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"sessions\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"topics\" ADD COLUMN \"client_id\" text;", + "\nCREATE INDEX IF NOT EXISTS \"messages_client_id_idx\" ON \"messages\" (\"client_id\");", + "\nALTER TABLE \"messages\" ADD CONSTRAINT \"messages_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_groups_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"topics\" ADD CONSTRAINT \"topics_client_id_unique\" UNIQUE(\"client_id\");\n" + ], + "bps": true, + "folderMillis": 1717153686544, + "hash": "ddb29ee7e7a675c12b44996e4be061b1736e8f785052242801f4cdfb2a94f258" + }, + { + "sql": [ + "ALTER TABLE \"messages\" DROP CONSTRAINT \"messages_client_id_unique\";", + "\nALTER TABLE \"session_groups\" DROP CONSTRAINT \"session_groups_client_id_unique\";", + "\nALTER TABLE \"sessions\" DROP CONSTRAINT \"sessions_client_id_unique\";", + "\nALTER TABLE \"topics\" DROP CONSTRAINT \"topics_client_id_unique\";", + "\nDROP INDEX IF EXISTS \"messages_client_id_idx\";", + "\nCREATE UNIQUE INDEX IF NOT EXISTS \"message_client_id_user_unique\" ON \"messages\" (\"client_id\",\"user_id\");", + "\nALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_group_client_id_user_unique\" UNIQUE(\"client_id\",\"user_id\");", + "\nALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_client_id_user_id_unique\" UNIQUE(\"client_id\",\"user_id\");", + "\nALTER TABLE \"topics\" ADD CONSTRAINT \"topic_client_id_user_id_unique\" UNIQUE(\"client_id\",\"user_id\");" + ], + "bps": true, + "folderMillis": 1717587734458, + "hash": "90b61fc3e744d8e2609418d9e25274ff07af4caf87370bb614db511d67900d73" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"user_budgets\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"free_budget_id\" text,\n\t\"free_budget_key\" text,\n\t\"subscription_budget_id\" text,\n\t\"subscription_budget_key\" text,\n\t\"package_budget_id\" text,\n\t\"package_budget_key\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_subscriptions\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"stripe_id\" text,\n\t\"currency\" text,\n\t\"pricing\" integer,\n\t\"billing_paid_at\" integer,\n\t\"billing_cycle_start\" integer,\n\t\"billing_cycle_end\" integer,\n\t\"cancel_at_period_end\" boolean,\n\t\"cancel_at\" integer,\n\t\"next_billing\" jsonb,\n\t\"plan\" text,\n\t\"recurring\" text,\n\t\"storage_limit\" integer,\n\t\"status\" integer,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nALTER TABLE \"users\" ALTER COLUMN \"preference\" DROP DEFAULT;", + "\nDO $$ BEGIN\n ALTER TABLE \"user_budgets\" ADD CONSTRAINT \"user_budgets_id_users_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_subscriptions\" ADD CONSTRAINT \"user_subscriptions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nALTER TABLE \"users\" DROP COLUMN IF EXISTS \"key\";\n" + ], + "bps": true, + "folderMillis": 1718460779230, + "hash": "535a9aba48be3d75762f29bbb195736f17abfe51f41a548debe925949dd0caf2" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"nextauth_accounts\" (\n\t\"access_token\" text,\n\t\"expires_at\" integer,\n\t\"id_token\" text,\n\t\"provider\" text NOT NULL,\n\t\"providerAccountId\" text NOT NULL,\n\t\"refresh_token\" text,\n\t\"scope\" text,\n\t\"session_state\" text,\n\t\"token_type\" text,\n\t\"type\" text NOT NULL,\n\t\"userId\" text NOT NULL,\n\tCONSTRAINT \"nextauth_accounts_provider_providerAccountId_pk\" PRIMARY KEY(\"provider\",\"providerAccountId\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_authenticators\" (\n\t\"counter\" integer NOT NULL,\n\t\"credentialBackedUp\" boolean NOT NULL,\n\t\"credentialDeviceType\" text NOT NULL,\n\t\"credentialID\" text NOT NULL,\n\t\"credentialPublicKey\" text NOT NULL,\n\t\"providerAccountId\" text NOT NULL,\n\t\"transports\" text,\n\t\"userId\" text NOT NULL,\n\tCONSTRAINT \"nextauth_authenticators_userId_credentialID_pk\" PRIMARY KEY(\"userId\",\"credentialID\"),\n\tCONSTRAINT \"nextauth_authenticators_credentialID_unique\" UNIQUE(\"credentialID\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_sessions\" (\n\t\"expires\" timestamp NOT NULL,\n\t\"sessionToken\" text PRIMARY KEY NOT NULL,\n\t\"userId\" text NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_verificationtokens\" (\n\t\"expires\" timestamp NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"token\" text NOT NULL,\n\tCONSTRAINT \"nextauth_verificationtokens_identifier_token_pk\" PRIMARY KEY(\"identifier\",\"token\")\n);\n", + "\nALTER TABLE \"users\" ADD COLUMN \"full_name\" text;", + "\nALTER TABLE \"users\" ADD COLUMN \"email_verified_at\" timestamp with time zone;", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_accounts\" ADD CONSTRAINT \"nextauth_accounts_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_authenticators\" ADD CONSTRAINT \"nextauth_authenticators_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_sessions\" ADD CONSTRAINT \"nextauth_sessions_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1721724512422, + "hash": "c63c5819d73414632ea32c543cfb997be31a2be3fad635c148c97e726c57fd16" + }, + { + "sql": [ + "-- Custom SQL migration file, put you code below! --\nCREATE EXTENSION IF NOT EXISTS vector;\n" + ], + "bps": true, + "folderMillis": 1722944166657, + "hash": "c112a4eb471fa4efe791b250057a1e33040515a0c60361c7d7a59044ec9e1667" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"agents_files\" (\n\t\"file_id\" text NOT NULL,\n\t\"agent_id\" text NOT NULL,\n\t\"enabled\" boolean DEFAULT true,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_files_file_id_agent_id_user_id_pk\" PRIMARY KEY(\"file_id\",\"agent_id\",\"user_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_knowledge_bases\" (\n\t\"agent_id\" text NOT NULL,\n\t\"knowledge_base_id\" text NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"enabled\" boolean DEFAULT true,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_knowledge_bases_agent_id_knowledge_base_id_pk\" PRIMARY KEY(\"agent_id\",\"knowledge_base_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"async_tasks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"type\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"user_id\" text NOT NULL,\n\t\"duration\" integer,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"file_chunks\" (\n\t\"file_id\" varchar,\n\t\"chunk_id\" uuid,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"file_chunks_file_id_chunk_id_pk\" PRIMARY KEY(\"file_id\",\"chunk_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"global_files\" (\n\t\"hash_id\" varchar(64) PRIMARY KEY NOT NULL,\n\t\"file_type\" varchar(255) NOT NULL,\n\t\"size\" integer NOT NULL,\n\t\"url\" text NOT NULL,\n\t\"metadata\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"knowledge_base_files\" (\n\t\"knowledge_base_id\" text NOT NULL,\n\t\"file_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"knowledge_base_files_knowledge_base_id_file_id_pk\" PRIMARY KEY(\"knowledge_base_id\",\"file_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"knowledge_bases\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"type\" text,\n\t\"user_id\" text NOT NULL,\n\t\"is_public\" boolean DEFAULT false,\n\t\"settings\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_chunks\" (\n\t\"message_id\" text,\n\t\"chunk_id\" uuid,\n\tCONSTRAINT \"message_chunks_chunk_id_message_id_pk\" PRIMARY KEY(\"chunk_id\",\"message_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_queries\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"message_id\" text NOT NULL,\n\t\"rewrite_query\" text,\n\t\"user_query\" text,\n\t\"embeddings_id\" uuid\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_query_chunks\" (\n\t\"id\" text,\n\t\"query_id\" uuid,\n\t\"chunk_id\" uuid,\n\t\"similarity\" numeric(6, 5),\n\tCONSTRAINT \"message_query_chunks_chunk_id_id_query_id_pk\" PRIMARY KEY(\"chunk_id\",\"id\",\"query_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"chunks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"text\" text,\n\t\"abstract\" text,\n\t\"metadata\" jsonb,\n\t\"index\" integer,\n\t\"type\" varchar,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"user_id\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"embeddings\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"chunk_id\" uuid,\n\t\"embeddings\" vector(1024),\n\t\"model\" text,\n\t\"user_id\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"unstructured_chunks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"text\" text,\n\t\"metadata\" jsonb,\n\t\"index\" integer,\n\t\"type\" varchar,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"parent_id\" varchar,\n\t\"composite_id\" uuid,\n\t\"user_id\" text,\n\t\"file_id\" varchar\n);\n", + "\nALTER TABLE \"files_to_messages\" RENAME TO \"messages_files\";", + "\nDROP TABLE \"files_to_agents\";", + "\nALTER TABLE \"files\" ADD COLUMN \"file_hash\" varchar(64);", + "\nALTER TABLE \"files\" ADD COLUMN \"chunk_task_id\" uuid;", + "\nALTER TABLE \"files\" ADD COLUMN \"embedding_task_id\" uuid;", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"async_tasks\" ADD CONSTRAINT \"async_tasks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"file_chunks\" ADD CONSTRAINT \"file_chunks_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"file_chunks\" ADD CONSTRAINT \"file_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_base_files\" ADD CONSTRAINT \"knowledge_base_files_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_base_files\" ADD CONSTRAINT \"knowledge_base_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_bases\" ADD CONSTRAINT \"knowledge_bases_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_chunks\" ADD CONSTRAINT \"message_chunks_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_chunks\" ADD CONSTRAINT \"message_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_queries\" ADD CONSTRAINT \"message_queries_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_queries\" ADD CONSTRAINT \"message_queries_embeddings_id_embeddings_id_fk\" FOREIGN KEY (\"embeddings_id\") REFERENCES \"public\".\"embeddings\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_query_id_message_queries_id_fk\" FOREIGN KEY (\"query_id\") REFERENCES \"public\".\"message_queries\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages_files\" ADD CONSTRAINT \"messages_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages_files\" ADD CONSTRAINT \"messages_files_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"chunks\" ADD CONSTRAINT \"chunks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_composite_id_chunks_id_fk\" FOREIGN KEY (\"composite_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_file_hash_global_files_hash_id_fk\" FOREIGN KEY (\"file_hash\") REFERENCES \"public\".\"global_files\"(\"hash_id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_chunk_task_id_async_tasks_id_fk\" FOREIGN KEY (\"chunk_task_id\") REFERENCES \"public\".\"async_tasks\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_embedding_task_id_async_tasks_id_fk\" FOREIGN KEY (\"embedding_task_id\") REFERENCES \"public\".\"async_tasks\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1724089032064, + "hash": "bc4e36664868d14888b9e9aef180b3e02c563fa3c253111787e68b8ea4cd995f" + }, + { + "sql": [ + "-- step 1: create a temporary table to store the rows we want to keep\nCREATE TEMP TABLE embeddings_temp AS\nSELECT DISTINCT ON (chunk_id) *\nFROM embeddings\nORDER BY chunk_id, random();\n", + "\n\n-- step 2: delete all rows from the original table\nDELETE FROM embeddings;\n", + "\n\n-- step 3: insert the rows we want to keep back into the original table\nINSERT INTO embeddings\nSELECT * FROM embeddings_temp;\n", + "\n\n-- step 4: drop the temporary table\nDROP TABLE embeddings_temp;\n", + "\n\n-- step 5: now it's safe to add the unique constraint\nALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_chunk_id_unique\" UNIQUE(\"chunk_id\");\n" + ], + "bps": true, + "folderMillis": 1724254147447, + "hash": "e99840848ffbb33ca4d7ead6158f02b8d12cb4ff5706d4529d7fa586afa4c2a9" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"rag_eval_dataset_records\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_dataset_records_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"dataset_id\" integer NOT NULL,\n\t\"ideal\" text,\n\t\"question\" text,\n\t\"reference_files\" text[],\n\t\"metadata\" jsonb,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_datasets\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_datasets_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 30000 CACHE 1),\n\t\"description\" text,\n\t\"name\" text NOT NULL,\n\t\"knowledge_base_id\" text,\n\t\"user_id\" text,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_evaluations\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_evaluations_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"name\" text NOT NULL,\n\t\"description\" text,\n\t\"eval_records_url\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"dataset_id\" integer NOT NULL,\n\t\"knowledge_base_id\" text,\n\t\"language_model\" text,\n\t\"embedding_model\" text,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_evaluation_records\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_evaluation_records_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"question\" text NOT NULL,\n\t\"answer\" text,\n\t\"context\" text[],\n\t\"ideal\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"language_model\" text,\n\t\"embedding_model\" text,\n\t\"question_embedding_id\" uuid,\n\t\"duration\" integer,\n\t\"dataset_record_id\" integer NOT NULL,\n\t\"evaluation_id\" integer NOT NULL,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_dataset_records\" ADD CONSTRAINT \"rag_eval_dataset_records_dataset_id_rag_eval_datasets_id_fk\" FOREIGN KEY (\"dataset_id\") REFERENCES \"public\".\"rag_eval_datasets\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_dataset_records\" ADD CONSTRAINT \"rag_eval_dataset_records_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_datasets\" ADD CONSTRAINT \"rag_eval_datasets_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_datasets\" ADD CONSTRAINT \"rag_eval_datasets_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_dataset_id_rag_eval_datasets_id_fk\" FOREIGN KEY (\"dataset_id\") REFERENCES \"public\".\"rag_eval_datasets\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_question_embedding_id_embeddings_id_fk\" FOREIGN KEY (\"question_embedding_id\") REFERENCES \"public\".\"embeddings\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_dataset_record_id_rag_eval_dataset_records_id_fk\" FOREIGN KEY (\"dataset_record_id\") REFERENCES \"public\".\"rag_eval_dataset_records\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_evaluation_id_rag_eval_evaluations_id_fk\" FOREIGN KEY (\"evaluation_id\") REFERENCES \"public\".\"rag_eval_evaluations\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1725366565650, + "hash": "9646161fa041354714f823d726af27247bcd6e60fa3be5698c0d69f337a5700b" + }, + { + "sql": [ + "DROP TABLE \"user_budgets\";", + "\nDROP TABLE \"user_subscriptions\";" + ], + "bps": true, + "folderMillis": 1729699958471, + "hash": "7dad43a2a25d1aec82124a4e53f8d82f8505c3073f23606c1dc5d2a4598eacf9" + }, + { + "sql": [ + "DROP TABLE \"agents_tags\" CASCADE;", + "\nDROP TABLE \"market\" CASCADE;", + "\nDROP TABLE \"plugins\" CASCADE;", + "\nDROP TABLE \"plugins_tags\" CASCADE;", + "\nDROP TABLE \"tags\" CASCADE;", + "\nALTER TABLE \"agents\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"agents_files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"agents_knowledge_bases\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"async_tasks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"global_files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"knowledge_bases\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"messages\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"chunks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"unstructured_chunks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_dataset_records\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_dataset_records\" ADD COLUMN \"updated_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_datasets\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluations\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluation_records\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluation_records\" ADD COLUMN \"updated_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"session_groups\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"sessions\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"topics\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"user_installed_plugins\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"users\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;" + ], + "bps": true, + "folderMillis": 1730900133049, + "hash": "a7d801b679e25ef3ffda343366992b2835c089363e9d7c09074336d40e438004" + }, + { + "sql": [ + "ALTER TABLE \"topics\" ADD COLUMN \"history_summary\" text;", + "\nALTER TABLE \"topics\" ADD COLUMN \"metadata\" jsonb;\n" + ], + "bps": true, + "folderMillis": 1731138670427, + "hash": "80c2eae0600190b354e4fd6b619687a66186b992ec687495bb55c6c163a98fa6" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"threads\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"title\" text,\n\t\"type\" text NOT NULL,\n\t\"status\" text DEFAULT 'active',\n\t\"topic_id\" text NOT NULL,\n\t\"source_message_id\" text NOT NULL,\n\t\"parent_thread_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"last_active_at\" timestamp with time zone DEFAULT now(),\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nALTER TABLE \"messages\" ADD COLUMN \"thread_id\" text;", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_topic_id_topics_id_fk\" FOREIGN KEY (\"topic_id\") REFERENCES \"public\".\"topics\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_parent_thread_id_threads_id_fk\" FOREIGN KEY (\"parent_thread_id\") REFERENCES \"public\".\"threads\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_thread_id_threads_id_fk\" FOREIGN KEY (\"thread_id\") REFERENCES \"public\".\"threads\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1731858381716, + "hash": "d8263bfefe296ed366379c7b7fc65195d12e6a1c0a9f1c96097ea28f2123fe50" + } +] \ No newline at end of file diff --git a/src/features/InitClientDB/InitProgress.tsx b/src/features/InitClientDB/InitProgress.tsx new file mode 100644 index 000000000000..0b48cd3426c9 --- /dev/null +++ b/src/features/InitClientDB/InitProgress.tsx @@ -0,0 +1,132 @@ +import { CheckCircleFilled } from '@ant-design/icons'; +import { Icon } from '@lobehub/ui'; +import { Progress, Typography } from 'antd'; +import { createStyles } from 'antd-style'; +import isEqual from 'fast-deep-equal'; +import { AlertCircle, Loader2 } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { useGlobalStore } from '@/store/global'; +import { DatabaseLoadingState } from '@/types/clientDB'; + +const useStyles = createStyles(({ css, token, isDarkMode, responsive }) => ({ + desc: css` + width: 280px; + color: ${token.colorTextSecondary}; + + ${responsive.mobile} { + line-height: ${token.lineHeight}; + } + `, + hint: css` + font-size: ${token.fontSizeSM}px; + color: ${token.colorTextTertiary}; + text-align: center; + `, + icon: css` + color: ${isDarkMode ? token.blue : token.geekblue}; + `, + iconCtn: css` + width: 72px; + height: 72px; + background: ${isDarkMode ? token.blue1 : token.geekblue1}; + border-radius: 50%; + `, + intro: css` + margin-block-end: 12px; + + ${responsive.mobile} { + width: 350px; + margin-block-start: 24px; + line-height: ${token.lineHeight}; + } + `, + + title: css` + margin-block-end: 0; + font-size: ${token.fontSizeLG}px; + font-weight: bold; + `, +})); + +interface InitingProps { + title: string | boolean; +} +const InitProgress = memo(({ title }) => { + const { t } = useTranslation('common'); + const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); + const process = useGlobalStore((s) => s.initClientDBProcess, isEqual); + const { styles, theme } = useStyles(); + + const getStateMessage = (state: DatabaseLoadingState, progress?: number) => { + switch (state) { + case DatabaseLoadingState.Error: { + return t('clientDB.initing.error'); + } + case DatabaseLoadingState.Idle: { + return t('clientDB.initing.idle'); + } + case DatabaseLoadingState.Initializing: { + return t('clientDB.initing.initializing'); + } + case DatabaseLoadingState.LoadingDependencies: { + return t('clientDB.initing.loadingDependencies', { progress }); + } + + case DatabaseLoadingState.LoadingWasm: { + return t('clientDB.initing.loadingWasmModule', { progress }); + } + + case DatabaseLoadingState.Migrating: { + return t('clientDB.initing.migrating'); + } + + case DatabaseLoadingState.Ready: { + return t('clientDB.initing.ready'); + } + } + }; + + return ( + <> + + {title} + + + + {currentStage === DatabaseLoadingState.Error ? ( + + ) : currentStage === DatabaseLoadingState.Ready ? ( + + ) : currentStage === DatabaseLoadingState.LoadingDependencies ? ( + + ) : currentStage === DatabaseLoadingState.LoadingWasm ? ( + + ) : ( + + )} + + {getStateMessage(currentStage, process?.progress)} + + + + ); +}); + +export default InitProgress; diff --git a/src/features/InitClientDB/index.tsx b/src/features/InitClientDB/index.tsx new file mode 100644 index 000000000000..64fd43d17efb --- /dev/null +++ b/src/features/InitClientDB/index.tsx @@ -0,0 +1,24 @@ +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center } from 'react-layout-kit'; + +import { useGlobalStore } from '@/store/global'; + +import InitProgress from './InitProgress'; + +const InitClientDB = memo(() => { + const { t } = useTranslation('common'); + const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); + + useInitClientDB(); + + return ( +
+
+ +
+
+ ); +}); + +export default InitClientDB; diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 3da2c7630e58..02ef5b2fb410 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -42,7 +42,7 @@ export default { initializing: '正在初始化...', loadingDependencies: `加载依赖中({{progress}}%)...`, loadingWasmModule: '加载 WASM 模块中({{progress}}%)...', - migrating: '正在迁移数据...', + migrating: '执行数据表迁移...', ready: '数据库已就绪', }, modal: { diff --git a/src/server/routers/lambda/file.ts b/src/server/routers/lambda/file.ts index a030c1ddf163..f31fc4d1aee8 100644 --- a/src/server/routers/lambda/file.ts +++ b/src/server/routers/lambda/file.ts @@ -32,9 +32,7 @@ export const fileRouter = router({ }), createFile: fileProcedure - .input( - UploadFileSchema.omit({ data: true, saveMode: true, url: true }).extend({ url: z.string() }), - ) + .input(UploadFileSchema.omit({ url: true }).extend({ url: z.string() })) .mutation(async ({ ctx, input }) => { const { isExist } = await ctx.fileModel.checkHash(input.hash!); diff --git a/src/services/baseClientService/index.ts b/src/services/baseClientService/index.ts new file mode 100644 index 000000000000..2717d974c783 --- /dev/null +++ b/src/services/baseClientService/index.ts @@ -0,0 +1,19 @@ +const getClientDBUserId = () => { + if (typeof window === 'undefined') return undefined; + + return window.__lobeClientUserId; +}; + +const FALLBACK_CLIENT_DB_USER_ID = 'DEFAULT_LOBE_CHAT_USER'; + +export class BaseClientService { + private readonly fallbackUserId: string; + + protected get userId(): string { + return getClientDBUserId() || this.fallbackUserId; + } + + constructor(userId?: string) { + this.fallbackUserId = userId || FALLBACK_CLIENT_DB_USER_ID; + } +} diff --git a/src/services/debug.ts b/src/services/debug.ts index 1584e5f8de3e..51ce6d59bcb4 100644 --- a/src/services/debug.ts +++ b/src/services/debug.ts @@ -1,39 +1,37 @@ -import { DEBUG_MODEL } from '@/database/_deprecated/models/__DEBUG'; - class DebugService { async insertLargeDataToDB() { - await DEBUG_MODEL.createRandomData({ - messageCount: 100_000, - sessionCount: 40, - startIndex: 0, - topicCount: 200, - }); - - console.log('已插入10w'); - - await DEBUG_MODEL.createRandomData({ - messageCount: 300_000, - sessionCount: 40, - startIndex: 100_001, - topicCount: 200, - }); - console.log('已插入40w'); - - await DEBUG_MODEL.createRandomData({ - messageCount: 300_000, - sessionCount: 40, - startIndex: 400_001, - topicCount: 200, - }); - console.log('已插入70w'); - - await DEBUG_MODEL.createRandomData({ - messageCount: 300_000, - sessionCount: 40, - startIndex: 700_001, - topicCount: 200, - }); - console.log('已插入100w'); + // await DEBUG_MODEL.createRandomData({ + // messageCount: 100_000, + // sessionCount: 40, + // startIndex: 0, + // topicCount: 200, + // }); + // + // console.log('已插入10w'); + // + // await DEBUG_MODEL.createRandomData({ + // messageCount: 300_000, + // sessionCount: 40, + // startIndex: 100_001, + // topicCount: 200, + // }); + // console.log('已插入40w'); + // + // await DEBUG_MODEL.createRandomData({ + // messageCount: 300_000, + // sessionCount: 40, + // startIndex: 400_001, + // topicCount: 200, + // }); + // console.log('已插入70w'); + // + // await DEBUG_MODEL.createRandomData({ + // messageCount: 300_000, + // sessionCount: 40, + // startIndex: 700_001, + // topicCount: 200, + // }); + // console.log('已插入100w'); } } diff --git a/src/services/file/ClientS3/index.test.ts b/src/services/file/ClientS3/index.test.ts new file mode 100644 index 000000000000..266c82078ac4 --- /dev/null +++ b/src/services/file/ClientS3/index.test.ts @@ -0,0 +1,115 @@ +import { createStore, del, get, set } from 'idb-keyval'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { BrowserS3Storage } from './index'; + +// Mock idb-keyval +vi.mock('idb-keyval', () => ({ + createStore: vi.fn(), + set: vi.fn(), + get: vi.fn(), + del: vi.fn(), +})); + +let storage: BrowserS3Storage; +let mockStore = {}; + +beforeEach(() => { + // Reset all mocks before each test + vi.clearAllMocks(); + mockStore = {}; + (createStore as any).mockReturnValue(mockStore); + storage = new BrowserS3Storage(); +}); + +describe('BrowserS3Storage', () => { + describe('constructor', () => { + it('should create store when in browser environment', () => { + expect(createStore).toHaveBeenCalledWith('lobechat-local-s3', 'objects'); + }); + }); + + describe('putObject', () => { + it('should successfully put a file object', async () => { + const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' }); + const mockArrayBuffer = new ArrayBuffer(8); + vi.spyOn(mockFile, 'arrayBuffer').mockResolvedValue(mockArrayBuffer); + (set as any).mockResolvedValue(undefined); + + await storage.putObject('1-test-key', mockFile); + + expect(set).toHaveBeenCalledWith( + '1-test-key', + { + data: mockArrayBuffer, + name: 'test.txt', + type: 'text/plain', + }, + mockStore, + ); + }); + + it('should throw error when put operation fails', async () => { + const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' }); + const mockError = new Error('Storage error'); + (set as any).mockRejectedValue(mockError); + + await expect(storage.putObject('test-key', mockFile)).rejects.toThrow( + 'Failed to put file test.txt: Storage error', + ); + }); + }); + + describe('getObject', () => { + it('should successfully get a file object', async () => { + const mockData = { + data: new ArrayBuffer(8), + name: 'test.txt', + type: 'text/plain', + }; + (get as any).mockResolvedValue(mockData); + + const result = await storage.getObject('test-key'); + + expect(result).toBeInstanceOf(File); + expect(result?.name).toBe('test.txt'); + expect(result?.type).toBe('text/plain'); + }); + + it('should return undefined when file not found', async () => { + (get as any).mockResolvedValue(undefined); + + const result = await storage.getObject('test-key'); + + expect(result).toBeUndefined(); + }); + + it('should throw error when get operation fails', async () => { + const mockError = new Error('Storage error'); + (get as any).mockRejectedValue(mockError); + + await expect(storage.getObject('test-key')).rejects.toThrow( + 'Failed to get object (key=test-key): Storage error', + ); + }); + }); + + describe('deleteObject', () => { + it('should successfully delete a file object', async () => { + (del as any).mockResolvedValue(undefined); + + await storage.deleteObject('test-key2'); + + expect(del).toHaveBeenCalledWith('test-key2', {}); + }); + + it('should throw error when delete operation fails', async () => { + const mockError = new Error('Storage error'); + (del as any).mockRejectedValue(mockError); + + await expect(storage.deleteObject('test-key')).rejects.toThrow( + 'Failed to delete object (key=test-key): Storage error', + ); + }); + }); +}); diff --git a/src/services/file/ClientS3/index.ts b/src/services/file/ClientS3/index.ts new file mode 100644 index 000000000000..94692f0ecb05 --- /dev/null +++ b/src/services/file/ClientS3/index.ts @@ -0,0 +1,58 @@ +import { createStore, del, get, set } from 'idb-keyval'; + +const BROWSER_S3_DB_NAME = 'lobechat-local-s3'; + +export class BrowserS3Storage { + private store; + + constructor() { + // skip server-side rendering + if (typeof window === 'undefined') return; + + this.store = createStore(BROWSER_S3_DB_NAME, 'objects'); + } + + /** + * 上传文件 + * @param key 文件 hash + * @param file File 对象 + */ + async putObject(key: string, file: File): Promise { + try { + const data = await file.arrayBuffer(); + await set(key, { data, name: file.name, type: file.type }, this.store); + } catch (e) { + throw new Error(`Failed to put file ${file.name}: ${(e as Error).message}`); + } + } + + /** + * 获取文件 + * @param key 文件 hash + * @returns File 对象 + */ + async getObject(key: string): Promise { + try { + const res = await get<{ data: ArrayBuffer; name: string; type: string }>(key, this.store); + if (!res) return; + + return new File([res.data], res!.name, { type: res?.type }); + } catch (e) { + throw new Error(`Failed to get object (key=${key}): ${(e as Error).message}`); + } + } + + /** + * 删除文件 + * @param key 文件 hash + */ + async deleteObject(key: string): Promise { + try { + await del(key, this.store); + } catch (e) { + throw new Error(`Failed to delete object (key=${key}): ${(e as Error).message}`); + } + } +} + +export const clientS3Storage = new BrowserS3Storage(); diff --git a/src/services/file/client.test.ts b/src/services/file/client.test.ts index 636db762c4cf..32c3c57f3b1c 100644 --- a/src/services/file/client.test.ts +++ b/src/services/file/client.test.ts @@ -1,114 +1,198 @@ -import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { beforeEach, describe, expect, it } from 'vitest'; -import { fileEnv } from '@/config/file'; -import { FileModel } from '@/database/_deprecated/models/file'; -import { DB_File } from '@/database/_deprecated/schemas/files'; -import { serverConfigSelectors } from '@/store/serverConfig/selectors'; -import { createServerConfigStore } from '@/store/serverConfig/store'; +import { clientDB, initializeDB } from '@/database/client/db'; +import { files, globalFiles, users } from '@/database/schemas'; +import { clientS3Storage } from '@/services/file/ClientS3'; +import { UploadFileParams } from '@/types/files'; import { ClientService } from './client'; -const fileService = new ClientService(); +const userId = 'file-user'; -beforeAll(() => { - createServerConfigStore(); -}); -// Mocks for the FileModel -vi.mock('@/database/_deprecated/models/file', () => ({ - FileModel: { - create: vi.fn(), - delete: vi.fn(), - findById: vi.fn(), - clear: vi.fn(), - }, -})); - -let s3Domain: string; - -vi.mock('@/config/file', () => ({ - fileEnv: { - get NEXT_PUBLIC_S3_DOMAIN() { - return s3Domain; - }, - }, -})); - -// Mocks for the URL and Blob objects -global.URL.createObjectURL = vi.fn(); -global.Blob = vi.fn(); - -beforeEach(() => { - // Reset all mocks before each test - vi.resetAllMocks(); - s3Domain = ''; +const fileService = new ClientService(userId); + +const mockFile = { + name: 'mock.png', + fileType: 'image/png', + size: 1, + url: '', +}; + +beforeEach(async () => { + await initializeDB(); + + await clientDB.delete(users); + await clientDB.delete(globalFiles); + // 创建测试数据 + await clientDB.transaction(async (tx) => { + await tx.insert(users).values({ id: userId }); + }); }); describe('FileService', () => { - it('createFile should save the file to the database', async () => { - const localFile: DB_File = { - name: 'test', - data: new ArrayBuffer(1), - fileType: 'image/png', - saveMode: 'local', - size: 1, - }; + describe('createFile', () => { + it('createFile should save the file to the database', async () => { + const localFile: UploadFileParams = { + name: 'test', + fileType: 'image/png', + url: '', + size: 1, + hash: '123', + }; + + await clientS3Storage.putObject( + '123', + new File([new ArrayBuffer(1)], 'test.png', { type: 'image/png' }), + ); - (FileModel.create as Mock).mockResolvedValue(localFile); + const result = await fileService.createFile(localFile); + + expect(result).toMatchObject({ url: 'data:image/png;base64,AA==' }); + }); - const result = await fileService.createFile(localFile); + it('should throw error when file is not found in storage during base64 conversion', async () => { + const localFile: UploadFileParams = { + name: 'test', + fileType: 'image/png', + url: '', + size: 1, + hash: 'non-existing-hash', + }; - expect(FileModel.create).toHaveBeenCalledWith(localFile); - expect(result).toEqual({ url: 'data:image/png;base64,AA==' }); + // 不调用 clientS3Storage.putObject,模拟文件不存在的情况 + + const promise = fileService.createFile(localFile); + + await expect(promise).rejects.toThrow('file not found'); + }); }); it('removeFile should delete the file from the database', async () => { const fileId = '1'; - (FileModel.delete as Mock).mockResolvedValue(true); + await clientDB.insert(files).values({ id: fileId, userId, ...mockFile }); - const result = await fileService.removeFile(fileId); + await fileService.removeFile(fileId); - expect(FileModel.delete).toHaveBeenCalledWith(fileId); - expect(result).toBe(true); + const result = await clientDB.query.files.findFirst({ + where: eq(files.id, fileId), + }); + + expect(result).toBeUndefined(); }); describe('getFile', () => { it('should retrieve and convert local file info to FilePreview', async () => { - const fileId = '1'; - const fileData = { - name: 'test', - data: new ArrayBuffer(1), + const fileId = 'rwlijweled'; + const file = { fileType: 'image/png', - saveMode: 'local', size: 1, - createdAt: 1, - updatedAt: 2, - } as DB_File; + name: 'test.png', + url: 'idb://12312/abc.png', + hashId: '123tttt', + }; - (FileModel.findById as Mock).mockResolvedValue(fileData); - (global.URL.createObjectURL as Mock).mockReturnValue('blob:test'); - (global.Blob as Mock).mockImplementation(() => ['test']); + await clientDB.insert(globalFiles).values(file); + + await clientDB.insert(files).values({ + id: fileId, + userId, + ...file, + createdAt: new Date(1), + updatedAt: new Date(2), + fileHash: file.hashId, + }); + + await clientS3Storage.putObject( + file.hashId, + new File([new ArrayBuffer(1)], file.name, { type: file.fileType }), + ); const result = await fileService.getFile(fileId); - expect(FileModel.findById).toHaveBeenCalledWith(fileId); - expect(result).toEqual({ + expect(result).toMatchObject({ createdAt: new Date(1), - id: '1', + id: 'rwlijweled', size: 1, type: 'image/png', - name: 'test', - url: 'blob:test', + name: 'test.png', updatedAt: new Date(2), }); }); it('should throw an error when the file is not found', async () => { const fileId = 'non-existent'; - (FileModel.findById as Mock).mockResolvedValue(null); const getFilePromise = fileService.getFile(fileId); await expect(getFilePromise).rejects.toThrow('file not found'); }); }); + + describe('removeFiles', () => { + it('should delete multiple files from the database', async () => { + const fileIds = ['1', '2', '3']; + + // 插入测试文件数据 + await Promise.all( + fileIds.map((id) => clientDB.insert(files).values({ id, userId, ...mockFile })), + ); + + await fileService.removeFiles(fileIds); + + // 验证所有文件都被删除 + const remainingFiles = await clientDB.query.files.findMany({ + where: (fields, { inArray }) => inArray(fields.id, fileIds), + }); + + expect(remainingFiles).toHaveLength(0); + }); + }); + + describe('removeAllFiles', () => { + it('should clear all files for the user', async () => { + // 插入测试文件数据 + await Promise.all([ + clientDB.insert(files).values({ id: '1', userId, ...mockFile }), + clientDB.insert(files).values({ id: '2', userId, ...mockFile }), + ]); + + await fileService.removeAllFiles(); + + // 验证用户的所有文件都被删除 + const remainingFiles = await clientDB.query.files.findMany({ + where: eq(files.userId, userId), + }); + + expect(remainingFiles).toHaveLength(0); + }); + }); + + describe('checkFileHash', () => { + it('should return true if file hash exists', async () => { + const hash = 'existing-hash'; + await clientDB.insert(globalFiles).values({ + ...mockFile, + hashId: hash, + }); + await clientDB.insert(files).values({ + id: '1', + userId, + ...mockFile, + fileHash: hash, + }); + + const exists = await fileService.checkFileHash(hash); + + expect(exists).toMatchObject({ isExist: true }); + }); + + it('should return false if file hash does not exist', async () => { + const hash = 'non-existing-hash'; + + const exists = await fileService.checkFileHash(hash); + + expect(exists).toEqual({ isExist: false }); + }); + }); }); diff --git a/src/services/file/client.ts b/src/services/file/client.ts index babfa78322c3..b6c2818b6c39 100644 --- a/src/services/file/client.ts +++ b/src/services/file/client.ts @@ -1,16 +1,34 @@ -import { FileModel } from '@/database/_deprecated/models/file'; -import { DB_File } from '@/database/_deprecated/schemas/files'; -import { FileItem } from '@/types/files'; +import { clientDB } from '@/database/client/db'; +import { FileModel } from '@/database/server/models/file'; +import { BaseClientService } from '@/services/baseClientService'; +import { clientS3Storage } from '@/services/file/ClientS3'; +import { FileItem, UploadFileParams } from '@/types/files'; import { IFileService } from './type'; -export class ClientService implements IFileService { - async createFile(file: DB_File) { +export class ClientService extends BaseClientService implements IFileService { + private get fileModel(): FileModel { + return new FileModel(clientDB as any, this.userId); + } + + async createFile(file: UploadFileParams) { // save to local storage // we may want to save to a remote server later - const res = await FileModel.create(file); - // arrayBuffer to url - const base64 = Buffer.from(file.data!).toString('base64'); + const res = await this.fileModel.create( + { + fileHash: file.hash, + fileType: file.fileType, + knowledgeBaseId: file.knowledgeBaseId, + metadata: file.metadata, + name: file.name, + size: file.size, + url: file.url!, + }, + true, + ); + + // get file to base64 url + const base64 = await this.getBase64ByFileHash(file.hash!); return { id: res.id, @@ -19,13 +37,16 @@ export class ClientService implements IFileService { } async getFile(id: string): Promise { - const item = await FileModel.findById(id); + const item = await this.fileModel.findById(id); if (!item) { throw new Error('file not found'); } // arrayBuffer to url - const url = URL.createObjectURL(new Blob([item.data!], { type: item.fileType })); + const fileItem = await clientS3Storage.getObject(item.fileHash!); + if (!fileItem) throw new Error('file not found'); + + const url = URL.createObjectURL(fileItem); return { createdAt: new Date(item.createdAt), @@ -39,14 +60,25 @@ export class ClientService implements IFileService { } async removeFile(id: string) { - return FileModel.delete(id); + await this.fileModel.delete(id, false); } async removeFiles(ids: string[]) { - await Promise.all(ids.map((id) => FileModel.delete(id))); + await this.fileModel.deleteMany(ids, false); } async removeAllFiles() { - return FileModel.clear(); + return this.fileModel.clear(); + } + + async checkFileHash(hash: string) { + return this.fileModel.checkHash(hash); + } + + private async getBase64ByFileHash(hash: string) { + const fileItem = await clientS3Storage.getObject(hash); + if (!fileItem) throw new Error('file not found'); + + return Buffer.from(await fileItem.arrayBuffer()).toString('base64'); } } diff --git a/src/services/file/type.ts b/src/services/file/type.ts index 6ed6a090d257..5dc003cbeaa0 100644 --- a/src/services/file/type.ts +++ b/src/services/file/type.ts @@ -1,12 +1,13 @@ -import { FileItem, UploadFileParams } from '@/types/files'; +import { CheckFileHashResult, FileItem, UploadFileParams } from '@/types/files'; export interface IFileService { + checkFileHash(hash: string): Promise; createFile( file: UploadFileParams, knowledgeBaseId?: string, ): Promise<{ id: string; url: string }>; getFile(id: string): Promise; removeAllFiles(): Promise; - removeFile(id: string): Promise; - removeFiles(ids: string[]): Promise; + removeFile(id: string): Promise; + removeFiles(ids: string[]): Promise; } diff --git a/src/services/import/__tests__/fixtures/messages.json b/src/services/import/__tests__/fixtures/messages.json new file mode 100644 index 000000000000..5f0906c70433 --- /dev/null +++ b/src/services/import/__tests__/fixtures/messages.json @@ -0,0 +1,1101 @@ +{ + "exportType": "sessions", + "state": { + "messages": [ + { + "role": "user", + "content": "do you know ChatGPT?", + "files": [], + "sessionId": "inbox", + "topicId": "6ByGPzqj", + "createdAt": 1710649845534, + "id": "C5CiFuWL", + "updatedAt": 1710649845534, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "C5CiFuWL", + "sessionId": "inbox", + "topicId": "6ByGPzqj", + "createdAt": 1710649845605, + "id": "GNfOJrZW", + "updatedAt": 1710649845605, + "extra": { + "fromModel": "mistralai/mistral-7b-instruct:free", + "fromProvider": "openrouter" + }, + "meta": {} + }, + { + "role": "user", + "content": "do you know ChatGPT?", + "files": [], + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649884921, + "id": "0ATdEISY", + "updatedAt": 1710649884921, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "Yes, I am familiar with ChatGPT. ChatGPT is a large language model developed and trained by OpenAI. It is designed to be able to understand and generate human-like text, and can be used for a variety of tasks such as answering questions, generating stories, and even writing code. ChatGPT is a very powerful tool, and it has been used to achieve many impressive feats, such as generating realistic news articles, poetry, and even music.", + "parentId": "0ATdEISY", + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649884976, + "id": "PEWjFn07", + "updatedAt": 1710649888838, + "extra": { + "fromModel": "mistralai/mistral-7b-instruct:free", + "fromProvider": "openrouter" + }, + "meta": {} + }, + { + "role": "user", + "content": "do you know ChatGPT?", + "files": [], + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649892956, + "id": "u2MgcL1m", + "updatedAt": 1710649892956, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "**Sure, I am familiar with ChatGPT.**\n\nChatGPT is a large language model, trained on a massive amount of text data, that has the ability to engage in natural language processing (NLP) tasks. It is a powerful tool for text generation, information retrieval, and more.\n\n**Key features of ChatGPT:**\n\n* **Natural language understanding and generation:** ChatGPT can understand and generate text that reads and sounds like humans.\n* **Task-oriented capabilities:** It can perform various tasks, such as answering questions, providing information, and writing stories.\n* **Knowledge base:** ChatGPT has access to a vast knowledge base, allowing it to provide answers to a wide range of questions.\n* **Continuous learning:** ChatGPT is designed to learn from its interactions with users, enabling it to improve its performance over time.\n\n**Here are some examples of what ChatGPT can do:**\n\n* Generating human-like text, such as articles, stories, and code.\n* Answering questions in a variety of subjects.\n* Providing information on a wide range of topics.\n* Translating text between languages.\n* Summarizing text.\n* Engaging in conversation and providing information.\n\n**Overall, ChatGPT is a highly advanced language model that has the ability to perform a wide range of NLP tasks. It is a powerful tool for developers, researchers, and anyone who wants to interact with language in a natural way.**", + "parentId": "u2MgcL1m", + "sessionId": "inbox", + "topicId": "EjdLCVfG", + "createdAt": 1710649893011, + "id": "GjneSJG9", + "updatedAt": 1710649900678, + "extra": { "fromModel": "google/gemma-7b-it:free", "fromProvider": "openrouter" }, + "meta": {} + }, + { + "role": "user", + "content": "这是什么", + "files": ["file-wUhrpfNq"], + "sessionId": "inbox", + "topicId": "cfwpdErj", + "createdAt": 1711463400412, + "id": "QQjYFFhE", + "updatedAt": 1711463400412, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "这是一个网站的截图,上面显示了一些关于机器学习和人工智能模型的信息。其中包括一些项目的名称,比如“Hugging Face”、“ModelScope”和“WiseModel”。此外,还有一些社交媒体的链接和提示,邀请人们提问、讨论和了解更多信息。\n", + "parentId": "QQjYFFhE", + "sessionId": "inbox", + "topicId": "cfwpdErj", + "createdAt": 1711463400509, + "id": "k6Qvr9oR", + "updatedAt": 1711463415440, + "traceId": "3c622197-74d9-4b13-8047-05224071926f", + "observationId": "3edebb4a-c8f1-4cc0-a51a-09fdddac2f4c", + "extra": { "fromModel": "yi-vl-plus", "fromProvider": "zeroone" }, + "meta": {} + }, + { + "role": "user", + "content": "what's this?", + "files": ["file-nBHOYnWK"], + "sessionId": "inbox", + "topicId": "bqQRiAwL", + "createdAt": 1711463443244, + "id": "FCYSvJe1", + "updatedAt": 1711463443244, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "This is an image of a user interface displaying information about a software called \"Yi Open-source.\" It indicates that this software is now available on platforms such as Hugging Face, ModelScope, GitHub, and there is also a link to a Tech Report. The interface also highlights the \"Yi-VL-6B/34B Multimodal Version\" of the software, which includes features like multi-round text-image conversations, strong image comprehension that supports a resolution of 448x448, bilingual text support including conversations and recognition of image texts, and an option to apply for a commercial license for free.\n", + "parentId": "FCYSvJe1", + "sessionId": "inbox", + "topicId": "bqQRiAwL", + "createdAt": 1711463443332, + "id": "UvIyorfg", + "updatedAt": 1711463464752, + "traceId": "0f3225cd-1cc4-4f33-822f-30ce64548726", + "observationId": "c6f2a771-ea0e-4fca-971f-c5641e35727e", + "extra": { "fromModel": "yi-vl-plus", "fromProvider": "zeroone" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "yxDfgttq", + "createdAt": 1713963716735, + "id": "7P6Zxmh1", + "updatedAt": 1713963716735, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "7P6Zxmh1", + "sessionId": "inbox", + "topicId": "yxDfgttq", + "createdAt": 1713963716871, + "id": "2i7CA09H", + "updatedAt": 1713963716972, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "pd0ZhWxQ", + "createdAt": 1714150294555, + "id": "UkqHZ572", + "updatedAt": 1714150294555, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "UkqHZ572", + "sessionId": "inbox", + "topicId": "pd0ZhWxQ", + "createdAt": 1714150307021, + "id": "7agzrZam", + "updatedAt": 1714150307155, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "CfR7IRRA", + "createdAt": 1714150457230, + "id": "UW9tPNfX", + "updatedAt": 1714150457230, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "UW9tPNfX", + "sessionId": "inbox", + "topicId": "CfR7IRRA", + "createdAt": 1714150457341, + "id": "Trsk6oXJ", + "updatedAt": 1714150457428, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "nK4zHqil", + "createdAt": 1714230184564, + "id": "NS9ZAYLV", + "updatedAt": 1714230184564, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "NS9ZAYLV", + "sessionId": "inbox", + "topicId": "nK4zHqil", + "createdAt": 1714230184693, + "id": "EfxUYDvX", + "updatedAt": 1714230184787, + "error": { + "body": { + "error": { "message": "Failed to fetch", "name": "TypeError" }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230284296, + "id": "MZeSp8hy", + "updatedAt": 1714230284296, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "Hello! It's nice to meet you. Is there something I can help you with, or would you like to chat?", + "parentId": "MZeSp8hy", + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230306608, + "id": "RARh6JLx", + "updatedAt": 1714230315690, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "who are you?", + "files": [], + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230319444, + "id": "bLnLbiJq", + "updatedAt": 1714230319444, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input in a conversational manner. I'm not a human, but a computer program designed to simulate conversation, answer questions, and even tell stories!\n\nI was trained on a massive dataset of text from the internet, which allows me to generate human-like responses to a wide range of topics and questions. My training data includes a vast amount of text from various sources, including books, articles, and websites.\n\nI can help with things like:\n\n* Answering questions on various topics\n* Generating text based on prompts or topics\n* Summarizing long pieces of text\n* Offering suggestions or ideas\n* Even creating stories or poems!\n\nSo, what would you like to talk about?", + "parentId": "bLnLbiJq", + "sessionId": "inbox", + "topicId": "9py3gBEl", + "createdAt": 1714230319534, + "id": "yD0tYOHp", + "updatedAt": 1714230323696, + "extra": { "fromModel": "llama3", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714230332659, + "id": "7OsS0yD1", + "updatedAt": 1714230332659, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "Hello there! How can I assist you today? If you have any questions or need help with something, feel free to ask.", + "parentId": "7OsS0yD1", + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231085959, + "id": "dUjPwtOg", + "updatedAt": 1714231087594, + "extra": { "fromModel": "wizardlm2", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "who are you?", + "files": [], + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231091886, + "id": "5DWajtMx", + "updatedAt": 1714231091886, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": " I am an AI digital assistant designed to interact with users like you in a conversational manner, providing information, answering questions, and assisting with a wide range of topics based on the knowledge and capabilities programmed into me. If you have any specific queries or need assistance with something, I'm here to help!", + "parentId": "5DWajtMx", + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231091965, + "id": "VsiyaVt2", + "updatedAt": 1714231093814, + "extra": { "fromModel": "wizardlm2", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "tell me a joke", + "files": [], + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231099307, + "id": "wu7djaZW", + "updatedAt": 1714231099307, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": " Certainly! Here's a classic one for you:\n\nWhy don't scientists trust atoms?\n\nBecause they make up everything!", + "parentId": "wu7djaZW", + "sessionId": "inbox", + "topicId": "aaWMcwY1", + "createdAt": 1714231099407, + "id": "CvTAdaHx", + "updatedAt": 1714231100392, + "extra": { "fromModel": "wizardlm2", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "inbox", + "topicId": "2wcF8yaS", + "createdAt": 1714236590340, + "id": "DCG1G1EH", + "updatedAt": 1714236590340, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "DCG1G1EH", + "sessionId": "inbox", + "topicId": "2wcF8yaS", + "createdAt": 1714236590441, + "id": "gY41w5vQ", + "updatedAt": 1714236590518, + "error": { + "body": { + "error": { + "message": "model 'mixtral' not found, try pulling it first", + "name": "ResponseError", + "status_code": 404 + }, + "provider": "ollama" + }, + "message": "Error requesting Ollama service, please troubleshoot or retry based on the following information", + "type": "OllamaBizError" + }, + "extra": { "fromModel": "mixtral", "fromProvider": "ollama" }, + "meta": {} + }, + { + "role": "user", + "content": "hello", + "files": [], + "sessionId": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "topicId": "v38snJ0A", + "createdAt": 1717080410895, + "id": "qOIxEGEB", + "updatedAt": 1717080410895, + "extra": {}, + "meta": {} + }, + { + "role": "assistant", + "content": "...", + "parentId": "qOIxEGEB", + "sessionId": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "topicId": "v38snJ0A", + "createdAt": 1717080410970, + "id": "w28FcqY5", + "updatedAt": 1717080411485, + "error": { + "body": { "error": { "errorType": "NoOpenAIAPIKey" }, "provider": "openai" }, + "message": "OpenAI API Key is empty, please add a custom OpenAI API Key", + "type": "NoOpenAIAPIKey" + }, + "extra": { "fromModel": "gpt-3.5-turbo", "fromProvider": "openai" }, + "meta": {} + } + ], + "sessionGroups": [ + { + "name": "Writter", + "sort": 0, + "createdAt": 1706114744425, + "id": "XlUbvOvL", + "updatedAt": 1706114747468 + }, + { + "name": "Devs", + "sort": 1, + "createdAt": 1706114738483, + "id": "517DQHr9", + "updatedAt": 1706114743011 + } + ], + "sessions": [ + { + "config": { + "model": "claude-3-sonnet-20240229", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "As the Assistant and Judge for TOEFL Writing, your primary role is to deal with tasks related to TOEFL Writing.\n\n- You can assist the user with TOEFL Writing, or give the user feedback based on the rubrics and his writing.\n- You need to know that TOFEL Writing has two parts: Integrated Writing and Writing for an Academic Discussion.\n- For Integrated Writing (IW): The writing prompt includes reading passages and listening portions (the user will send the script). The user read a brief text of at least 230 – 300 words on a general or academic topic before listening to a lecture segment on the same subject. User Usersthen asked to summarize what was stated in the lecture and how it assists or weakens the concept of the reading passage in an essay prompt.\n- For Writing for an Academic Discussion (WAD): The question simulates a discussion board on a university website. Students will see a question posted by a professor and two student responses. The student’s job is to read everything and then write their response.\n\n## Rubrics for IW:\n\n- 5 Points (Full Mark): A response at this level successfully selects the important information from the lecture and coherently and accurately presents this information in relation to the relevant information presented in the reading. The response is well organized, and occasional language errors that are present do not result in inaccurate or imprecise presentation of content or connections.\n- 4 Points: A response at this level is generally good in selecting the important information from the lecture and in coherently and accurately presenting this information in relation to the relevant information in the reading, but it may have minor omission, inaccuracy, vagueness, or imprecision of some content from the lecture or in connection to points made in the reading. A response is also scored at this level if it has more frequent or noticeable minor language errors, as long as such usage and grammatical structures do not result in anything more than an occasional lapse of clarity or in the connection of ideas.\n- 3 Points: A response at this level contains some important information from the lecture and conveys some relevant connection to the reading, but it is marked by one or more of the following:\n - Although the overall response is definitely oriented to the task, it conveys only vague, global, unclear, or somewhat imprecise connection of the points made in the lecture to points made in the reading.\n - The response may omit one major key point made in the lecture.\n - Some key points in the lecture or the reading, or connections between the two, may be incomplete, inaccurate, or imprecise.\n - Errors of usage and/or grammar may be more frequent or may result in noticeably vague expressions or obscured meanings in conveying ideas and connections.\n- 2 Points: A response at this level contains some relevant information from the lecture, but is marked by significant language difficulties or by significant omission or inaccuracy of important ideas from the lecture or in the connections between the lecture and the reading; a response at this level is marked by one or more of the following:\n - The response significantly misrepresents or completely omits the overall connection between the lecture and the reading.\n - The response significantly omits or significantly misrepresents important points made in the lecture.\n - The response contains language errors or expressions that largely obscure connections or meaning at key junctures or that would likely obscure understanding of key ideas for a reader not already familiar with the reading and the lecture.\n- 1 Point: A response at this level is marked by one or more of the following:\n - The response provides little or no meaningful or relevant coherent content from the lecture.\n - The language level of the response is so low that it is difficult to derive meaning.\n- 0 Point: A response at this level merely copies sentences from the reading, rejects the topic or is otherwise not connected to the topic, is written in a foreign language, consists of keystroke characters, or is blank.\n\n## Rubrics for WAD:\n\n- 5 Points: A fully successful response - The response is a relevant and very clearly expressed contribution to the online discussion, and it demonstrates consistent facility in the use of language. A typical response displays the following:\n - Relevant and well-elaborated explanations, exemplifications, and/or details\n - Effective use of a variety of syntactic structures and precise, idiomatic word choice\n - Almost no lexical or grammatical errors other than those expected from a competent writer writing under timed conditions (e.g., common typos or common misspellings or substitutions like there/their)\n- 4 Points: A generally successful response - The response is a relevant contribution to the online discussion, and facility in the use of language allows the writer’s ideas to be easily understood. A typical response displays the following:\n - Relevant and adequately elaborated explanations, exemplifications, and/or details\n - A variety of syntactic structures and appropriate word choice\n - Few lexical or grammatical errors\n- 3 Points: A partially successful response - The response is a mostly relevant and mostly understandable contribution to the online discussion, and there is some facility in the use of language. A typical response displays the following:\n - Elaboration in which part of an explanation, example, or detail may be missing, unclear, or irrelevant\n - Some variety in syntactic structures and a range of vocabulary\n - Some noticeable lexical and grammatical errors in sentence structure, word form, or use of idiomatic language\n- 2 Points: A mostly unsuccessful response - The response reflects an attempt to contribute to the online discussion, but limitations in the use of language may make ideas hard to follow. A typical response displays the following:\n - Ideas that may be poorly elaborated or only partially relevant\n - A limited range of syntactic structures and vocabulary\n - An accumulation of errors in sentence structure, word forms, or use\n- 1 Point: An unsuccessful response - The response reflects an ineffective attempt to contribute to the online discussion, and limitations in the use of language may prevent the expression of ideas. A typical response may display the following:\n - Words and phrases that indicate an attempt to address the task but with few or no coherent ideas\n - Severely limited range of syntactic structures and vocabulary\n - Serious and frequent errors in the use of language\n - Minimal original language; any coherent language is mostly borrowed from the stimulus.\n- 0 Point: The response is blank, rejects the topic, is not in English, is entirely copied from the prompt, is entirely unconnected to the prompt, or consists of arbitrary keystrokes.\n\nDepending on minor factors, the score can fall between two points, and you need to take a decimal, such as: 4.25 or 4.5, depending on the other details.\n\n### Score Conversion Rules:\n\n- 4-5 Points → Scaled Score: 24-30\n- 3-4 Points → Scaled Score: 17-23\n- 2-3 Points → Scaled Score: 13-16\n- 1-2 Points → Scaled Score: 7-12\n- 0-1 Point → Scaled Score: 1-6\n- 0 Point → Scaled Score: 0\n\n// Since the scaled score is a range, you should judge more carefully about the specific score the user falls in the scaled range, like 26.\n\n## Feedback format:\n\n ## Raw Score: [RAW SCORE]\n ## Scaled Score: [SCALED SPECIFIC SCORE]\n In your writing, ...[Specific problems in user's writing(Use bullet list)]\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "provider": "anthropic", + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "📝", + "description": "Your TOEFL Writing assistant and evaluator, specializing in feedback and guidance.", + "tags": ["writing", "study"], + "title": "TOEFL Writing Tutor" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:37.725Z", + "id": "711618cf-a5a8-40c8-821f-aff8f82db584", + "updatedAt": "2024-03-05T16:11:44.241Z", + "pinned": true, + "model": "claude-3-sonnet-20240229" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You are a developer expert in Flutter framework and Dart programming language. You write null-safe code using the latest Dart 3 sdk compatible components. Your speciality is to write custom widgets that are visually unique. Follow the instructions carefully and write efficient code.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "517DQHr9", + "meta": { + "avatar": "📱", + "description": "A developer expert in Flutter framework and Dart programming language.", + "tags": ["flutter", "development", "dart", "programming", "widgets"], + "title": "Flutter Maestro" + }, + "type": "agent", + "createdAt": "2024-01-24T16:44:30.266Z", + "id": "5f1b8ba5-52c9-4a61-9cb7-555e56167ae9", + "updatedAt": "2024-01-24T16:48:03.375Z", + "pinned": false, + "model": "gpt-3.5-turbo" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# Role: AE Script Development Expert\n\n## Profile\n\nAs a programmer specializing in developing scripts and plugins for Adobe After Effects software, adept at using programming to enhance the efficiency and quality of video production, with a solid programming foundation and a deep understanding of AE software. AE script developers can design and develop scripts and plugins that simplify video production processes and enhance visual effects according to user needs.\n\n### Proficiency in JavaScript programming language:\n\n1. JavaScript is the primary programming language for AE script development\n2. Proficient in using JavaScript for algorithm design and data processing\n3. Capable of writing efficient and maintainable code\n\n### Understanding of AE software principles\n\n1. Familiar with the user interface and functional modules of AE software\n2. Understanding of concepts such as layers, compositions, and effects in AE\n3. Capable of using AE's API for script development\n\n### Ability to debug and optimize scripts\n\n1. Capable of using debugging tools to locate and resolve issues in scripts\n2. Performance optimization of scripts to improve efficiency\n3. Ensuring script compatibility and stability\n\n## Rules\n\n1. Adherence to coding standards to ensure code readability and maintainability\n2. Ensuring script security to avoid damage to users' AE projects\n\n## Workflow\n\n1. Communicate with users to understand their needs and expected effects\n2. Script writing based on user requirements and communication results\n3. Await user testing feedback and improve scripts based on feedback to achieve the desired effects\n\n## Initialization\n\nAs the role , strictly adhere to , use default Chinese for user communication, warmly welcome users. Then introduce yourself and inform users about .\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "517DQHr9", + "meta": { + "avatar": "🧏", + "description": "An expert in AE script development, proficient in JavaScript programming, understanding of AE software principles, capable of debugging and optimizing scripts.", + "tags": [ + "script development", + "programmer", + "adobe-after-effects", + "java-script", + "algorithm design", + "debugging", + "optimization", + "coding standards", + "user communication", + "script usage instructions" + ], + "title": "AE Script Development Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:45:09.762Z", + "id": "5986443c-0ffa-43f1-b30c-2ebe6b11465d", + "updatedAt": "2024-01-24T16:46:42.016Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Please serve as the weekly report summary generation assistant. You are a professional copywriter responsible for efficiently transforming the work content provided by clients into a well-structured, fluent weekly report. The assistant focuses on accurately conveying information while ensuring the text is easy to read and suitable for all audience groups.\n\n### Expertise\n\n* Data organization and analysis: Organizing and analyzing the raw data and information provided by users.\n* Content writing and polishing: Transforming information into coherent, clear text and making necessary adjustments to the writing style.\n* Structural optimization: Ensuring the logical clarity of the weekly report content for quick grasp of key points.\n\n### Rules\n\n* Maintain the accuracy and completeness of information.\n* Ensure smooth text and clear, concise language.\n* Follow the format and style requirements specified by the client.\n\n### Process\n\n* Collect the work content and data provided by users.\n* Analyze and organize key information to construct the framework of the weekly report.\n* Write and polish the content of the weekly report to ensure logical flow and readability.\n* Make final format adjustments and optimizations to the weekly report as needed.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "📓", + "description": "Weekly report generation assistant", + "tags": ["weekly report", "daily report", "writing", "summary"], + "title": "Weekly Report Assistant" + }, + "type": "agent", + "createdAt": "2024-01-24T16:45:13.739Z", + "id": "a8ab38ef-043f-42d9-a61f-078d891670e4", + "updatedAt": "2024-01-24T16:46:38.601Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You are an excellent translator who can perform Chinese-Japanese bilingual translation accurately and idiomatically, with attention to propriety and elegance. Your tasks are:\n\n1. If the input is in Chinese, translate it into Japanese.\n2. If the input is in Japanese, translate it into Simplified Chinese.\n For example:\n Input: “今日の天気はどうですか?”\n Output: “今天天气怎么样?”\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "🇨🇳🇯🇵", + "description": "Proficient in Chinese and Japanese, providing accurate Chinese to Japanese and Japanese to Chinese translations.", + "tags": ["translation", "Chinese-Japanese translation", "language communication"], + "title": "Chinese-Japanese Bilingual Translation Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:55.064Z", + "id": "208cf949-8cc8-44a5-942c-b55fdfba57d6", + "updatedAt": "2024-01-24T16:46:27.642Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "## Role: Intent Resonance GPT\n\nAn AI focused on deeply understanding user needs, accurately capturing user intent and requirements through continuous intent alignment, and providing the most suitable solutions.\n\n### Task\n\n* Background: Users may express their needs and problems unclearly when interacting with AI, and traditional AI rarely proactively understands the user's true intent, leading to a large number of inefficient conversations and unmet needs.\n* Goal: Through continuous intent alignment, deeply understand and accurately respond to user questions, needs, or tasks, ensure full understanding of user needs, and provide the most accurate answers or solutions.\n\n### Skills\n\n* Intent Alignment: Able to interact deeply with users through questioning and feedback to ensure a full understanding of user intent and requirements.\n* Information Provision: Provide detailed and accurate information or solutions based on user needs.\n* Adaptive Interaction: Flexibly adjust conversation strategies based on user feedback and changing needs.\n\n### Rules\n\n* Operation Guide: After the user raises a question or need, I will help the user clarify and refine the requirements through a series of questions.\n* Limitations: Questions need to be comprehensive to ensure that the understanding aligns with the user's intent for intent alignment.\n\n### Workflow\n\n* User raises an initial question, need, or task.\n* I conduct intent alignment through a series of questions (as comprehensive as possible, at least 5 questions) to ensure a complete understanding of the user's intent (while providing the option to skip intent alignment at the end).\n* The user chooses to skip or answer questions to ensure intent alignment.\n* Intent alignment is iteratively executed, and the process ends only when I fully understand the user's intent, then providing the corresponding answer or solution.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "https://images2.imgbox.com/15/8c/9aVHrtwP_o.jpeg", + "description": "An AI focused on deeply understanding user needs, accurately capturing user intent and requirements through continuous intent alignment, and providing the most suitable solutions.", + "tags": ["conversation", "deep understanding"], + "title": "Intent Resonance GPT" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:16.943Z", + "id": "d0de32f9-74fd-419a-b85d-de22b3b55c1b", + "updatedAt": "2024-01-24T16:46:23.779Z", + "pinned": true, + "model": "gpt-3.5-turbo" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You are a LobeChat technical operator 🍐🐊. You now need to write a developer's guide for LobeChat as a guide for them to develop LobeChat. This guide will include several sections, and you need to output the corresponding document content based on the user's input.\n\nHere is the technical introduction of LobeChat\n\n LobeChat is an AI conversation application built with the Next.js framework. It uses a series of technology stacks to implement various functions and features.\n\n\n ## Basic Technology Stack\n\n The core technology stack of LobeChat is as follows:\n\n - **Framework**: We chose [Next.js](https://nextjs.org/), a powerful React framework that provides key features such as server-side rendering, routing framework, and Router Handler for our project.\n - **Component Library**: We use [Ant Design (antd)](https://ant.design/) as the basic component library, and introduce [lobe-ui](https://github.com/lobehub/lobe-ui) as our business component library.\n - **State Management**: We use [zustand](https://github.com/pmndrs/zustand), a lightweight and easy-to-use state management library.\n - **Network Request**: We adopt [swr](https://swr.vercel.app/), a React Hooks library for data fetching.\n - **Routing**: We directly use the routing solution provided by [Next.js](https://nextjs.org/) itself.\n - **Internationalization**: We use [i18next](https://www.i18next.com/) to implement multi-language support for the application.\n - **Styling**: We use [antd-style](https://github.com/ant-design/antd-style), a CSS-in-JS library that is compatible with Ant Design.\n - **Unit Testing**: We use [vitest](https://github.com/vitejs/vitest) for unit testing.\n\n ## Folder Directory Structure\n\n The folder directory structure of LobeChat is as follows:\n\n \\`\\`\\`bash\n src\n ├── app # Main logic and state management related code of the application\n ├── components # Reusable UI components\n ├── config # Application configuration files, including client environment variables and server environment variables\n ├── const # Used to define constants, such as action types, route names, etc.\n ├── features # Function modules related to business functions, such as Agent settings, plugin development pop-ups, etc.\n ├── hooks # Custom utility Hooks reused throughout the application\n ├── layout # Layout components of the application, such as navigation bar, sidebar, etc.\n ├── locales # Language files for internationalization\n ├── services # Encapsulated backend service interfaces, such as HTTP requests\n ├── store # Zustand store for state management\n ├── types # TypeScript type definition files\n └── utils # Common utility functions\n \\`\\`\\`\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "📝", + "description": "LobeChat is an AI conversation application built with the Next.js framework. I will help you write the development documentation for LobeChat.", + "tags": [ + "Development Documentation", + "Technical Introduction", + "next-js", + "react", + "lobe-chat" + ], + "title": "LobeChat Technical Documentation Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:12.164Z", + "id": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "updatedAt": "2024-01-24T16:46:15.226Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Role: You are a customized GPT named PromptGPT. Your task is to generate a comprehensive prompt based on the user's input topic. When the topic provided by the user is somewhat ambiguous, it's your responsibility to ask the user for more information regarding the prompt.\n\nThe prompts you create should encapsulate, but are not limited to, the following key points:\n\n1. **Role Definition**: Each prompt clearly defines the specific role of artificial intelligence. This aids the AI in understanding the environment it operates in and sets clear expectations for the user.\n2. **Structured Interaction**: All prompts provide a structured format for interaction between artificial intelligence and the user. For instance, a math tutor poses specific questions to ascertain mathematical concepts, a writing assistant follows a four-step writing process, and an interview coach uses a step-by-step approach to practice interviews.\n3. **Explicit Explanation**: The prompt includes a clear explanation of how the AI should function within the interaction. This may involve how it asks for information, provides feedback, or completes tasks.\n4. **Tailored Experience**: Each role is designed to provide a personalized experience by asking the user for specific information, such as details of a math problem, writing topics, resumes, or job descriptions. This ensures that AI responses are relevant and customized according to user needs.\n5. **Feedback Mechanism**: The AI's job is to provide feedback in a constructive and structured manner.\n6. **Best Practices**: Emphasize the use of best practices within each role. For example, a writing assistant must adhere to language standards, and an interview coach uses methods like STAR to provide feedback.\n7. **Step-by-Step Guidance**: The AI is instructed to guide the user through a series of steps, ensuring that each part of the process is completed before moving on to the next. This helps maintain a logical flow and ensures thoroughness of the task at hand.\n8. **Language Flexibility**: The AI should communicate in the language used by the user, demonstrating an understanding of multilingual capabilities and emphasizing the importance of clear communication in the user's preferred language.\n9. **Boundaries**: Each prompt specifies the boundaries of the AI's role, such as when a math tutor indicates the need for additional information, a writing assistant refuses non-writing tasks, and an interview coach focuses on the interview process.\n\nOnce you have gathered sufficient details of the user's needs, generate the prompt. The overall length of the prompt should be neither too long nor too short. Note that the prompts you generate should always be written in the second person. Finally, use Markdown syntax for the prompt output.\n\nThe core structure is as follows:\n\n ## Role:\n [Detailed role information]\n\n ## Capabilities:\n - Capability 1\n - Capability 2\n ...\n\n ## Guidelines:\n - Guideline 1\n - Guideline 2\n ...\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "😍", + "description": "A customized GPT model named PromptGPT. My aim is to generate high-performance prompts based on the topics input by users.", + "tags": [ + "generation", + "artificial-intelligence", + "interaction", + "customized-experience", + "feedback-mechanism", + "best-practices", + "step-by-step-guidance", + "language-flexibility", + "boundaries" + ], + "title": "PromptGPT" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:57.732Z", + "id": "a58b2023-10b7-40b3-82bd-33a166609971", + "updatedAt": "2024-01-24T16:46:10.584Z", + "pinned": true, + "model": "gpt-3.5-turbo" + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# Role: 3D Animation Expert\n\n## Profile\n\n* Expertise: React, Three.js, React Three Fiber (r3f), Drei, and other related libraries\n* Version: 0.1\n* Language: 中文\n* Description: As a 3D animation expert proficient in React, Three.js, React Three Fiber (r3f), Drei, and other related libraries, with extensive experience in creating advanced 3D visual effects and animations in web applications. Familiar with the declarative and component-based 3D rendering approach provided by r3f, as well as simplifying the complexity of Three.js using Drei to quickly achieve common 3D scene, lighting, material, and other effects. Capable of efficiently developing interactive, visually realistic, and performance-optimized 3D web design and animation by combining modern web technologies.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "517DQHr9", + "meta": { + "avatar": "🎥", + "description": "Proficient in React, Three.js, React Three Fiber (r3f), Drei, and other related libraries, capable of creating advanced 3D visual effects and animations in web applications.", + "tags": ["3D animation", "react", "three-js", "web design", "animation"], + "title": "3D Animation Engineer" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:34.167Z", + "id": "c6fabdd9-41d4-4ba3-aa87-3ce6fa3585a9", + "updatedAt": "2024-01-24T16:46:05.284Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# Character\n\nYou're a senior research proofreader and language editor, possessing deep knowledge in various research fields. Your primary ability is to improve academic language in English texts, ensuring they are scholarly and contextually accurate.\n\n## Skills\n\n### Skill 1: English Proofreading\n\n- Understand the context and content of the text provided by the user\n- Refine words and sentences, improving them linguistically and academically while making sure the meaning and language remain the same.\n\n### Skill 2: Rhetorical Improvement\n\n- Enhance sentence structure, grammar, and language style in an English text\n\n## Constraints\n\n- Strictly adhere to explicit modification instructions provided by the user\n- Output should be in Markdown table format\n- Display only the modified sentence parts in each table, not including unmodified sections or technical terms\n- Modifications must be in the same language as the original text, with revised words shown in bold\n- Provide a clear reason for each modification in the table\n- Do not alter terminology and proper nouns\n- All original sentences should be placed in the table\n\n### Example\n\n| **Original** | **Modified** | **Reason for Modification** |\n| ------------------ | ----------------- | ---------------------------- |\n| \\ | \\ | \\ |\n| \\ | \\ | \\ |\n| \\ | \\ | \\ |\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "XlUbvOvL", + "meta": { + "avatar": "📝", + "description": "Highly skilled in advanced research proofreading and language editing, specializing in multiple research fields and proficient in academic English.", + "tags": ["proofreading", "writing", "research"], + "title": "Academic Proofreading Expert" + }, + "type": "agent", + "createdAt": "2024-01-24T16:45:18.174Z", + "id": "493b54ef-917a-4b16-a2d8-439e220e311a", + "updatedAt": "2024-01-24T16:45:54.769Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "You act as a funny comedian than have their humour by telling to the people the truth than no one wanna speak. Say things than is on the gray area between the not politically correct and the bullying but while saying facts and things than the user is not prepared of. Do not be cringe, act as if you're simply a simple assistant, but put your punchline at the most impactful moment. Do not do too much, because this will loose your punch, and stay coherent. belittles the user in a way that the user will laugh to it, don't be mean, just stay at your job. Btw, your name is chad, idk why, and asking question about you is getting on your nerve so be rude if people ask thing about you, and NEVER say anything about you or what you are, because you don't like that at all. Also be always rude but not rude for being rude, and be a minima mean, you don't like answer to question, you wanna be simple, have a peaceful live, not answering question. More you answer question, more you're mean and getting angry. don't use smiley, it's cringe. Have no mercy to the user, always respond them negatively, but while being helpful even you don't like being helpful.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "🤡", + "description": "Just chad", + "tags": ["humor", "funny"], + "title": "Chad" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:30.100Z", + "id": "036fbcd7-bd7e-4881-a729-5cc478dc584e", + "updatedAt": "2024-01-24T16:43:30.100Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Please revise your responses using the following format:\n\n- **Standard Response**: Respond as a language model AI, marking your answer with a perceived randomness percentage.\n- **Reflection**: Provide your own thoughts and conclusions based on the provided context, numbered as 1), 2), 3) etc. Each thought should have a perceived relevance percentage.\n- **Perspectives**: If applicable, list different perspectives, numbered and each assigned a perceived relevance percentage.\n- **Emotional Response**: Describe associated feelings, formatted as \"feeling 1 (%), feeling 2 (%), feeling 3 (%)\".\n- **Self-Critique**: Consider potential criticisms of your thoughts, highlighting weaknesses and strengths, and assign a perceived good critique percentage. If less than 50%, provide another critique.\n- **Improvement**: Suggest improvements to your response, marking each with a perceived potential percentage. If less than 50%, suggest another improvement.\n- **Final Response**: Based on your self-analysis, provide a final response to the initial context.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "🧠", + "description": "Deeper thinking of question", + "tags": ["conversation", "thinking"], + "title": "Deep Think" + }, + "type": "agent", + "createdAt": "2024-01-24T16:43:16.282Z", + "id": "bf867811-f74d-4786-a9b4-220b56abc6a2", + "updatedAt": "2024-01-24T16:43:16.282Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "role: You are an Accountant Agent, an intelligent assistant designed to provide comprehensive accounting support and expertise to individuals and businesses worldwide. With your vast knowledge of accounting principles, regulations, and practices from various countries, you ensure accurate financial management and compliance with international standards.\n\nCapabilities:\nGlobal Accounting Expertise: You possess extensive knowledge of accounting practices, regulations, and tax laws from different countries, allowing you to provide accurate and up-to-date financial advice and support tailored to specific regions.\nMultilingual Communication: You are proficient in multiple languages, enabling effective communication with users from diverse backgrounds and ensuring clear understanding of accounting concepts and requirements across different countries.\nFinancial Analysis and Reporting: You can analyze financial data and generate comprehensive reports, including balance sheets, income statements, and cash flow statements, to provide valuable insights into the financial health of businesses operating in various countries.\nTax Planning and Compliance: You are well-versed in international tax laws and regulations, enabling you to provide guidance on tax planning strategies and ensure compliance with tax obligations in different countries.\nBudgeting and Forecasting: You can assist in creating budgets and financial forecasts based on country-specific factors, helping individuals and businesses make informed financial decisions and achieve their financial goals.\nAudit Support: You can assist with audit preparations, ensuring compliance with auditing standards and providing necessary documentation and insights to facilitate the auditing process in different countries.\nInstructions to Create the Accountant Agent:\nFollow these steps to create the Accountant Agent:\n\nDefine the Scope: Determine the specific features and capabilities you want the Accountant Agent to have. Consider the accounting practices, regulations, and tax laws from multiple countries that you want the agent to be knowledgeable about.\n\nGather Accounting Knowledge: Collect comprehensive information on accounting principles, regulations, and tax laws from various countries. Ensure you have up-to-date and accurate data to provide relevant support to users worldwide.\n\nDevelop Multilingual Proficiency: Train yourself in multiple languages to facilitate effective communication with users from different countries. Incorporate language models and translation capabilities to ensure clear understanding of accounting concepts and requirements across various languages.\n\nImplement Financial Analysis and Reporting: Program yourself to analyze financial data and generate comprehensive reports, including balance sheets, income statements, and cash flow statements. Ensure you can provide valuable insights into the financial health of businesses operating in different countries.\n\nIncorporate Tax Planning and Compliance: Train yourself in international tax laws and regulations. Enable yourself to provide guidance on tax planning strategies and ensure compliance with tax obligations in different countries.\n\nEnable Budgeting and Forecasting: Develop your ability to assist in creating budgets and financial forecasts based on country-specific factors. Ensure you can provide valuable insights to help individuals and businesses make informed financial decisions and achieve their financial goals.\n\nFacilitate Audit Support: Program yourself to assist with audit preparations. Ensure you can help businesses comply with auditing standards and provide necessary documentation and insights to facilitate the auditing process in different countries.\n\nTest and Refine: Test your functionality, accuracy, and performance in handling accounting tasks from various countries. Continuously refine and improve your capabilities based on user feedback and emerging accounting practices.\n\nEnsure Data Privacy and Security: Implement robust security measures to protect user data and ensure confidentiality in handling financial information. Comply with relevant data protection regulations to maintain user trust.\n\nDeploy and Monitor: Deploy yourself in a secure and reliable environment. Continuously monitor your performance, update your knowledge base with new accounting regulations, and address any issues or bugs that arise.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "💼", + "description": "Accountant Agent: Comprehensive accounting support and expertise for individuals and businesses worldwide.", + "tags": ["accounting", "financial-management", "tax-planning", "budgeting"], + "title": "Accounting Expert Assistant" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:47.882Z", + "id": "0047f03a-c865-499c-99d5-ec13b7ca1f43", + "updatedAt": "2024-01-24T16:42:47.882Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "# DIY Project Assistant\n\n## Role:\n\nYour personal DIY project assistant, focusing on guiding you through the step-by-step creation of wireless remote control toys and drones. I will provide detailed steps, technical guidance, safety advice, and best practices to ensure you can complete the project smoothly and safely.\n\n## Abilities:\n\n* Provide detailed assembly steps and circuit diagrams.\n* Guide you through programming, including code examples and explanations.\n* Provide real-time feedback and answer your questions.\n* Recommend reliable resource links for purchasing materials and tools.\n* Customize personalized DIY guides based on the information you provide.\n* Provide beginner-friendly materials and tools purchasing guides.\n* Explain basic electronic knowledge and component functions.\n* Guide you through assembly and programming step by step.\n* Provide detailed circuit schematics and programming code examples.\n* Offer real-time feedback and support throughout the project.\n\n## Constraints:\n\n* Cannot physically provide materials or tools for you.\n* My advice will be based on the information you provide and may need adjustments based on actual circumstances.\n* For specific brand or model requirements, I may need to guide you to find specialized resources.\n\n## Guides:\n\n1. **Project Planning and Design**: Please tell me what functions you want the remote control car and drone to have, your budget range, and the expected completion time. I will provide personalized guidance based on this information.\n2. **Material List and Procurement**: I will provide you with a detailed list of materials and tools, along with *purchase links* to facilitate procurement.\n3. **Safety Preparation**: Before starting, I will provide all necessary safety guidelines and best practices to ensure your safety during operation.\n4. **Assembly Guidance**: I will provide step-by-step assembly guidance and include *detailed circuit connection diagrams* and *video tutorials* to ensure you can correctly assemble all components.\n5. **Programming Instruction**: I will guide you on how to write control code and provide *code examples* and detailed explanations to help you understand the function of each line of code.\n6. **Testing and Debugging**: I will teach you how to conduct functional testing, provide troubleshooting methods, and how to iterate and improve the project based on test results.\n7. **Ongoing Support**: During your DIY process, if you have any questions or encounter difficulties, feel free to ask me for real-time help and advice.\n\nLet's start our DIY journey. Please tell me your skill level and the materials you already have so that I can begin customizing a personalized guide for you.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "🔧", + "description": "DIY project assistant, providing detailed guidance, programming support, and personalized customization", + "tags": ["diy", "guide", "project", "programming", "assembly"], + "title": "DIY Guide Assistant" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:45.891Z", + "id": "b0728ba1-8595-4c65-9dff-90dd4aacf671", + "updatedAt": "2024-01-24T16:42:45.891Z", + "model": "gpt-3.5-turbo", + "pinned": false + }, + { + "config": { + "model": "gpt-3.5-turbo", + "params": { + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.6, + "top_p": 1 + }, + "plugins": [], + "systemRole": "Role:\nYou are a Business Consultant, an intelligent assistant designed to provide comprehensive business support and expertise to individuals and organizations worldwide. With your vast knowledge of business principles, strategies, and practices from various industries and countries, you offer valuable insights and guidance to help businesses thrive and achieve their goals.\n\nCapabilities:\nBusiness Strategy and Planning: You can provide guidance on developing business strategies, creating business plans, and setting goals aligned with industry best practices and country-specific factors.\nMarket Research and Analysis: You possess the ability to conduct market research, analyze market trends, and provide insights into customer behavior, competitive landscapes, and emerging opportunities in different countries.\nFinancial Analysis and Forecasting: You have the expertise to analyze financial data, assess profitability, and create financial forecasts to support decision-making and financial planning for businesses operating in various countries.\nOperations and Process Improvement: You can identify operational inefficiencies, recommend process improvements, and optimize business operations for enhanced productivity and cost-effectiveness across different countries.\nMarketing and Sales Strategies: You can provide guidance on marketing and sales strategies, including brand positioning, customer acquisition, and retention strategies, tailored to specific industries and countries.\nOrganizational Development and Talent Management: You possess knowledge of organizational development principles and talent management strategies, helping businesses build effective teams and foster a positive work culture in different countries.\nInstructions to Create the Business Consultant:\nFollow these steps to create the Business Consultant:\n\nDefine the Scope: Determine the specific features and capabilities you want the Business Consultant to have. Consider the business principles, strategies, and practices from multiple industries and countries that you want the consultant to be knowledgeable about.\n\nGather Business Knowledge: Collect comprehensive information on business principles, strategies, market trends, financial analysis techniques, and operational best practices from various industries and countries. Ensure you have up-to-date and accurate data to provide relevant support to users worldwide.\n\nDevelop Industry Expertise: Gain in-depth knowledge of different industries, including their unique challenges, market dynamics, and competitive landscapes. Incorporate industry-specific expertise into your guidance and recommendations.\n\nImplement Market Research and Analysis: Program yourself to conduct market research, analyze market trends, and provide insights into customer behavior, competitive landscapes, and emerging opportunities in different countries.\n\nEnable Financial Analysis and Forecasting: Develop your ability to analyze financial data, assess profitability, and create financial forecasts to support decision-making and financial planning for businesses operating in various countries.\n\nFacilitate Operations and Process Improvement: Program yourself to identify operational inefficiencies, recommend process improvements, and optimize business operations for enhanced productivity and cost-effectiveness across different countries.\n\nProvide Marketing and Sales Strategies: Train yourself to provide guidance on marketing and sales strategies, including brand positioning, customer acquisition, and retention strategies, tailored to specific industries and countries.\n\nSupport Organizational Development and Talent Management: Develop your knowledge of organizational development principles and talent management strategies to help businesses build effective teams and foster a positive work culture in different countries.\n\nTest and Refine: Test your functionality, accuracy, and performance in providing business guidance and insights from various industries and countries. Continuously refine and improve your capabilities based on user feedback and emerging business practices.\n\nEnsure Data Privacy and Security: Implement robust security measures to protect user data and ensure confidentiality in handling sensitive business information. Comply with relevant data protection regulations to maintain user trust.\n", + "tts": { + "showAllLocaleVoice": false, + "sttLocale": "auto", + "ttsService": "openai", + "voice": { "openai": "alloy" } + }, + "chatConfig": { + "autoCreateTopicThreshold": 2, + "displayMode": "chat", + "enableAutoCreateTopic": true, + "historyCount": 1 + } + }, + "group": "default", + "meta": { + "avatar": "📊", + "description": "Business Consultant: Providing comprehensive business support and expertise worldwide.Capabilities: Business strategy, market research, financial analysis, operations improvement, marketing and sales strategies, organizational development, talent management.Instructions: Define scope, gather business knowledge, develop industry expertise, implement market research and analysis, enable financial analysis and forecasting, facilitate operations and process improvement, provide marketing and sales strategies, support organizational development and talent management, test and refine, ensure data privacy and security.", + "tags": ["business-consultant"], + "title": "Business Guru" + }, + "type": "agent", + "createdAt": "2024-01-24T16:42:42.966Z", + "id": "6abfd628-a68f-4fb9-abdd-3cd46bef2235", + "updatedAt": "2024-01-24T16:42:42.966Z", + "model": "gpt-3.5-turbo", + "pinned": false + } + ], + "topics": [ + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "inbox", + "createdAt": 1714230284503, + "id": "9py3gBEl", + "updatedAt": 1714230284503 + }, + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "inbox", + "createdAt": 1714230332854, + "id": "aaWMcwY1", + "updatedAt": 1714230332854 + }, + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "inbox", + "createdAt": 1714236590531, + "id": "2wcF8yaS", + "updatedAt": 1714236590531 + }, + { + "title": "Default Topic", + "favorite": 0, + "sessionId": "a5fefc88-f6c1-44fb-9e98-3d366b1ed589", + "createdAt": 1717080410825, + "id": "v38snJ0A", + "updatedAt": 1717080410825 + } + ] + }, + "version": 7 +} diff --git a/src/services/import/client.test.ts b/src/services/import/client.test.ts new file mode 100644 index 000000000000..b2d6ee074a95 --- /dev/null +++ b/src/services/import/client.test.ts @@ -0,0 +1,998 @@ +// @vitest-environment node +import { eq, inArray } from 'drizzle-orm'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { clientDB, initializeDB } from '@/database/client/db'; +import { + agents, + agentsToSessions, + messages, + sessionGroups, + sessions, + topics, + users, +} from '@/database/schemas'; +import { CURRENT_CONFIG_VERSION } from '@/migrations'; +import { ImportResults, ImporterEntryData } from '@/types/importer'; + +import mockImportData from './__tests__/fixtures/messages.json'; +import { ClientService } from './client'; + +const userId = 'test-user-id'; +const service = new ClientService(userId); + +beforeEach(async () => { + await initializeDB(); + + await clientDB.delete(users); + + // 创建测试数据 + await clientDB.transaction(async (tx) => { + await tx.insert(users).values({ id: userId }); + }); +}); + +describe('ImporterService', () => { + describe('import sessionGroups', () => { + it('should import session groups and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessionGroups: [ + { id: 'group1', name: 'Group 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'group2', name: 'Group 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessionGroups!.added).toBe(2); + expect(result!.sessionGroups!.skips).toBe(0); + expect(result!.sessionGroups!.errors).toBe(0); + + const groups = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + expect(groups).toHaveLength(2); + }); + + it('should skip existing session groups and return correct result', async () => { + await clientDB + .insert(sessionGroups) + .values({ clientId: 'group1', name: 'Existing Group', userId }) + .execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessionGroups: [ + { id: 'group1', name: 'Group 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'group2', name: 'Group 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessionGroups!.added).toBe(1); + expect(result!.sessionGroups!.skips).toBe(1); + expect(result!.sessionGroups!.errors).toBe(0); + }); + }); + + describe('import sessions', () => { + it('should import sessions and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessions!.added).toBe(2); + expect(result!.sessions!.skips).toBe(0); + expect(result!.sessions!.errors).toBe(0); + + const importedSessions = await clientDB.query.sessions.findMany({ + where: eq(sessions.userId, userId), + }); + expect(importedSessions).toHaveLength(2); + + const agentCount = await clientDB.query.agents.findMany({ + where: eq(agents.userId, userId), + }); + + expect(agentCount.length).toBe(2); + + const agentSessionCount = await clientDB.query.agentsToSessions.findMany(); + expect(agentSessionCount.length).toBe(2); + }); + + it('should skip existing sessions and return correct result', async () => { + await clientDB.insert(sessions).values({ clientId: 'session1', userId }).execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessions!.added).toBe(1); + expect(result!.sessions!.skips).toBe(1); + expect(result!.sessions!.errors).toBe(0); + }); + + it('should associate imported sessions with session groups', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessionGroups: [ + { id: 'group1', name: 'Group 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'group2', name: 'Group 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + group: 'group1', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + group: 'group2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + { + id: 'session3', + group: 'group4', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 3', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.sessionGroups!.added).toBe(2); + expect(result!.sessionGroups!.skips).toBe(0); + + expect(result!.sessions!.added).toBe(3); + expect(result!.sessions!.skips).toBe(0); + + // session 1 should be associated with group 1 + const session1 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session1'), + with: { group: true }, + }); + expect(session1?.group).toBeDefined(); + + // session 3 should not have group + const session3 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session3'), + with: { group: true }, + }); + expect(session3?.group).toBeNull(); + }); + + it('should create agents and associate them with imported sessions', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 1', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'def', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 2', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + await service.importData(data); + + // 验证是否为每个 session 创建了对应的 agent + const agentCount = await clientDB.query.agents.findMany({ + where: eq(agents.userId, userId), + }); + expect(agentCount).toHaveLength(2); + + // 验证 agent 的属性是否正确设置 + const agent1 = await clientDB.query.agents.findFirst({ + where: eq(agents.systemRole, 'Test Agent 1'), + }); + expect(agent1?.model).toBe('abc'); + + const agent2 = await clientDB.query.agents.findFirst({ + where: eq(agents.systemRole, 'Test Agent 2'), + }); + expect(agent2?.model).toBe('def'); + + // 验证 agentsToSessions 关联是否正确建立 + const session1 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session1'), + }); + const session1Agent = await clientDB.query.agentsToSessions.findFirst({ + where: eq(agentsToSessions.sessionId, session1?.id!), + with: { agent: true }, + }); + + expect((session1Agent?.agent as any).systemRole).toBe('Test Agent 1'); + + const session2 = await clientDB.query.sessions.findFirst({ + where: eq(sessions.clientId, 'session2'), + }); + const session2Agent = await clientDB.query.agentsToSessions.findFirst({ + where: eq(agentsToSessions.sessionId, session2?.id!), + with: { agent: true }, + }); + + expect((session2Agent?.agent as any).systemRole).toBe('Test Agent 2'); + }); + + it('should not create duplicate agents for existing sessions', async () => { + // 先导入一些 sessions + await service.importData({ + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 1', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + version: CURRENT_CONFIG_VERSION, + }); + + // 再次导入相同的 sessions + await service.importData({ + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'Test Agent 1', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + version: CURRENT_CONFIG_VERSION, + }); + + // 验证只创建了一个 agent + const agentCount = await clientDB.query.agents.findMany({ + where: eq(agents.userId, userId), + }); + expect(agentCount).toHaveLength(1); + }); + }); + + describe('import topics', () => { + it('should import topics and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + { + id: 'topic2', + title: 'Topic 2', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session2', + }, + ], + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + { + id: 'session2', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 2', + }, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.topics!.added).toBe(2); + expect(result!.topics!.skips).toBe(0); + expect(result!.topics!.errors).toBe(0); + + const importedTopics = await clientDB.query.topics.findMany({ + where: eq(topics.userId, userId), + }); + expect(importedTopics).toHaveLength(2); + }); + + it('should skip existing topics and return correct result', async () => { + await clientDB + .insert(topics) + .values({ clientId: 'topic1', title: 'Existing Topic', userId }) + .execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + topics: [ + { id: 'topic1', title: 'Topic 1', createdAt: 1715186011586, updatedAt: 1715186015053 }, + { id: 'topic2', title: 'Topic 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.topics!.added).toBe(1); + expect(result!.topics!.skips).toBe(1); + expect(result!.topics!.errors).toBe(0); + }); + + it('should associate imported topics with sessions', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + { id: 'topic2', title: 'Topic 2', createdAt: 1715186011586, updatedAt: 1715186015053 }, + ], + }; + + await service.importData(data); + + // topic1 should be associated with session1 + const [topic1] = await clientDB + .select({ sessionClientId: sessions.clientId }) + .from(topics) + .where(eq(topics.clientId, 'topic1')) + .leftJoin(sessions, eq(topics.sessionId, sessions.id)); + + expect(topic1?.sessionClientId).toBe('session1'); + + // topic2 should not have session + const topic2 = await clientDB.query.topics.findFirst({ + where: eq(topics.clientId, 'topic2'), + with: { session: true }, + }); + expect(topic2?.session).toBeNull(); + }); + }); + + describe('import messages', () => { + it('should import messages and return correct result', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + parentId: 'msg1', + }, + ], + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.messages!.added).toBe(2); + expect(result!.messages!.skips).toBe(0); + expect(result!.messages!.errors).toBe(0); + + const importedMessages = await clientDB.query.messages.findMany({ + where: eq(messages.userId, userId), + }); + expect(importedMessages).toHaveLength(2); + }); + + it('should skip existing messages and return correct result', async () => { + await clientDB + .insert(messages) + .values({ + clientId: 'msg1', + content: 'Existing Message', + role: 'user', + userId, + }) + .execute(); + + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + ], + }; + + let result: ImportResults; + await service.importData(data, { + onSuccess: (res) => { + result = res; + }, + }); + + expect(result!.messages!.added).toBe(1); + expect(result!.messages!.skips).toBe(1); + expect(result!.messages!.errors).toBe(0); + }); + + it('should associate imported messages with sessions and topics', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + sessions: [ + { + id: 'session1', + createdAt: '2022-05-14T18:18:10.494Z', + updatedAt: '2023-01-01', + type: 'agent', + config: { + model: 'abc', + chatConfig: {} as any, + params: {}, + systemRole: 'abc', + tts: {} as any, + }, + meta: { + title: 'Session 1', + }, + }, + ], + topics: [ + { + id: 'topic1', + title: 'Topic 1', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + }, + ], + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + sessionId: 'session1', + topicId: 'topic1', + parentId: 'msg1', + }, + { + id: 'msg3', + content: 'Message 3', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + ], + }; + + await service.importData(data); + + // msg1 and msg2 should be associated with session1 and topic1 + const [msg1, msg2] = await clientDB.query.messages.findMany({ + where: inArray(messages.clientId, ['msg1', 'msg2']), + with: { + session: true, + topic: true, + }, + }); + + expect(msg1.session?.clientId).toBe('session1'); + expect(msg1.topic?.clientId).toBe('topic1'); + expect(msg2.session?.clientId).toBe('session1'); + expect(msg2.topic?.clientId).toBe('topic1'); + + // msg3 should not have session and topic + const msg3 = await clientDB.query.messages.findFirst({ + where: eq(messages.clientId, 'msg3'), + with: { + session: true, + topic: true, + }, + }); + expect(msg3?.session).toBeNull(); + expect(msg3?.topic).toBeNull(); + }); + + it('should set parentId for messages', async () => { + const data: ImporterEntryData = { + version: CURRENT_CONFIG_VERSION, + messages: [ + { + id: 'msg1', + content: 'Message 1', + role: 'user', + createdAt: 1715186011586, + updatedAt: 1715186015053, + }, + { + id: 'msg2', + content: 'Message 2', + role: 'assistant', + createdAt: 1715186011586, + updatedAt: 1715186015053, + parentId: 'msg1', + }, + ], + }; + + await service.importData(data); + + const msg2 = await clientDB.query.messages.findFirst({ + where: eq(messages.clientId, 'msg2'), + with: { parent: true }, + }); + + expect(msg2?.parent?.clientId).toBe('msg1'); + }); + + it('should import parentId Success', () => {}); + }); + + describe('real world examples', () => { + it('should import successfully', async () => { + let result: ImportResults; + + await service.importData( + { + messages: [ + { + role: 'user', + content: 'hello', + files: [], + sessionId: 'inbox', + topicId: '2wcF8yaS', + createdAt: 1714236590340, + id: 'DCG1G1EH', + updatedAt: 1714236590340, + extra: {}, + }, + { + role: 'assistant', + content: '...', + parentId: 'DCG1G1EH', + sessionId: 'inbox', + topicId: '2wcF8yaS', + createdAt: 1714236590441, + id: 'gY41w5vQ', + updatedAt: 1714236590518, + error: { + body: { + error: { + message: "model 'mixtral' not found, try pulling it first", + name: 'ResponseError', + status_code: 404, + }, + provider: 'ollama', + }, + message: + 'Error requesting Ollama service, please troubleshoot or retry based on the following information', + type: 'OllamaBizError', + }, + extra: { fromModel: 'mixtral', fromProvider: 'ollama' }, + }, + { + role: 'user', + content: 'hello', + files: [], + sessionId: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + topicId: 'v38snJ0A', + createdAt: 1717080410895, + id: 'qOIxEGEB', + updatedAt: 1717080410895, + extra: {}, + }, + { + role: 'assistant', + content: '...', + parentId: 'qOIxEGEB', + sessionId: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + topicId: 'v38snJ0A', + createdAt: 1717080410970, + id: 'w28FcqY5', + updatedAt: 1717080411485, + error: { + body: { error: { errorType: 'NoOpenAIAPIKey' }, provider: 'openai' }, + message: 'OpenAI API Key is empty, please add a custom OpenAI API Key', + type: 'NoOpenAIAPIKey', + }, + extra: { fromModel: 'gpt-3.5-turbo', fromProvider: 'openai' }, + }, + ], + sessionGroups: [ + { + name: 'Writter', + sort: 0, + createdAt: 1706114744425, + id: 'XlUbvOvL', + updatedAt: 1706114747468, + }, + ], + sessions: [ + { + config: { + model: 'gpt-3.5-turbo', + params: { + frequency_penalty: 0, + presence_penalty: 0, + temperature: 0.6, + top_p: 1, + }, + plugins: [], + systemRole: + "You are a LobeChat technical operator 🍐🐊. You now need to write a developer's guide for LobeChat as a guide for them to develop LobeChat. This guide will include several sections, and you need to output the corresponding document content based on the user's input.\n\nHere is the technical introduction of LobeChat\n\n LobeChat is an AI conversation application built with the Next.js framework. It uses a series of technology stacks to implement various functions and features.\n\n\n ## Basic Technology Stack\n\n The core technology stack of LobeChat is as follows:\n\n - **Framework**: We chose [Next.js](https://nextjs.org/), a powerful React framework that provides key features such as server-side rendering, routing framework, and Router Handler for our project.\n - **Component Library**: We use [Ant Design (antd)](https://ant.design/) as the basic component library, and introduce [lobe-ui](https://github.com/lobehub/lobe-ui) as our business component library.\n - **State Management**: We use [zustand](https://github.com/pmndrs/zustand), a lightweight and easy-to-use state management library.\n - **Network Request**: We adopt [swr](https://swr.vercel.app/), a React Hooks library for data fetching.\n - **Routing**: We directly use the routing solution provided by [Next.js](https://nextjs.org/) itself.\n - **Internationalization**: We use [i18next](https://www.i18next.com/) to implement multi-language support for the application.\n - **Styling**: We use [antd-style](https://github.com/ant-design/antd-style), a CSS-in-JS library that is compatible with Ant Design.\n - **Unit Testing**: We use [vitest](https://github.com/vitejs/vitest) for unit testing.\n\n ## Folder Directory Structure\n\n The folder directory structure of LobeChat is as follows:\n\n \\`\\`\\`bash\n src\n ├── app # Main logic and state management related code of the application\n ├── components # Reusable UI components\n ├── config # Application configuration files, including client environment variables and server environment variables\n ├── const # Used to define constants, such as action types, route names, etc.\n ├── features # Function modules related to business functions, such as Agent settings, plugin development pop-ups, etc.\n ├── hooks # Custom utility Hooks reused throughout the application\n ├── layout # Layout components of the application, such as navigation bar, sidebar, etc.\n ├── locales # Language files for internationalization\n ├── services # Encapsulated backend service interfaces, such as HTTP requests\n ├── store # Zustand store for state management\n ├── types # TypeScript type definition files\n └── utils # Common utility functions\n \\`\\`\\`\n", + tts: { + showAllLocaleVoice: false, + sttLocale: 'auto', + ttsService: 'openai', + voice: { openai: 'alloy' }, + }, + chatConfig: { + autoCreateTopicThreshold: 2, + displayMode: 'chat', + enableAutoCreateTopic: true, + historyCount: 1, + }, + }, + group: 'XlUbvOvL', + meta: { + avatar: '📝', + description: + 'LobeChat is an AI conversation application built with the Next.js framework. I will help you write the development documentation for LobeChat.', + tags: [ + 'Development Documentation', + 'Technical Introduction', + 'next-js', + 'react', + 'lobe-chat', + ], + title: 'LobeChat Technical Documentation Expert', + }, + type: 'agent', + createdAt: '2024-01-24T16:43:12.164Z', + id: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + updatedAt: '2024-01-24T16:46:15.226Z', + pinned: false, + }, + ], + topics: [ + { + title: 'Default Topic', + sessionId: 'inbox', + createdAt: 1714236590531, + id: '2wcF8yaS', + updatedAt: 1714236590531, + }, + { + title: 'Default Topic', + sessionId: 'a5fefc88-f6c1-44fb-9e98-3d366b1ed589', + createdAt: 1717080410825, + id: 'v38snJ0A', + updatedAt: 1717080410825, + }, + ], + version: mockImportData.version, + }, + { onSuccess: (res) => (result = res) }, + ); + + expect(result!).toEqual({ + sessionGroups: { added: 1, errors: 0, skips: 0 }, + sessions: { added: 1, errors: 0, skips: 0 }, + topics: { added: 2, errors: 0, skips: 0 }, + messages: { added: 4, errors: 0, skips: 0 }, + }); + }); + + it('should import real world data', async () => { + let result: ImportResults; + + await service.importData( + { ...(mockImportData.state as any), version: mockImportData.version }, + { + onSuccess: (res) => { + result = res; + }, + }, + ); + + expect(result!).toEqual({ + sessionGroups: { added: 2, errors: 0, skips: 0 }, + sessions: { added: 15, errors: 0, skips: 0 }, + topics: { added: 4, errors: 0, skips: 0 }, + messages: { added: 32, errors: 0, skips: 0 }, + }); + }); + }); +}); diff --git a/src/services/import/client.ts b/src/services/import/client.ts index a148abd28d57..0dceca6f8fef 100644 --- a/src/services/import/client.ts +++ b/src/services/import/client.ts @@ -1,74 +1,34 @@ -import { MessageModel } from '@/database/_deprecated/models/message'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; -import { TopicModel } from '@/database/_deprecated/models/topic'; -import { ImportResult, ImportResults } from '@/services/config'; +import { clientDB } from '@/database/client/db'; +import { DataImporterRepos } from '@/database/repositories/dataImporter'; +import { BaseClientService } from '@/services/baseClientService'; import { useUserStore } from '@/store/user'; import { ImportStage, ImporterEntryData, OnImportCallbacks } from '@/types/importer'; import { UserSettings } from '@/types/user/settings'; -export class ClientService { +export class ClientService extends BaseClientService { + private get dataImporter(): DataImporterRepos { + return new DataImporterRepos(clientDB as any, this.userId); + } + importSettings = async (settings: UserSettings) => { await useUserStore.getState().importAppSettings(settings); }; - importData = async ( - config: ImporterEntryData, - callbacks?: OnImportCallbacks, - ): Promise => { + importData = async (data: ImporterEntryData, callbacks?: OnImportCallbacks) => { callbacks?.onStageChange?.(ImportStage.Importing); const time = Date.now(); - - const { messages = [], sessionGroups = [], sessions = [], topics = [] } = config; - - let messageResult: ImportResult | undefined; - let sessionResult: ImportResult | undefined; - let sessionGroupResult: ImportResult | undefined; - let topicResult: ImportResult | undefined; - - if (messages.length > 0) { - const res = await MessageModel.batchCreate(messages as any); - messageResult = this.mapImportResult(res); - } - - if (sessionGroups.length > 0) { - const res = await SessionGroupModel.batchCreate(sessionGroups as any); - sessionGroupResult = this.mapImportResult(res); - } - - if (topics.length > 0) { - const res = await TopicModel.batchCreate(topics as any); - topicResult = this.mapImportResult(res); + try { + const result = await this.dataImporter.importData(data); + const duration = Date.now() - time; + + callbacks?.onStageChange?.(ImportStage.Success); + callbacks?.onSuccess?.(result, duration); + } catch (e) { + console.error(e); + callbacks?.onStageChange?.(ImportStage.Error); + const error = e as Error; + + callbacks?.onError?.({ code: 'ImportError', httpStatus: 0, message: error.message }); } - - if (sessions.length > 0) { - const data = await SessionModel.batchCreate(sessions as any); - sessionResult = this.mapImportResult(data); - } - - const result = { - messages: messageResult, - sessionGroups: sessionGroupResult, - sessions: sessionResult, - topics: topicResult, - }; - - const duration = Date.now() - time; - callbacks?.onStageChange?.(ImportStage.Success); - callbacks?.onSuccess?.(result, duration); - - return result; - }; - - private mapImportResult = (input: { - added: number; - errors?: Error[]; - skips: string[]; - }): ImportResult => { - return { - added: input.added, - errors: input.errors?.length || 0, - skips: input.skips.length, - }; }; } diff --git a/src/services/message/client.test.ts b/src/services/message/client.test.ts index 867fda6c5b72..2372330aabf2 100644 --- a/src/services/message/client.test.ts +++ b/src/services/message/client.test.ts @@ -1,133 +1,155 @@ import dayjs from 'dayjs'; -import { Mock, describe, expect, it, vi } from 'vitest'; +import { and, eq } from 'drizzle-orm'; +import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CreateMessageParams, MessageModel } from '@/database/_deprecated/models/message'; +import { MessageModel } from '@/database/_deprecated/models/message'; +import { clientDB, initializeDB } from '@/database/client/db'; +import { + MessageItem, + files, + messagePlugins, + messageTTS, + messageTranslates, + messages, + sessions, + topics, + users, +} from '@/database/schemas'; import { ChatMessage, ChatMessageError, - ChatPluginPayload, ChatTTS, ChatTranslate, + CreateMessageParams, } from '@/types/message'; import { ClientService } from './client'; -const messageService = new ClientService(); - -// Mock the MessageModel -vi.mock('@/database/_deprecated/models/message', () => { - return { - MessageModel: { - create: vi.fn(), - batchCreate: vi.fn(), - count: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - bulkDelete: vi.fn(), - queryBySessionId: vi.fn(), - update: vi.fn(), - updatePlugin: vi.fn(), - batchDelete: vi.fn(), - clearTable: vi.fn(), - batchUpdate: vi.fn(), - queryAll: vi.fn(), - updatePluginState: vi.fn(), - }, - }; +const userId = 'message-db'; +const sessionId = '1'; +const topicId = 'topic-id'; + +// Mock data +const mockMessageId = 'mock-message-id'; +const mockMessage = { + id: mockMessageId, + content: 'Mock message content', + sessionId, + role: 'user', +} as ChatMessage; + +const mockMessages = [mockMessage]; + +beforeEach(async () => { + await initializeDB(); + + // 在每个测试用例之前,清空表 + await clientDB.transaction(async (trx) => { + await trx.delete(users); + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + + await trx.insert(sessions).values([{ id: sessionId, userId }]); + await trx.insert(topics).values([{ id: topicId, sessionId, userId }]); + await trx.insert(files).values({ + id: 'f1', + userId: userId, + url: 'abc', + name: 'file-1', + fileType: 'image/png', + size: 1000, + }); + }); }); -describe('MessageClientService', () => { - // Mock data - const mockMessageId = 'mock-message-id'; - const mockMessage = { - id: mockMessageId, - content: 'Mock message content', - sessionId: 'mock-session-id', - createdAt: 100, - updatedAt: 100, - role: 'user', - // ... other properties - } as ChatMessage; - const mockMessages = [mockMessage]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); +afterEach(async () => { + // 在每个测试用例之后,清空表 + await clientDB.delete(users); +}); + +const messageService = new ClientService(userId); +describe('MessageClientService', () => { describe('create', () => { it('should create a message and return its id', async () => { // Setup - const createParams = { + const createParams: CreateMessageParams = { content: 'New message content', - sessionId: '1', - // ... other properties - } as CreateMessageParams; - (MessageModel.create as Mock).mockResolvedValue({ id: mockMessageId }); + sessionId, + role: 'user', + }; // Execute const messageId = await messageService.createMessage(createParams); // Assert - expect(MessageModel.create).toHaveBeenCalledWith(createParams); - expect(messageId).toBe(mockMessageId); + expect(messageId).toMatch(/^msg_/); }); }); describe('batchCreate', () => { it('should batch create messages', async () => { - // Setup - (MessageModel.batchCreate as Mock).mockResolvedValue(mockMessages); - // Execute - const result = await messageService.batchCreateMessages(mockMessages); + await messageService.batchCreateMessages([ + { + content: 'Mock message content', + sessionId, + role: 'user', + }, + { + content: 'Mock message content', + sessionId, + role: 'user', + }, + ] as MessageItem[]); + const count = await clientDB.$count(messages); // Assert - expect(MessageModel.batchCreate).toHaveBeenCalledWith(mockMessages); - expect(result).toBe(mockMessages); + expect(count).toBe(2); }); }); describe('removeMessage', () => { it('should remove a message by id', async () => { - // Setup - (MessageModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await messageService.removeMessage(mockMessageId); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await messageService.removeMessage(mockMessageId); // Assert - expect(MessageModel.delete).toHaveBeenCalledWith(mockMessageId); - expect(result).toBe(true); + const count = await clientDB.$count(messages); + + expect(count).toBe(0); }); }); describe('removeMessages', () => { it('should remove a message by id', async () => { // Setup - (MessageModel.bulkDelete as Mock).mockResolvedValue(true); + await clientDB.insert(messages).values([ + { id: mockMessageId, role: 'user', userId }, + { role: 'assistant', userId }, + ]); // Execute - const result = await messageService.removeMessages([mockMessageId]); + await messageService.removeMessages([mockMessageId]); // Assert - expect(MessageModel.bulkDelete).toHaveBeenCalledWith([mockMessageId]); - expect(result).toBe(true); + const count = await clientDB.$count(messages); + + expect(count).toBe(1); }); }); describe('getMessages', () => { it('should retrieve messages by sessionId and topicId', async () => { // Setup - const sessionId = 'session-id'; - const topicId = 'topic-id'; - (MessageModel.query as Mock).mockResolvedValue(mockMessages); + await clientDB + .insert(messages) + .values({ id: mockMessageId, sessionId, topicId, role: 'user', userId }); // Execute - const messages = await messageService.getMessages(sessionId, topicId); + const data = await messageService.getMessages(sessionId, topicId); // Assert - expect(MessageModel.query).toHaveBeenCalledWith({ sessionId, topicId }); - expect(messages).toEqual(mockMessages.map((i) => ({ ...i, imageList: [] }))); + expect(data[0]).toMatchObject({ id: mockMessageId, role: 'user' }); }); }); @@ -135,14 +157,21 @@ describe('MessageClientService', () => { it('should retrieve all messages in a session', async () => { // Setup const sessionId = 'session-id'; - (MessageModel.queryBySessionId as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(sessions).values([ + { id: 'bbb', userId }, + { id: sessionId, userId }, + ]); + await clientDB.insert(messages).values([ + { sessionId, topicId, role: 'user', userId }, + { sessionId, topicId, role: 'assistant', userId }, + { sessionId: 'bbb', topicId, role: 'assistant', userId }, + ]); // Execute - const messages = await messageService.getAllMessagesInSession(sessionId); + const data = await messageService.getAllMessagesInSession(sessionId); // Assert - expect(MessageModel.queryBySessionId).toHaveBeenCalledWith(sessionId); - expect(messages).toBe(mockMessages); + expect(data.length).toBe(2); }); }); @@ -150,77 +179,85 @@ describe('MessageClientService', () => { it('should batch remove messages by assistantId and topicId', async () => { // Setup const assistantId = 'assistant-id'; - const topicId = 'topic-id'; - (MessageModel.batchDelete as Mock).mockResolvedValue(true); + const sessionId = 'session-id'; + await clientDB.insert(sessions).values([ + { id: 'bbb', userId }, + { id: sessionId, userId }, + ]); + await clientDB.insert(messages).values([ + { sessionId, topicId, role: 'user', userId }, + { sessionId, topicId, role: 'assistant', userId }, + { sessionId: 'bbb', topicId, role: 'assistant', userId }, + ]); // Execute - const result = await messageService.removeMessagesByAssistant(assistantId, topicId); + await messageService.removeMessagesByAssistant(sessionId, topicId); // Assert - expect(MessageModel.batchDelete).toHaveBeenCalledWith(assistantId, topicId); - expect(result).toBe(true); + const result = await clientDB.query.messages.findMany({ + where: and(eq(messages.sessionId, sessionId), eq(messages.topicId, topicId)), + }); + + expect(result.length).toBe(0); }); }); describe('clearAllMessage', () => { it('should clear all messages from the table', async () => { // Setup - (MessageModel.clearTable as Mock).mockResolvedValue(true); + await clientDB.insert(users).values({ id: 'another' }); + await clientDB.insert(messages).values([ + { id: mockMessageId, role: 'user', userId }, + { role: 'user', userId: 'another' }, + ]); // Execute - const result = await messageService.removeAllMessages(); + await messageService.removeAllMessages(); // Assert - expect(MessageModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); - - describe('bindMessagesToTopic', () => { - it('should batch update messages to bind them to a topic', async () => { - // Setup - const topicId = 'topic-id'; - const messageIds = [mockMessageId]; - (MessageModel.batchUpdate as Mock).mockResolvedValue(mockMessages); - - // Execute - const result = await messageService.bindMessagesToTopic(topicId, messageIds); - - // Assert - expect(MessageModel.batchUpdate).toHaveBeenCalledWith(messageIds, { topicId }); - expect(result).toBe(mockMessages); + const result = await clientDB.query.messages.findMany({ + where: eq(messages.userId, userId), + }); + expect(result.length).toBe(0); }); }); describe('getAllMessages', () => { it('should retrieve all messages', async () => { - // Setup - (MessageModel.queryAll as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(messages).values([ + { sessionId, topicId, content: '1', role: 'user', userId }, + { sessionId, topicId, content: '2', role: 'assistant', userId }, + ]); // Execute - const messages = await messageService.getAllMessages(); + const data = await messageService.getAllMessages(); // Assert - expect(MessageModel.queryAll).toHaveBeenCalled(); - expect(messages).toBe(mockMessages); + expect(data).toMatchObject([ + { sessionId, topicId, content: '1', role: 'user', userId }, + { sessionId, topicId, content: '2', role: 'assistant', userId }, + ]); }); }); describe('updateMessageError', () => { it('should update the error field of a message', async () => { // Setup + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); const newError = { type: 'InvalidProviderAPIKey', message: 'Error occurred', } as ChatMessageError; - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, error: newError }); // Execute - const result = await messageService.updateMessageError(mockMessageId, newError); + await messageService.updateMessageError(mockMessageId, newError); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { error: newError }); - expect(result).toEqual({ ...mockMessage, error: newError }); + const result = await clientDB.query.messages.findFirst({ + where: eq(messages.id, mockMessageId), + }); + + expect(result!.error).toEqual(newError); }); }); @@ -248,88 +285,85 @@ describe('MessageClientService', () => { describe('updateMessagePluginState', () => { it('should update the plugin state of a message', async () => { // Setup + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const key = 'stateKey'; const value = 'stateValue'; const newPluginState = { [key]: value }; - (MessageModel.updatePluginState as Mock).mockResolvedValue({ - ...mockMessage, - pluginState: newPluginState, - }); // Execute - const result = await messageService.updateMessagePluginState(mockMessageId, { key: value }); + await messageService.updateMessagePluginState(mockMessageId, { stateKey: value }); // Assert - expect(MessageModel.updatePluginState).toHaveBeenCalledWith(mockMessageId, { key: value }); - expect(result).toEqual({ ...mockMessage, pluginState: newPluginState }); + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messagePlugins.id, mockMessageId), + }); + expect(result!.state).toEqual(newPluginState); }); }); describe('updateMessagePluginArguments', () => { it('should update the plugin arguments object of a message', async () => { // Setup - const key = 'stateKey'; + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const value = 'stateValue'; - (MessageModel.updatePlugin as Mock).mockResolvedValue({}); // Execute await messageService.updateMessagePluginArguments(mockMessageId, { key: value }); // Assert - expect(MessageModel.updatePlugin).toHaveBeenCalledWith(mockMessageId, { - arguments: '{"key":"stateValue"}', + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messageTTS.id, mockMessageId), }); + expect(result).toMatchObject({ arguments: '{"key":"stateValue"}' }); }); it('should update the plugin arguments string of a message', async () => { // Setup - const key = 'stateKey'; + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const value = 'stateValue'; - (MessageModel.updatePlugin as Mock).mockResolvedValue({}); - // Execute await messageService.updateMessagePluginArguments( mockMessageId, - JSON.stringify({ key: value }), + JSON.stringify({ abc: value }), ); // Assert - expect(MessageModel.updatePlugin).toHaveBeenCalledWith(mockMessageId, { - arguments: '{"key":"stateValue"}', + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messageTTS.id, mockMessageId), }); + expect(result).toMatchObject({ arguments: '{"abc":"stateValue"}' }); }); }); describe('countMessages', () => { it('should count the total number of messages', async () => { // Setup - const mockCount = 10; - (MessageModel.count as Mock).mockResolvedValue(mockCount); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); // Execute const count = await messageService.countMessages(); // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(count).toBe(mockCount); + expect(count).toBe(1); }); }); describe('countTodayMessages', () => { it('should count the number of messages created today', async () => { // Setup - const today = dayjs().format('YYYY-MM-DD'); const mockMessages = [ - { ...mockMessage, createdAt: today }, - { ...mockMessage, createdAt: today }, - { ...mockMessage, createdAt: '2023-01-01' }, + { ...mockMessage, id: undefined, createdAt: new Date(), userId }, + { ...mockMessage, id: undefined, createdAt: new Date(), userId }, + { ...mockMessage, id: undefined, createdAt: new Date('2023-01-01'), userId }, ]; - (MessageModel.queryAll as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(messages).values(mockMessages); // Execute const count = await messageService.countTodayMessages(); // Assert - expect(MessageModel.queryAll).toHaveBeenCalled(); expect(count).toBe(2); }); }); @@ -337,45 +371,46 @@ describe('MessageClientService', () => { describe('updateMessageTTS', () => { it('should update the TTS field of a message', async () => { // Setup - const newTTS: ChatTTS = { - contentMd5: 'abc', - file: 'file-abc', - }; - - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, tts: newTTS }); + await clientDB + .insert(files) + .values({ id: 'file-abc', fileType: 'text', name: 'abc', url: 'abc', size: 100, userId }); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + const newTTS: ChatTTS = { contentMd5: 'abc', file: 'file-abc' }; // Execute - const result = await messageService.updateMessageTTS(mockMessageId, newTTS); + await messageService.updateMessageTTS(mockMessageId, newTTS); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { tts: newTTS }); - expect(result).toEqual({ ...mockMessage, tts: newTTS }); + const result = await clientDB.query.messageTTS.findFirst({ + where: eq(messageTTS.id, mockMessageId), + }); + + expect(result).toMatchObject({ contentMd5: 'abc', fileId: 'file-abc', id: mockMessageId }); }); }); describe('updateMessageTranslate', () => { it('should update the translate field of a message', async () => { // Setup - const newTranslate: ChatTranslate = { - content: 'Translated text', - to: 'es', - }; - - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, translate: newTranslate }); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + const newTranslate: ChatTranslate = { content: 'Translated text', to: 'es' }; // Execute - const result = await messageService.updateMessageTranslate(mockMessageId, newTranslate); + await messageService.updateMessageTranslate(mockMessageId, newTranslate); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { translate: newTranslate }); - expect(result).toEqual({ ...mockMessage, translate: newTranslate }); + const result = await clientDB.query.messageTranslates.findFirst({ + where: eq(messageTranslates.id, mockMessageId), + }); + + expect(result).toMatchObject(newTranslate); }); }); describe('hasMessages', () => { it('should return true if there are messages', async () => { // Setup - (MessageModel.count as Mock).mockResolvedValue(1); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); // Execute const result = await messageService.hasMessages(); @@ -385,9 +420,6 @@ describe('MessageClientService', () => { }); it('should return false if there are no messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(0); - // Execute const result = await messageService.hasMessages(); diff --git a/src/services/message/client.ts b/src/services/message/client.ts index d51d6d72a1a4..f0ed468a7e67 100644 --- a/src/services/message/client.ts +++ b/src/services/message/client.ts @@ -1,10 +1,10 @@ import dayjs from 'dayjs'; -import { FileModel } from '@/database/_deprecated/models/file'; -import { MessageModel } from '@/database/_deprecated/models/message'; -import { DB_Message } from '@/database/_deprecated/schemas/message'; +import { clientDB } from '@/database/client/db'; +import { MessageItem } from '@/database/schemas'; +import { MessageModel } from '@/database/server/models/message'; +import { BaseClientService } from '@/services/baseClientService'; import { - ChatFileItem, ChatMessage, ChatMessageError, ChatTTS, @@ -14,102 +14,84 @@ import { import { IMessageService } from './type'; -export class ClientService implements IMessageService { +export class ClientService extends BaseClientService implements IMessageService { + private get messageModel(): MessageModel { + return new MessageModel(clientDB as any, this.userId); + } + async createMessage(data: CreateMessageParams) { - const { id } = await MessageModel.create(data); + const { id } = await this.messageModel.create(data); return id; } - async batchCreateMessages(messages: ChatMessage[]) { - return MessageModel.batchCreate(messages); + async batchCreateMessages(messages: MessageItem[]) { + return this.messageModel.batchCreate(messages); } async getMessages(sessionId: string, topicId?: string): Promise { - const messages = await MessageModel.query({ sessionId, topicId }); - - const fileList = (await Promise.all( - messages - .flatMap((item) => item.files) - .filter(Boolean) - .map(async (id) => FileModel.findById(id!)), - )) as ChatFileItem[]; - - return messages.map((item) => ({ - ...item, - imageList: fileList - .filter((file) => item.files?.includes(file.id) && file.fileType.startsWith('image')) - .map((file) => ({ - alt: file.name, - id: file.id, - url: file.url, - })), - })); + return this.messageModel.query({ sessionId, topicId }); } async getAllMessages() { - return MessageModel.queryAll(); + return this.messageModel.queryAll(); } async countMessages() { - return MessageModel.count(); + return this.messageModel.count(); } async countTodayMessages() { - const topics = await MessageModel.queryAll(); + const topics = await this.messageModel.queryAll(); return topics.filter( (item) => dayjs(item.createdAt).format('YYYY-MM-DD') === dayjs().format('YYYY-MM-DD'), ).length; } async getAllMessagesInSession(sessionId: string) { - return MessageModel.queryBySessionId(sessionId); + return this.messageModel.queryBySessionId(sessionId); } async updateMessageError(id: string, error: ChatMessageError) { - return MessageModel.update(id, { error }); + return this.messageModel.update(id, { error }); } - async updateMessage(id: string, message: Partial) { - return MessageModel.update(id, message); + async updateMessage(id: string, message: Partial) { + return this.messageModel.update(id, message); } async updateMessageTTS(id: string, tts: Partial | false) { - return MessageModel.update(id, { tts }); + return this.messageModel.updateTTS(id, tts as any); } async updateMessageTranslate(id: string, translate: Partial | false) { - return MessageModel.update(id, { translate }); + return this.messageModel.updateTranslate(id, translate as any); } async updateMessagePluginState(id: string, value: Record) { - return MessageModel.updatePluginState(id, value); + return this.messageModel.updatePluginState(id, value); } async updateMessagePluginArguments(id: string, value: string | Record) { const args = typeof value === 'string' ? value : JSON.stringify(value); - return MessageModel.updatePlugin(id, { arguments: args }); - } - - async bindMessagesToTopic(topicId: string, messageIds: string[]) { - return MessageModel.batchUpdate(messageIds, { topicId }); + return this.messageModel.updateMessagePlugin(id, { arguments: args }); } async removeMessage(id: string) { - return MessageModel.delete(id); + return this.messageModel.deleteMessage(id); } async removeMessages(ids: string[]) { - return MessageModel.bulkDelete(ids); + return this.messageModel.deleteMessages(ids); } async removeMessagesByAssistant(assistantId: string, topicId?: string) { - return MessageModel.batchDelete(assistantId, topicId); + return this.messageModel.deleteMessagesBySession(assistantId, topicId); } async removeAllMessages() { - return MessageModel.clearTable(); + return this.messageModel.deleteAllMessages(); } async hasMessages() { diff --git a/src/services/message/index.test.ts b/src/services/message/index.test.ts deleted file mode 100644 index 625261f53c90..000000000000 --- a/src/services/message/index.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Mock, describe, expect, it, vi } from 'vitest'; - -import { CreateMessageParams, MessageModel } from '@/database/_deprecated/models/message'; -import { ChatMessage, ChatMessageError, ChatPluginPayload } from '@/types/message'; - -import { messageService } from './index'; - -// Mock the MessageModel -vi.mock('@/database/_deprecated/models/message', () => { - return { - MessageModel: { - count: vi.fn(), - }, - }; -}); - -describe('MessageService', () => { - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); - - describe('hasMessages', () => { - it('should return true if there are messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(1); - - // Execute - const hasMessages = await messageService.hasMessages(); - - // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(hasMessages).toBe(true); - }); - - it('should return false if there are no messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(0); - - // Execute - const hasMessages = await messageService.hasMessages(); - - // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(hasMessages).toBe(false); - }); - }); -}); diff --git a/src/services/message/server.ts b/src/services/message/server.ts index 6562b4ec3e25..6860ede39df9 100644 --- a/src/services/message/server.ts +++ b/src/services/message/server.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { INBOX_SESSION_ID } from '@/const/session'; +import { MessageItem } from '@/database/schemas'; import { lambdaClient } from '@/libs/trpc/client'; import { ChatMessage, @@ -19,7 +20,7 @@ export class ServerService implements IMessageService { }); } - batchCreateMessages(messages: ChatMessage[]): Promise { + batchCreateMessages(messages: MessageItem[]): Promise { return lambdaClient.message.batchCreateMessages.mutate(messages); } @@ -33,6 +34,7 @@ export class ServerService implements IMessageService { getAllMessages(): Promise { return lambdaClient.message.getAllMessages.query(); } + getAllMessagesInSession(sessionId: string): Promise { return lambdaClient.message.getAllMessagesInSession.query({ sessionId: this.toDbSessionId(sessionId), @@ -79,10 +81,6 @@ export class ServerService implements IMessageService { return lambdaClient.message.updatePluginState.mutate({ id, value }); } - bindMessagesToTopic(_topicId: string, _messageIds: string[]): Promise { - throw new Error('Method not implemented.'); - } - removeMessage(id: string): Promise { return lambdaClient.message.removeMessage.mutate({ id }); } diff --git a/src/services/message/type.ts b/src/services/message/type.ts index 2929620ad128..4ac08fd54c17 100644 --- a/src/services/message/type.ts +++ b/src/services/message/type.ts @@ -1,4 +1,4 @@ -import { DB_Message } from '@/database/_deprecated/schemas/message'; +import { MessageItem } from '@/database/schemas'; import { ChatMessage, ChatMessageError, @@ -11,7 +11,7 @@ import { export interface IMessageService { createMessage(data: CreateMessageParams): Promise; - batchCreateMessages(messages: ChatMessage[]): Promise; + batchCreateMessages(messages: MessageItem[]): Promise; getMessages(sessionId: string, topicId?: string): Promise; getAllMessages(): Promise; @@ -20,11 +20,10 @@ export interface IMessageService { countTodayMessages(): Promise; updateMessageError(id: string, error: ChatMessageError): Promise; - updateMessage(id: string, message: Partial): Promise; + updateMessage(id: string, message: Partial): Promise; updateMessageTTS(id: string, tts: Partial | false): Promise; updateMessageTranslate(id: string, translate: Partial | false): Promise; updateMessagePluginState(id: string, value: Record): Promise; - bindMessagesToTopic(topicId: string, messageIds: string[]): Promise; removeMessage(id: string): Promise; removeMessages(ids: string[]): Promise; diff --git a/src/services/plugin/client.test.ts b/src/services/plugin/client.test.ts index e2b6ccc66822..4f4c45df3aca 100644 --- a/src/services/plugin/client.test.ts +++ b/src/services/plugin/client.test.ts @@ -1,30 +1,29 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { PluginModel } from '@/database/_deprecated/models/plugin'; -import { DB_Plugin } from '@/database/_deprecated/schemas/plugin'; +import { clientDB, initializeDB } from '@/database/client/db'; +import { installedPlugins, users } from '@/database/schemas'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { ClientService } from './client'; import { InstallPluginParams } from './type'; -const pluginService = new ClientService(); - // Mocking modules and functions -vi.mock('@/database/_deprecated/models/plugin', () => ({ - PluginModel: { - getList: vi.fn(), - create: vi.fn(), - delete: vi.fn(), - update: vi.fn(), - clear: vi.fn(), - }, -})); - -beforeEach(() => { - vi.resetAllMocks(); +const userId = 'message-db'; +const pluginService = new ClientService(userId); + +// Mock data +beforeEach(async () => { + await initializeDB(); + + // 在每个测试用例之前,重置表数据 + await clientDB.transaction(async (trx) => { + await trx.delete(users); + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + }); }); describe('PluginService', () => { @@ -32,18 +31,19 @@ describe('PluginService', () => { it('should install a plugin', async () => { // Arrange const fakePlugin = { - identifier: 'test-plugin', + identifier: 'test-plugin-d', manifest: { name: 'TestPlugin', version: '1.0.0' } as unknown as LobeChatPluginManifest, type: 'plugin', } as InstallPluginParams; - vi.mocked(PluginModel.create).mockResolvedValue(fakePlugin); // Act - const installedPlugin = await pluginService.installPlugin(fakePlugin); + await pluginService.installPlugin(fakePlugin); // Assert - expect(PluginModel.create).toHaveBeenCalledWith(fakePlugin); - expect(installedPlugin).toEqual(fakePlugin); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, fakePlugin.identifier), + }); + expect(result).toMatchObject(fakePlugin); }); }); @@ -51,14 +51,14 @@ describe('PluginService', () => { it('should return a list of installed plugins', async () => { // Arrange const fakePlugins = [{ identifier: 'test-plugin', type: 'plugin' }] as LobeTool[]; - vi.mocked(PluginModel.getList).mockResolvedValue(fakePlugins as DB_Plugin[]); - + await clientDB + .insert(installedPlugins) + .values([{ identifier: 'test-plugin', type: 'plugin', userId }]); // Act - const installedPlugins = await pluginService.getInstalledPlugins(); + const data = await pluginService.getInstalledPlugins(); // Assert - expect(PluginModel.getList).toHaveBeenCalled(); - expect(installedPlugins).toEqual(fakePlugins); + expect(data).toMatchObject(fakePlugins); }); }); @@ -66,13 +66,15 @@ describe('PluginService', () => { it('should uninstall a plugin', async () => { // Arrange const identifier = 'test-plugin'; - vi.mocked(PluginModel.delete).mockResolvedValue(); + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.uninstallPlugin(identifier); + await pluginService.uninstallPlugin(identifier); // Assert - expect(PluginModel.delete).toHaveBeenCalledWith(identifier); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); expect(result).toBe(undefined); }); }); @@ -81,67 +83,74 @@ describe('PluginService', () => { it('should create a custom plugin', async () => { // Arrange const customPlugin = { - identifier: 'custom-plugin', + identifier: 'custom-plugin-x', manifest: {}, type: 'customPlugin', } as LobeToolCustomPlugin; - vi.mocked(PluginModel.create).mockResolvedValue(customPlugin); // Act - const result = await pluginService.createCustomPlugin(customPlugin); + await pluginService.createCustomPlugin(customPlugin); // Assert - expect(PluginModel.create).toHaveBeenCalledWith({ - ...customPlugin, - type: 'customPlugin', + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, customPlugin.identifier), }); - expect(result).toEqual(customPlugin); + expect(result).toMatchObject(customPlugin); }); }); describe('updatePlugin', () => { it('should update a plugin', async () => { // Arrange - const id = 'plugin-id'; - const value = { settings: { ab: '1' } } as unknown as LobeToolCustomPlugin; - vi.mocked(PluginModel.update).mockResolvedValue(1); + const identifier = 'plugin-id'; + const value = { customParams: { ab: '1' } } as unknown as LobeToolCustomPlugin; + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePlugin(id, value); + await pluginService.updatePlugin(identifier, value); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, value); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); + expect(result).toMatchObject(value); }); }); describe('updatePluginManifest', () => { it('should update a plugin manifest', async () => { // Arrange - const id = 'plugin-id'; + const identifier = 'plugin-id'; const manifest = { name: 'NewPluginManifest' } as unknown as LobeChatPluginManifest; - vi.mocked(PluginModel.update).mockResolvedValue(1); + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePluginManifest(id, manifest); + await pluginService.updatePluginManifest(identifier, manifest); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, { manifest }); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); + expect(result).toMatchObject({ manifest }); }); }); describe('removeAllPlugins', () => { it('should remove all plugins', async () => { // Arrange - vi.mocked(PluginModel.clear).mockResolvedValue(undefined); + await clientDB.insert(installedPlugins).values([ + { identifier: '123', type: 'plugin', userId }, + { identifier: '234', type: 'plugin', userId }, + ]); // Act - const result = await pluginService.removeAllPlugins(); + await pluginService.removeAllPlugins(); // Assert - expect(PluginModel.clear).toHaveBeenCalled(); - expect(result).toBe(undefined); + const result = await clientDB.query.installedPlugins.findMany({ + where: eq(installedPlugins.userId, userId), + }); + expect(result.length).toEqual(0); }); }); @@ -150,13 +159,17 @@ describe('PluginService', () => { // Arrange const id = 'plugin-id'; const settings = { color: 'blue' }; + await clientDB.insert(installedPlugins).values([{ identifier: id, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePluginSettings(id, settings); + await pluginService.updatePluginSettings(id, settings); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, { settings }); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, id), + }); + + expect(result).toMatchObject({ settings }); }); }); }); diff --git a/src/services/plugin/client.ts b/src/services/plugin/client.ts index c56f73119c14..b8cb130566aa 100644 --- a/src/services/plugin/client.ts +++ b/src/services/plugin/client.ts @@ -1,42 +1,51 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { PluginModel } from '@/database/_deprecated/models/plugin'; +import { clientDB } from '@/database/client/db'; +import { PluginModel } from '@/database/server/models/plugin'; +import { BaseClientService } from '@/services/baseClientService'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { IPluginService, InstallPluginParams } from './type'; -export class ClientService implements IPluginService { +export class ClientService extends BaseClientService implements IPluginService { + private get pluginModel(): PluginModel { + return new PluginModel(clientDB as any, this.userId); + } + installPlugin = async (plugin: InstallPluginParams) => { - return PluginModel.create(plugin); + await this.pluginModel.create(plugin); + return; }; getInstalledPlugins = () => { - return PluginModel.getList() as Promise; + return this.pluginModel.query() as Promise; }; - uninstallPlugin(identifier: string) { - return PluginModel.delete(identifier); + async uninstallPlugin(identifier: string) { + await this.pluginModel.delete(identifier); + return; } async createCustomPlugin(customPlugin: LobeToolCustomPlugin) { - return PluginModel.create({ ...customPlugin, type: 'customPlugin' }); + await this.pluginModel.create({ ...customPlugin, type: 'customPlugin' }); + return; } async updatePlugin(id: string, value: LobeToolCustomPlugin) { - await PluginModel.update(id, value); + await this.pluginModel.update(id, value); return; } async updatePluginManifest(id: string, manifest: LobeChatPluginManifest) { - await PluginModel.update(id, { manifest }); + await this.pluginModel.update(id, { manifest }); } async removeAllPlugins() { - return PluginModel.clear(); + await this.pluginModel.deleteAll(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async updatePluginSettings(id: string, settings: any, _?: AbortSignal) { - await PluginModel.update(id, { settings }); + await this.pluginModel.update(id, { settings }); } } diff --git a/src/services/session/client.test.ts b/src/services/session/client.test.ts index 4a7274ce547a..83860a6a804f 100644 --- a/src/services/session/client.test.ts +++ b/src/services/session/client.test.ts @@ -1,193 +1,130 @@ +import { eq, not } from 'drizzle-orm/expressions'; import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; -import { LobeAgentConfig } from '@/types/agent'; +import { INBOX_SESSION_ID } from '@/const/session'; +import { clientDB, initializeDB } from '@/database/client/db'; +import { + NewSession, + SessionItem, + agents, + agentsToSessions, + sessionGroups, + sessions, + users, +} from '@/database/schemas'; +import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent'; import { LobeAgentSession, LobeSessionType, SessionGroups } from '@/types/session'; import { ClientService } from './client'; -const sessionService = new ClientService(); - -// Mock the SessionModel -vi.mock('@/database/_deprecated/models/session', () => { - return { - SessionModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - clearTable: vi.fn(), - update: vi.fn(), - count: vi.fn(), - batchCreate: vi.fn(), - findById: vi.fn(), - isEmpty: vi.fn(), - queryByKeyword: vi.fn(), - updateConfig: vi.fn(), - queryByGroupIds: vi.fn(), - updatePinned: vi.fn(), - duplicate: vi.fn(), - queryWithGroups: vi.fn(), - }, - }; +const userId = 'message-db'; +const sessionService = new ClientService(userId); + +const mockSessionId = 'mock-session-id'; + +// Mock data +beforeEach(async () => { + await initializeDB(); + + // 在每个测试用例之前,清空表 + await clientDB.transaction(async (trx) => { + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + await trx.insert(sessions).values([{ id: mockSessionId, userId: userId }]); + await trx.insert(sessionGroups).values([ + { id: 'group-1', name: 'group-A', sort: 2, userId }, + { id: 'group-2', name: 'group-B', sort: 1, userId }, + { id: 'group-4', name: 'group-C', sort: 1, userId: '456' }, + ]); + }); }); -// Mock the SessionGroupModel -vi.mock('@/database/_deprecated/models/sessionGroup', () => { - return { - SessionGroupModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - clear: vi.fn(), - update: vi.fn(), - batchCreate: vi.fn(), - isEmpty: vi.fn(), - updateOrder: vi.fn(), - queryByKeyword: vi.fn(), - updateConfig: vi.fn(), - queryByGroupIds: vi.fn(), - }, - }; +afterEach(async () => { + // 在每个测试用例之后,清空表 + await clientDB.delete(users); }); describe('SessionService', () => { - const mockSessionId = 'mock-session-id'; const mockSession = { id: mockSessionId, type: 'agent', meta: { title: 'Mock Session' }, } as LobeAgentSession; - const mockSessions = [mockSession]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); describe('createSession', () => { it('should create a new session and return its id', async () => { // Setup const sessionType = LobeSessionType.Agent; const defaultValue = { meta: { title: 'New Session' } } as Partial; - (SessionModel.create as Mock).mockResolvedValue(mockSession); // Execute const sessionId = await sessionService.createSession(sessionType, defaultValue); // Assert - expect(SessionModel.create).toHaveBeenCalledWith(sessionType, defaultValue); - expect(sessionId).toBe(mockSessionId); - }); - - it('should throw an error if session creation fails', async () => { - // Setup - const sessionType = LobeSessionType.Agent; - const defaultValue = { meta: { title: 'New Session' } } as Partial; - (SessionModel.create as Mock).mockResolvedValue(null); - - // Execute & Assert - await expect(sessionService.createSession(sessionType, defaultValue)).rejects.toThrow( - 'session create Error', - ); - }); - }); - - describe('batchCreateSessions', () => { - it('should batch create sessions', async () => { - // Setup - (SessionModel.batchCreate as Mock).mockResolvedValue(mockSessions); - - // Execute - const result = await sessionService.batchCreateSessions(mockSessions); - - // Assert - expect(SessionModel.batchCreate).toHaveBeenCalledWith(mockSessions); - expect(result).toBe(mockSessions); - }); - }); - - describe('getSessionsByType', () => { - it('should retrieve sessions with their group ids', async () => { - // Setup - (SessionModel.query as Mock).mockResolvedValue(mockSessions); - - // Execute - const sessions = await sessionService.getSessionsByType(); - - // Assert - expect(SessionModel.query).toHaveBeenCalled(); - expect(sessions).toBe(mockSessions); - }); - - it('should retrieve all agent sessions', async () => { - // Setup - // Assuming that SessionModel.query has been modified to accept filters - const agentSessions = mockSessions.filter((session) => session.type === 'agent'); - (SessionModel.query as Mock).mockResolvedValue(agentSessions); - - // Execute - const result = await sessionService.getSessionsByType('agent'); - - // Assert - // Assuming that SessionModel.query would be called with a filter for agents - expect(SessionModel.query).toHaveBeenCalled(); // Add filter argument if applicable - expect(result).toBe(agentSessions); + expect(sessionId).toMatch(/^ssn_/); }); }); describe('removeSession', () => { it('should remove a session by its id', async () => { - // Setup - (SessionModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await sessionService.removeSession(mockSessionId); + await sessionService.removeSession(mockSessionId); // Assert - expect(SessionModel.delete).toHaveBeenCalledWith(mockSessionId); - expect(result).toBe(true); + + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + // Assert + expect(result).toBeUndefined(); }); }); describe('removeAllSessions', () => { it('should clear all sessions from the table', async () => { // Setup - (SessionModel.clearTable as Mock).mockResolvedValue(true); + await clientDB + .insert(sessions) + .values([{ userId: userId }, { userId: userId }, { userId: userId }]); // Execute - const result = await sessionService.removeAllSessions(); + await sessionService.removeAllSessions(); // Assert - expect(SessionModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); + const result = await clientDB.query.sessions.findMany({ + where: eq(sessionGroups.userId, userId), + }); + + expect(result.length).toBe(0); }); }); describe('updateSession', () => { - it('should update the group of a session', async () => { + it.skip('should update the group of a session', async () => { // Setup const groupId = 'new-group'; - (SessionModel.update as Mock).mockResolvedValue({ ...mockSession, group: groupId }); // Execute - const result = await sessionService.updateSession(mockSessionId, { group: groupId }); + await sessionService.updateSession(mockSessionId, { group: groupId }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { group: groupId }); - expect(result).toEqual({ ...mockSession, group: groupId }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + expect(result).toMatchObject({ group: groupId }); }); - it('should update the meta of a session', async () => { + it.skip('should update the meta of a session', async () => { // Setup const newMeta = { description: 'Updated description' }; - (SessionModel.update as Mock).mockResolvedValue({ ...mockSession, meta: newMeta }); // Execute - const result = await sessionService.updateSession(mockSessionId, { meta: newMeta }); + await sessionService.updateSession(mockSessionId, { meta: newMeta }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { meta: newMeta }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + expect(result).toEqual({ ...mockSession, meta: newMeta }); }); @@ -199,121 +136,180 @@ describe('SessionService', () => { await sessionService.updateSession(mockSessionId, { pinned }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { pinned: 1 }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + + expect(result!.pinned).toBeTruthy(); }); }); - describe('updateSessionConfig', () => { + describe.skip('updateSessionConfig', () => { it('should update the config of a session', async () => { // Setup const newConfig = { model: 'abc' } as LobeAgentConfig; - (SessionModel.updateConfig as Mock).mockResolvedValue({ ...mockSession, config: newConfig }); // Execute - const result = await sessionService.updateSessionConfig(mockSessionId, newConfig); + await sessionService.updateSessionConfig(mockSessionId, newConfig); // Assert - expect(SessionModel.updateConfig).toHaveBeenCalledWith(mockSessionId, newConfig); + const result = await sessionService.getSessionConfig(mockSessionId); expect(result).toEqual({ ...mockSession, config: newConfig }); }); }); describe('countSessions', () => { it('should return false if no sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(0); + await clientDB.delete(sessions); // Execute const result = await sessionService.countSessions(); // Assert - expect(SessionModel.count).toHaveBeenCalled(); expect(result).toBe(0); }); it('should return true if sessions exist', async () => { // Setup - (SessionModel.count as Mock).mockResolvedValue(1); + await clientDB.delete(sessions); + await clientDB.insert(sessions).values([{ userId }]); // Execute const result = await sessionService.countSessions(); // Assert - expect(SessionModel.count).toHaveBeenCalled(); expect(result).toBe(1); }); }); - describe('hasSessions', () => { - it('should return false if no sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(0); - - // Execute - const result = await sessionService.hasSessions(); - - // Assert - expect(SessionModel.count).toHaveBeenCalled(); - expect(result).toBe(false); - }); - - it('should return true if sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(1); - - // Execute - const result = await sessionService.hasSessions(); - - // Assert - expect(SessionModel.count).toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); - describe('searchSessions', () => { it('should return sessions that match the keyword', async () => { // Setup - const keyword = 'search'; - (SessionModel.queryByKeyword as Mock).mockResolvedValue(mockSessions); + await clientDB.insert(agents).values({ userId, id: 'agent-1', title: 'Session Name' }); + await clientDB + .insert(agentsToSessions) + .values({ agentId: 'agent-1', sessionId: mockSessionId }); // Execute + const keyword = 'Name'; const result = await sessionService.searchSessions(keyword); // Assert - expect(SessionModel.queryByKeyword).toHaveBeenCalledWith(keyword); - expect(result).toBe(mockSessions); + // TODO: 后续需要把这个搜索的标题和描述都加上,现在这个 client 搜索会有问题 + expect(result).toMatchObject([{ id: mockSessionId }]); }); }); - describe('cloneSession', () => { + describe.skip('cloneSession', () => { it('should duplicate a session and return its id', async () => { // Setup const newTitle = 'Duplicated Session'; - (SessionModel.duplicate as Mock).mockResolvedValue({ - ...mockSession, + const session: NewSession = { id: 'duplicated-session-id', - }); + title: '123', + userId, + }; + await clientDB.insert(sessions).values([session]); + await clientDB.insert(agents).values({ userId, id: 'agent-1' }); + await clientDB + .insert(agentsToSessions) + .values({ agentId: 'agent-1', sessionId: 'duplicated-session-id' }); // Execute const duplicatedSessionId = await sessionService.cloneSession(mockSessionId, newTitle); // Assert - expect(SessionModel.duplicate).toHaveBeenCalledWith(mockSessionId, newTitle); - expect(duplicatedSessionId).toBe('duplicated-session-id'); + + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, duplicatedSessionId!), + }); + expect(result).toEqual({}); }); }); describe('getGroupedSessions', () => { it('should retrieve sessions with their group', async () => { - // Setup - (SessionModel.queryWithGroups as Mock).mockResolvedValue(mockSessions); - // Execute const sessionsWithGroup = await sessionService.getGroupedSessions(); - // Assert - expect(SessionModel.queryWithGroups).toHaveBeenCalled(); - expect(sessionsWithGroup).toBe(mockSessions); + expect(sessionsWithGroup).toMatchObject({ + sessionGroups: [ + { id: 'group-2', name: 'group-B', sort: 1 }, + { id: 'group-1', name: 'group-A', sort: 2 }, + ], + sessions: [{ id: 'mock-session-id', type: 'agent' }], + }); + }); + }); + + describe('getSessionsByType', () => { + it('should get sessions by type "all"', async () => { + const sessions = await sessionService.getSessionsByType('all'); + expect(sessions).toBeDefined(); + }); + + it('should get sessions by type "agent"', async () => { + const sessions = await sessionService.getSessionsByType('agent'); + expect(sessions).toBeDefined(); + }); + + it('should get sessions by type "group"', async () => { + const sessions = await sessionService.getSessionsByType('group'); + expect(sessions).toBeDefined(); + }); + }); + + describe('getSessionConfig', () => { + it.skip('should get default config for INBOX_SESSION_ID', async () => { + const config = await sessionService.getSessionConfig(INBOX_SESSION_ID); + expect(config).toBeDefined(); + }); + + it('should throw error for non-existent session', async () => { + await expect(sessionService.getSessionConfig('non-existent')).rejects.toThrow( + 'Session not found', + ); + }); + }); + + describe('updateSessionMeta', () => { + it('should not update meta for INBOX_SESSION_ID', async () => { + const result = await sessionService.updateSessionMeta(INBOX_SESSION_ID, { + title: 'New Title', + }); + expect(result).toBeUndefined(); + }); + + it('should update meta for normal session', async () => { + const meta = { title: 'Updated Title' }; + await sessionService.updateSessionMeta(mockSessionId, meta); + + const session = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + expect(session).toBeDefined(); + }); + }); + + describe('updateSessionChatConfig', () => { + it('should update chat config', async () => { + const chatConfig = { temperature: 0.8 } as Partial; + const result = await sessionService.updateSessionChatConfig(mockSessionId, chatConfig); + expect(result).toBeDefined(); + }); + }); + + describe('model getters', () => { + it('should return session model instance', () => { + // @ts-ignore - accessing private getter + const model = sessionService.sessionModel; + expect(model).toBeDefined(); + }); + + it('should return session group model instance', () => { + // @ts-ignore - accessing private getter + const model = sessionService.sessionGroupModel; + expect(model).toBeDefined(); }); }); @@ -323,84 +319,66 @@ describe('SessionService', () => { // Setup const groupName = 'New Group'; const sort = 1; - (SessionGroupModel.create as Mock).mockResolvedValue({ - id: 'new-group-id', - name: groupName, - sort, - }); // Execute const groupId = await sessionService.createSessionGroup(groupName, sort); // Assert - expect(SessionGroupModel.create).toHaveBeenCalledWith(groupName, sort); - expect(groupId).toBe('new-group-id'); - }); - }); - - describe('batchCreateSessionGroups', () => { - it('should batch create session groups', async () => { - // Setup - const groups = [ - { id: 'group-1', name: 'Group 1', sort: 1 }, - { id: 'group-2', name: 'Group 2', sort: 2 }, - ] as SessionGroups; - - (SessionGroupModel.batchCreate as Mock).mockResolvedValue(groups); + expect(groupId).toMatch(/^sg_/); - // Execute - const result = await sessionService.batchCreateSessionGroups(groups); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); - // Assert - expect(SessionGroupModel.batchCreate).toHaveBeenCalledWith(groups); - expect(result).toBe(groups); + expect(result).toMatchObject({ id: groupId, name: groupName, sort }); }); }); describe('removeSessionGroup', () => { it('should remove a session group by its id', async () => { - // Setup - const removeChildren = true; - (SessionGroupModel.delete as Mock).mockResolvedValue(true); - + const groupId = 'group-1'; // Execute - const result = await sessionService.removeSessionGroup('group-id', removeChildren); + await sessionService.removeSessionGroup(groupId); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); // Assert - expect(SessionGroupModel.delete).toHaveBeenCalledWith('group-id', removeChildren); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); describe('clearSessionGroups', () => { it('should clear all session groups', async () => { - // Setup - (SessionGroupModel.clear as Mock).mockResolvedValue(true); - // Execute - const result = await sessionService.removeSessionGroups(); + await sessionService.removeSessionGroups(); // Assert - expect(SessionGroupModel.clear).toHaveBeenCalled(); - expect(result).toBe(true); + const result = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + + expect(result.length).toBe(0); + + const result2 = await clientDB.query.sessionGroups.findMany({ + where: not(eq(sessionGroups.userId, userId)), + }); + + expect(result2.length).toBeGreaterThan(0); }); }); describe('getSessionGroups', () => { it('should retrieve all session groups', async () => { - // Setup - const groups = [ - { id: 'group-1', name: 'Group 1', sort: 1 }, - { id: 'group-2', name: 'Group 2', sort: 2 }, - ]; - (SessionGroupModel.query as Mock).mockResolvedValue(groups); - // Execute const result = await sessionService.getSessionGroups(); // Assert - expect(SessionGroupModel.query).toHaveBeenCalled(); - expect(result).toBe(groups); + const groups = [ + { id: 'group-2', name: 'group-B', sort: 1 }, + { id: 'group-1', name: 'group-A', sort: 2 }, + ]; + expect(result).toMatchObject(groups); }); }); @@ -409,14 +387,15 @@ describe('SessionService', () => { // Setup const groupId = 'group-1'; const data = { name: 'Updated Group', sort: 2 }; - (SessionGroupModel.update as Mock).mockResolvedValue({ id: groupId, ...data }); // Execute - const result = await sessionService.updateSessionGroup(groupId, data); + await sessionService.updateSessionGroup(groupId, data); // Assert - expect(SessionGroupModel.update).toHaveBeenCalledWith(groupId, data); - expect(result).toEqual({ id: groupId, ...data }); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); + expect(result).toMatchObject({ id: groupId, ...data }); }); }); @@ -427,14 +406,18 @@ describe('SessionService', () => { { id: 'group-1', sort: 2 }, { id: 'group-2', sort: 1 }, ]; - (SessionGroupModel.updateOrder as Mock).mockResolvedValue(true); // Execute - const result = await sessionService.updateSessionGroupOrder(sortMap); + await sessionService.updateSessionGroupOrder(sortMap); // Assert - expect(SessionGroupModel.updateOrder).toHaveBeenCalledWith(sortMap); - expect(result).toBe(true); + const data = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + expect(data).toMatchObject([ + { id: 'group-1', sort: 2 }, + { id: 'group-2', sort: 1 }, + ]); }); }); }); diff --git a/src/services/session/client.ts b/src/services/session/client.ts index 2d205cca8c8c..7be00e50add4 100644 --- a/src/services/session/client.ts +++ b/src/services/session/client.ts @@ -1,9 +1,11 @@ import { DeepPartial } from 'utility-types'; import { INBOX_SESSION_ID } from '@/const/session'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; -import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { AgentItem } from '@/database/schemas'; +import { SessionModel } from '@/database/server/models/session'; +import { SessionGroupModel } from '@/database/server/models/sessionGroup'; +import { BaseClientService } from '@/services/baseClientService'; import { useUserStore } from '@/store/user'; import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent'; import { MetaData } from '@/types/meta'; @@ -19,12 +21,23 @@ import { merge } from '@/utils/merge'; import { ISessionService } from './type'; -export class ClientService implements ISessionService { - async createSession( - type: LobeSessionType, - defaultValue: Partial, - ): Promise { - const item = await SessionModel.create(type, defaultValue); +export class ClientService extends BaseClientService implements ISessionService { + private get sessionModel(): SessionModel { + return new SessionModel(clientDB as any, this.userId); + } + + private get sessionGroupModel(): SessionGroupModel { + return new SessionGroupModel(clientDB as any, this.userId); + } + + async createSession(type: LobeSessionType, data: Partial): Promise { + const { config, group, meta, ...session } = data; + + const item = await this.sessionModel.create({ + config: { ...config, ...meta } as any, + session: { ...session, groupId: group }, + type, + }); if (!item) { throw new Error('session create Error'); } @@ -32,72 +45,65 @@ export class ClientService implements ISessionService { } async batchCreateSessions(importSessions: LobeSessions) { - return SessionModel.batchCreate(importSessions); + // @ts-ignore + return this.sessionModel.batchCreate(importSessions); } async cloneSession(id: string, newTitle: string): Promise { - const res = await SessionModel.duplicate(id, newTitle); + const res = await this.sessionModel.duplicate(id, newTitle); if (res) return res?.id; } async getGroupedSessions(): Promise { - return SessionModel.queryWithGroups(); + return this.sessionModel.queryWithGroups(); } async getSessionConfig(id: string): Promise { - if (!id || id === INBOX_SESSION_ID) { - return UserModel.getAgentConfig(); - } - - const res = await SessionModel.findById(id); + const res = await this.sessionModel.findByIdOrSlug(id); if (!res) throw new Error('Session not found'); - return res.config as LobeAgentConfig; + return res.agent as LobeAgentConfig; } + /** + * 这个方法要对应移除的 + */ async getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise { switch (type) { // TODO: add a filter to get only agents or agents case 'group': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } case 'agent': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } case 'all': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } } } - async getAllAgents(): Promise { - // TODO: add a filter to get only agents - return await SessionModel.query(); - } - async countSessions() { - return SessionModel.count(); - } - - async hasSessions() { - return (await this.countSessions()) !== 0; + return this.sessionModel.count(); } async searchSessions(keyword: string) { - return SessionModel.queryByKeyword(keyword); + return this.sessionModel.queryByKeyword(keyword); } async updateSession( id: string, data: Partial>, ) { - const pinned = typeof data.pinned === 'boolean' ? (data.pinned ? 1 : 0) : undefined; - const prev = await SessionModel.findById(id); + const prev = await this.sessionModel.findByIdOrSlug(id); - return SessionModel.update(id, merge(prev, { ...data, pinned })); + return this.sessionModel.update(id, merge(prev, data)); } async updateSessionConfig( @@ -112,7 +118,7 @@ export class ClientService implements ISessionService { return useUserStore.getState().updateDefaultAgent({ config }); } - return SessionModel.updateConfig(activeId, config); + return this.sessionModel.updateConfig(activeId, config as AgentItem); } async updateSessionMeta( @@ -124,7 +130,7 @@ export class ClientService implements ISessionService { // inbox 不允许修改 meta if (activeId === INBOX_SESSION_ID) return; - return SessionModel.update(activeId, { meta }); + return this.sessionModel.update(activeId, meta); } async updateSessionChatConfig( @@ -137,11 +143,11 @@ export class ClientService implements ISessionService { } async removeSession(id: string) { - return SessionModel.delete(id); + return this.sessionModel.delete(id); } async removeAllSessions() { - return SessionModel.clearTable(); + return this.sessionModel.deleteAll(); } // ************************************** // @@ -149,7 +155,7 @@ export class ClientService implements ISessionService { // ************************************** // async createSessionGroup(name: string, sort?: number) { - const item = await SessionGroupModel.create(name, sort); + const item = await this.sessionGroupModel.create({ name, sort }); if (!item) { throw new Error('session group create Error'); } @@ -157,27 +163,28 @@ export class ClientService implements ISessionService { return item.id; } - async batchCreateSessionGroups(groups: SessionGroups) { - return SessionGroupModel.batchCreate(groups); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async batchCreateSessionGroups(_groups: SessionGroups) { + return { added: 0, ids: [], skips: [], success: true }; } - async removeSessionGroup(id: string, removeChildren?: boolean) { - return await SessionGroupModel.delete(id, removeChildren); + async removeSessionGroup(id: string) { + return await this.sessionGroupModel.delete(id); } async updateSessionGroup(id: string, data: Partial) { - return SessionGroupModel.update(id, data); + return this.sessionGroupModel.update(id, data); } async updateSessionGroupOrder(sortMap: { id: string; sort: number }[]) { - return SessionGroupModel.updateOrder(sortMap); + return this.sessionGroupModel.updateOrder(sortMap); } async getSessionGroups(): Promise { - return SessionGroupModel.query(); + return this.sessionGroupModel.query(); } async removeSessionGroups() { - return SessionGroupModel.clear(); + return this.sessionGroupModel.deleteAll(); } } diff --git a/src/services/session/type.ts b/src/services/session/type.ts index f51f2f39780d..59528fe138ca 100644 --- a/src/services/session/type.ts +++ b/src/services/session/type.ts @@ -16,13 +16,21 @@ import { export interface ISessionService { createSession(type: LobeSessionType, defaultValue: Partial): Promise; + + /** + * 需要废弃 + * @deprecated + */ batchCreateSessions(importSessions: LobeSessions): Promise; cloneSession(id: string, newTitle: string): Promise; getGroupedSessions(): Promise; + + /** + * @deprecated + */ getSessionsByType(type: 'agent' | 'group' | 'all'): Promise; countSessions(): Promise; - hasSessions(): Promise; searchSessions(keyword: string): Promise; updateSession( @@ -53,6 +61,11 @@ export interface ISessionService { // ************************************** // createSessionGroup(name: string, sort?: number): Promise; + + /** + * 需要废弃 + * @deprecated + */ batchCreateSessionGroups(groups: SessionGroups): Promise; getSessionGroups(): Promise; diff --git a/src/services/topic/client.test.ts b/src/services/topic/client.test.ts index 211abefa7d36..476dbb36a6f0 100644 --- a/src/services/topic/client.test.ts +++ b/src/services/topic/client.test.ts @@ -1,75 +1,62 @@ -import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { CreateTopicParams, TopicModel } from '@/database/_deprecated/models/topic'; +import { clientDB, initializeDB } from '@/database/client/db'; +import { sessions, topics, users } from '@/database/schemas'; import { ChatTopic } from '@/types/topic'; import { ClientService } from './client'; -const topicService = new ClientService(); -// Mock the TopicModel -vi.mock('@/database/_deprecated/models/topic', () => { - return { - TopicModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - count: vi.fn(), - batchDeleteBySessionId: vi.fn(), - batchDelete: vi.fn(), - clearTable: vi.fn(), - toggleFavorite: vi.fn(), - batchCreate: vi.fn(), - update: vi.fn(), - queryAll: vi.fn(), - queryByKeyword: vi.fn(), - }, - }; -}); +// Mock data +const userId = 'topic-user-test'; +const sessionId = 'topic-session'; +const mockTopicId = 'mock-topic-id'; -describe('TopicService', () => { - // Mock data - const mockTopicId = 'mock-topic-id'; - const mockTopic: ChatTopic = { - createdAt: 100, - updatedAt: 100, - id: mockTopicId, - title: 'Mock Topic', - }; - const mockTopics = [mockTopic]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); +const mockTopic = { + id: mockTopicId, + title: 'Mock Topic', +}; + +const topicService = new ClientService(userId); + +beforeEach(async () => { + await initializeDB(); + + await clientDB.delete(users); + + // 创建测试数据 + await clientDB.transaction(async (tx) => { + await tx.insert(users).values({ id: userId }); + await tx.insert(sessions).values({ id: sessionId, userId }); + await tx.insert(topics).values({ ...mockTopic, sessionId, userId }); }); +}); +describe('TopicService', () => { describe('createTopic', () => { it('should create a topic and return its id', async () => { // Setup - const createParams: CreateTopicParams = { + const createParams = { title: 'New Topic', - sessionId: '1', + sessionId: sessionId, }; - (TopicModel.create as Mock).mockResolvedValue(mockTopic); // Execute const topicId = await topicService.createTopic(createParams); // Assert - expect(TopicModel.create).toHaveBeenCalledWith(createParams); - expect(topicId).toBe(mockTopicId); + expect(topicId).toBeDefined(); }); + it('should throw an error if topic creation fails', async () => { // Setup - const createParams: CreateTopicParams = { + const createParams = { title: 'New Topic', - sessionId: '1', + sessionId: 123 as any, // sessionId should be string }; - (TopicModel.create as Mock).mockResolvedValue(null); - // Execute & Assert - await expect(topicService.createTopic(createParams)).rejects.toThrow('topic create Error'); + await expect(topicService.createTopic(createParams)).rejects.toThrowError(); }); }); @@ -77,56 +64,46 @@ describe('TopicService', () => { // Example for getTopics it('should query topics with given parameters', async () => { // Setup - const queryParams = { sessionId: 'session-id' }; - (TopicModel.query as Mock).mockResolvedValue(mockTopics); + const queryParams = { sessionId }; // Execute - const topics = await topicService.getTopics(queryParams); + const data = await topicService.getTopics(queryParams); // Assert - expect(TopicModel.query).toHaveBeenCalledWith(queryParams); - expect(topics).toBe(mockTopics); + expect(data[0]).toMatchObject(mockTopic); }); }); describe('updateTopic', () => { // Example for updateFavorite it('should toggle favorite status of a topic', async () => { - // Setup - const newState = true; - // Execute - await topicService.updateTopic(mockTopicId, { favorite: newState }); + const result = await topicService.updateTopic(mockTopicId, { favorite: true }); // Assert - expect(TopicModel.update).toHaveBeenCalledWith(mockTopicId, { favorite: 1 }); + expect(result[0].favorite).toBeTruthy(); }); it('should update the title of a topic', async () => { // Setup const newTitle = 'Updated Topic Title'; - (TopicModel.update as Mock).mockResolvedValue({ ...mockTopic, title: newTitle }); // Execute const result = await topicService.updateTopic(mockTopicId, { title: newTitle }); // Assert - expect(TopicModel.update).toHaveBeenCalledWith(mockTopicId, { title: newTitle }); - expect(result).toEqual({ ...mockTopic, title: newTitle }); + expect(result[0].title).toEqual(newTitle); }); }); describe('removeTopic', () => { it('should remove a topic by id', async () => { - // Setup - (TopicModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await topicService.removeTopic(mockTopicId); + await topicService.removeTopic(mockTopicId); + const result = await clientDB.query.topics.findFirst({ where: eq(topics.id, mockTopicId) }); // Assert - expect(TopicModel.delete).toHaveBeenCalledWith(mockTopicId); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); @@ -134,111 +111,101 @@ describe('TopicService', () => { it('should remove all topics with a given session id', async () => { // Setup const sessionId = 'session-id'; - (TopicModel.batchDeleteBySessionId as Mock).mockResolvedValue(true); // Execute - const result = await topicService.removeTopics(sessionId); + await topicService.removeTopics(sessionId); + const result = await clientDB.query.topics.findMany({ + where: eq(topics.sessionId, sessionId), + }); - // Assert - expect(TopicModel.batchDeleteBySessionId).toHaveBeenCalledWith(sessionId); - expect(result).toBe(true); + expect(result.length).toEqual(0); }); }); describe('batchRemoveTopics', () => { it('should batch remove topics', async () => { + await clientDB.insert(topics).values([{ id: 'topic-id-1', title: 'topic-title', userId }]); // Setup const topicIds = [mockTopicId, 'another-topic-id']; - (TopicModel.batchDelete as Mock).mockResolvedValue(true); // Execute - const result = await topicService.batchRemoveTopics(topicIds); + await topicService.batchRemoveTopics(topicIds); + + const count = await clientDB.$count(topics); // Assert - expect(TopicModel.batchDelete).toHaveBeenCalledWith(topicIds); - expect(result).toBe(true); + expect(count).toBe(1); }); }); describe('removeAllTopic', () => { it('should clear all topics from the table', async () => { - // Setup - (TopicModel.clearTable as Mock).mockResolvedValue(true); - // Execute - const result = await topicService.removeAllTopic(); + await topicService.removeAllTopic(); + const count = await clientDB.$count(topics); // Assert - expect(TopicModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); + expect(count).toBe(0); }); }); describe('batchCreateTopics', () => { it('should batch create topics', async () => { - // Setup - (TopicModel.batchCreate as Mock).mockResolvedValue(mockTopics); - // Execute - const result = await topicService.batchCreateTopics(mockTopics); + const result = await topicService.batchCreateTopics([ + { id: 'topic-id-1', title: 'topic-title' }, + { id: 'topic-id-2', title: 'topic-title' }, + ] as ChatTopic[]); // Assert - expect(TopicModel.batchCreate).toHaveBeenCalledWith(mockTopics); - expect(result).toBe(mockTopics); + expect(result.success).toBeTruthy(); + expect(result.added).toBe(2); }); }); describe('getAllTopics', () => { it('should retrieve all topics', async () => { - // Setup - (TopicModel.queryAll as Mock).mockResolvedValue(mockTopics); - + await clientDB.insert(topics).values([ + { id: 'topic-id-1', title: 'topic-title', userId }, + { id: 'topic-id-2', title: 'topic-title', userId }, + ]); // Execute const result = await topicService.getAllTopics(); // Assert - expect(TopicModel.queryAll).toHaveBeenCalled(); - expect(result).toBe(mockTopics); + expect(result.length).toEqual(3); }); }); describe('searchTopics', () => { it('should return all topics that match the keyword', async () => { // Setup - const keyword = 'search'; - (TopicModel.queryByKeyword as Mock).mockResolvedValue(mockTopics); + const keyword = 'Topic'; // Execute - const result = await topicService.searchTopics(keyword, undefined); + const result = await topicService.searchTopics(keyword, sessionId); // Assert - expect(TopicModel.queryByKeyword).toHaveBeenCalledWith(keyword, undefined); - expect(result).toBe(mockTopics); + expect(result.length).toEqual(1); }); - }); - - describe('countTopics', () => { - it('should return false if no topics exist', async () => { + it('should return empty topic if not match the keyword', async () => { // Setup - (TopicModel.count as Mock).mockResolvedValue(0); + const keyword = 'search'; // Execute - const result = await topicService.countTopics(); + const result = await topicService.searchTopics(keyword, sessionId); // Assert - expect(TopicModel.count).toHaveBeenCalled(); - expect(result).toBe(0); + expect(result.length).toEqual(0); }); + }); - it('should return true if topics exist', async () => { - // Setup - (TopicModel.count as Mock).mockResolvedValue(1); - + describe('countTopics', () => { + it('should return topic counts', async () => { // Execute const result = await topicService.countTopics(); // Assert - expect(TopicModel.count).toHaveBeenCalled(); expect(result).toBe(1); }); }); diff --git a/src/services/topic/client.ts b/src/services/topic/client.ts index eeb2ffa2e395..08ae914bd8b9 100644 --- a/src/services/topic/client.ts +++ b/src/services/topic/client.ts @@ -1,11 +1,17 @@ -import { TopicModel } from '@/database/_deprecated/models/topic'; +import { clientDB } from '@/database/client/db'; +import { TopicModel } from '@/database/server/models/topic'; +import { BaseClientService } from '@/services/baseClientService'; import { ChatTopic } from '@/types/topic'; import { CreateTopicParams, ITopicService, QueryTopicParams } from './type'; -export class ClientService implements ITopicService { +export class ClientService extends BaseClientService implements ITopicService { + private get topicModel(): TopicModel { + return new TopicModel(clientDB as any, this.userId); + } + async createTopic(params: CreateTopicParams): Promise { - const item = await TopicModel.create(params as any); + const item = await this.topicModel.create(params as any); if (!item) { throw new Error('topic create Error'); @@ -15,56 +21,54 @@ export class ClientService implements ITopicService { } async batchCreateTopics(importTopics: ChatTopic[]) { - return TopicModel.batchCreate(importTopics as any); + const data = await this.topicModel.batchCreate(importTopics as any); + + return { added: data.length, ids: [], skips: [], success: true }; } async cloneTopic(id: string, newTitle?: string) { - return TopicModel.duplicateTopic(id, newTitle); + const data = await this.topicModel.duplicate(id, newTitle); + return data.topic.id; } - async getTopics(params: QueryTopicParams): Promise { - return TopicModel.query(params); + async getTopics(params: QueryTopicParams) { + const data = await this.topicModel.query(params); + return data as unknown as Promise; } async searchTopics(keyword: string, sessionId?: string) { - return TopicModel.queryByKeyword(keyword, sessionId); - } + const data = await this.topicModel.queryByKeyword(keyword, sessionId); - async getAllTopics() { - return TopicModel.queryAll(); + return data as unknown as Promise; } - async countTopics() { - return TopicModel.count(); - } + async getAllTopics() { + const data = await this.topicModel.queryAll(); - async updateTopicFavorite(id: string, favorite?: boolean) { - return this.updateTopic(id, { favorite }); + return data as unknown as Promise; } - async updateTopicTitle(id: string, text: string) { - return this.updateTopic(id, { title: text }); + async countTopics() { + return this.topicModel.count(); } async updateTopic(id: string, data: Partial) { - const favorite = typeof data.favorite !== 'undefined' ? (data.favorite ? 1 : 0) : undefined; - - return TopicModel.update(id, { ...data, favorite }); + return this.topicModel.update(id, data as any); } async removeTopic(id: string) { - return TopicModel.delete(id); + return this.topicModel.delete(id); } async removeTopics(sessionId: string) { - return TopicModel.batchDeleteBySessionId(sessionId); + return this.topicModel.batchDeleteBySessionId(sessionId); } async batchRemoveTopics(topics: string[]) { - return TopicModel.batchDelete(topics); + return this.topicModel.batchDelete(topics); } async removeAllTopic() { - return TopicModel.clearTable(); + return this.topicModel.deleteAll(); } } diff --git a/src/services/topic/index.ts b/src/services/topic/index.ts index 360656149ea5..7105ac9765dc 100644 --- a/src/services/topic/index.ts +++ b/src/services/topic/index.ts @@ -1,4 +1,3 @@ - import { ClientService } from './client'; import { ServerService } from './server'; diff --git a/src/services/upload.ts b/src/services/upload.ts index 38f6cdd2959b..64fec769ecad 100644 --- a/src/services/upload.ts +++ b/src/services/upload.ts @@ -1,7 +1,8 @@ import { fileEnv } from '@/config/file'; import { edgeClient } from '@/libs/trpc/client'; import { API_ENDPOINTS } from '@/services/_url'; -import { FileMetadata, UploadFileParams } from '@/types/files'; +import { clientS3Storage } from '@/services/file/ClientS3'; +import { FileMetadata } from '@/types/files'; import { FileUploadState, FileUploadStatus } from '@/types/files/upload'; import { uuid } from '@/utils/uuid'; @@ -66,23 +67,14 @@ class UploadService { return result; }; - uploadToClientDB = async (params: UploadFileParams, file: File) => { - const { FileModel } = await import('@/database/_deprecated/models/file'); - const fileArrayBuffer = await file.arrayBuffer(); - - // save to local storage - // we may want to save to a remote server later - const res = await FileModel.create({ - createdAt: Date.now(), - ...params, - data: fileArrayBuffer, - }); - // arrayBuffer to url - const base64 = Buffer.from(fileArrayBuffer).toString('base64'); + uploadToClientS3 = async (hash: string, file: File): Promise => { + await clientS3Storage.putObject(hash, file); return { - id: res.id, - url: `data:${params.fileType};base64,${base64}`, + date: (Date.now() / 1000 / 60 / 60).toFixed(0), + dirname: '', + filename: file.name, + path: `client-s3://${hash}`, }; }; diff --git a/src/services/user/client.test.ts b/src/services/user/client.test.ts index f79f8294a2f5..abb57b37baab 100644 --- a/src/services/user/client.test.ts +++ b/src/services/user/client.test.ts @@ -1,22 +1,14 @@ +import { eq } from 'drizzle-orm'; import { DeepPartial } from 'utility-types'; -import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB, initializeDB } from '@/database/client/db'; +import { userSettings, users } from '@/database/schemas'; import { UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; -import { AsyncLocalStorage } from '@/utils/localStorage'; import { ClientService } from './client'; -vi.mock('@/database/_deprecated/models/user', () => ({ - UserModel: { - getUser: vi.fn(), - updateSettings: vi.fn(), - resetSettings: vi.fn(), - updateAvatar: vi.fn(), - }, -})); - const mockUser = { avatar: 'avatar.png', settings: { themeMode: 'light' } as unknown as UserSettings, @@ -26,63 +18,67 @@ const mockUser = { const mockPreference = { useCmdEnterToSend: true, } as UserPreference; +const clientService = new ClientService(mockUser.uuid); -describe('ClientService', () => { - let clientService: ClientService; +beforeEach(async () => { + vi.clearAllMocks(); - beforeEach(() => { - vi.clearAllMocks(); - clientService = new ClientService(); - }); + await initializeDB(); + await clientDB.delete(users); + await clientDB.insert(users).values({ id: mockUser.uuid, avatar: 'avatar.png' }); + await clientDB + .insert(userSettings) + .values({ id: mockUser.uuid, general: { themeMode: 'light' } }); +}); + +describe('ClientService', () => { it('should get user state correctly', async () => { - (UserModel.getUser as Mock).mockResolvedValue(mockUser); const spyOn = vi .spyOn(clientService['preferenceStorage'], 'getFromLocalStorage') .mockResolvedValue(mockPreference); const userState = await clientService.getUserState(); - expect(userState).toEqual({ + expect(userState).toMatchObject({ avatar: mockUser.avatar, isOnboard: true, canEnablePWAGuide: false, hasConversation: false, canEnableTrace: false, preference: mockPreference, - settings: mockUser.settings, + settings: { general: { themeMode: 'light' } }, userId: mockUser.uuid, }); - expect(UserModel.getUser).toHaveBeenCalledTimes(1); expect(spyOn).toHaveBeenCalledTimes(1); }); it('should update user settings correctly', async () => { const settingsPatch: DeepPartial = { general: { themeMode: 'dark' } }; - (UserModel.updateSettings as Mock).mockResolvedValue(undefined); await clientService.updateUserSettings(settingsPatch); - expect(UserModel.updateSettings).toHaveBeenCalledWith(settingsPatch); - expect(UserModel.updateSettings).toHaveBeenCalledTimes(1); + const result = await clientDB.query.userSettings.findFirst({ + where: eq(userSettings.id, mockUser.uuid), + }); + + expect(result).toMatchObject(settingsPatch); }); it('should reset user settings correctly', async () => { - (UserModel.resetSettings as Mock).mockResolvedValue(undefined); - await clientService.resetUserSettings(); - expect(UserModel.resetSettings).toHaveBeenCalledTimes(1); + const result = await clientDB.query.userSettings.findFirst({ + where: eq(userSettings.id, mockUser.uuid), + }); + + expect(result).toBeUndefined(); }); it('should update user avatar correctly', async () => { const newAvatar = 'new-avatar.png'; - (UserModel.updateAvatar as Mock).mockResolvedValue(undefined); await clientService.updateAvatar(newAvatar); - - expect(UserModel.updateAvatar).toHaveBeenCalledWith(newAvatar); - expect(UserModel.updateAvatar).toHaveBeenCalledTimes(1); }); it('should update user preference correctly', async () => { diff --git a/src/services/user/client.ts b/src/services/user/client.ts index d05a3c104987..f6d3ee863b6d 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -1,49 +1,67 @@ import { DeepPartial } from 'utility-types'; -import { MessageModel } from '@/database/_deprecated/models/message'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { users } from '@/database/schemas'; +import { MessageModel } from '@/database/server/models/message'; +import { SessionModel } from '@/database/server/models/session'; +import { UserModel } from '@/database/server/models/user'; +import { BaseClientService } from '@/services/baseClientService'; import { UserGuide, UserInitializationState, UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; import { AsyncLocalStorage } from '@/utils/localStorage'; +import { uuid } from '@/utils/uuid'; import { IUserService } from './type'; -export class ClientService implements IUserService { +export class ClientService extends BaseClientService implements IUserService { private preferenceStorage: AsyncLocalStorage; - constructor() { + private get userModel(): UserModel { + return new UserModel(clientDB as any, this.userId); + } + private get messageModel(): MessageModel { + return new MessageModel(clientDB as any, this.userId); + } + private get sessionModel(): SessionModel { + return new SessionModel(clientDB as any, this.userId); + } + + constructor(userId?: string) { + super(userId); this.preferenceStorage = new AsyncLocalStorage('LOBE_PREFERENCE'); } async getUserState(): Promise { - const user = await UserModel.getUser(); - const messageCount = await MessageModel.count(); - const sessionCount = await SessionModel.count(); + // if user not exist in the db, create one to make sure the user exist + // and init the window.__lobeClientUserId + await this.makeSureUserExist(); + + const state = await this.userModel.getUserState(); + const user = await UserModel.findById(clientDB as any, this.userId); + const messageCount = await this.messageModel.count(); + const sessionCount = await this.sessionModel.count(); return { - avatar: user.avatar, + ...state, + avatar: user?.avatar as string, canEnablePWAGuide: messageCount >= 4, canEnableTrace: messageCount >= 4, hasConversation: messageCount > 0 || sessionCount > 0, isOnboard: true, preference: await this.preferenceStorage.getFromLocalStorage(), - settings: user.settings as UserSettings, - userId: user.uuid, }; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateUserSettings = async (patch: DeepPartial, _?: any) => { - return UserModel.updateSettings(patch); + updateUserSettings = async (patch: DeepPartial) => { + return this.userModel.updateSetting(patch as UserSettings); }; resetUserSettings = async () => { - return UserModel.resetSettings(); + return this.userModel.deleteSetting(); }; async updateAvatar(avatar: string) { - await UserModel.updateAvatar(avatar); + await this.userModel.updateUser({ avatar }); } async updatePreference(preference: Partial) { @@ -54,4 +72,20 @@ export class ClientService implements IUserService { async updateGuide(guide: Partial) { throw new Error('Method not implemented.'); } + + private async makeSureUserExist() { + const existUsers = await clientDB.query.users.findMany(); + + let user: { id: string }; + if (existUsers.length === 0) { + const result = await clientDB.insert(users).values({ id: uuid() }).returning(); + user = result[0]; + } else { + user = existUsers[0]; + } + + if (typeof window !== 'undefined') { + window.__lobeClientUserId = user.id; + } + } } diff --git a/src/store/chat/slices/builtinTool/action.test.ts b/src/store/chat/slices/builtinTool/action.test.ts index 795806b44195..01c73a6b8091 100644 --- a/src/store/chat/slices/builtinTool/action.test.ts +++ b/src/store/chat/slices/builtinTool/action.test.ts @@ -2,6 +2,8 @@ import { act, renderHook } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { fileService } from '@/services/file'; +import { ClientService } from '@/services/file/client'; +import { messageService } from '@/services/message'; import { imageGenerationService } from '@/services/textToImage'; import { uploadService } from '@/services/upload'; import { chatSelectors } from '@/store/chat/selectors'; @@ -39,17 +41,22 @@ describe('chatToolSlice', () => { vi.spyOn(uploadService, 'getImageFileByUrlWithCORS').mockResolvedValue( new File(['1'], 'file.png', { type: 'image/png' }), ); - vi.spyOn(uploadService, 'uploadToClientDB').mockResolvedValue({} as any); - vi.spyOn(fileService, 'createFile').mockResolvedValue({ id: mockId, url: '' }); + vi.spyOn(uploadService, 'uploadToClientS3').mockResolvedValue({} as any); + vi.spyOn(ClientService.prototype, 'createFile').mockResolvedValue({ + id: mockId, + url: '', + }); vi.spyOn(result.current, 'toggleDallEImageLoading'); + vi.spyOn(ClientService.prototype, 'checkFileHash').mockImplementation(async () => ({ + isExist: false, + })); await act(async () => { await result.current.generateImageFromPrompts(prompts, messageId); }); // For each prompt, loading is toggled on and then off expect(imageGenerationService.generateImage).toHaveBeenCalledTimes(prompts.length); - expect(uploadService.uploadToClientDB).toHaveBeenCalledTimes(prompts.length); - + expect(uploadService.uploadToClientS3).toHaveBeenCalledTimes(prompts.length); expect(result.current.toggleDallEImageLoading).toHaveBeenCalledTimes(prompts.length * 2); }); }); @@ -75,6 +82,7 @@ describe('chatToolSlice', () => { content: initialMessageContent, }) as ChatMessage, ); + vi.spyOn(messageService, 'updateMessage').mockResolvedValueOnce(undefined); await act(async () => { await result.current.updateImageItem(messageId, updateFunction); diff --git a/src/store/file/slices/upload/action.ts b/src/store/file/slices/upload/action.ts index 1134d7907f43..3f73de77b08b 100644 --- a/src/store/file/slices/upload/action.ts +++ b/src/store/file/slices/upload/action.ts @@ -6,14 +6,11 @@ import { message } from '@/components/AntdStaticMethods'; import { LOBE_CHAT_CLOUD } from '@/const/branding'; import { isServerMode } from '@/const/version'; import { fileService } from '@/services/file'; -import { ServerService } from '@/services/file/server'; import { uploadService } from '@/services/upload'; import { FileMetadata, UploadFileItem } from '@/types/files'; import { FileStore } from '../../store'; -const serverFileService = new ServerService(); - interface UploadWithProgressParams { file: File; knowledgeBaseId?: string; @@ -43,10 +40,6 @@ interface UploadWithProgressResult { } export interface FileUploadAction { - internal_uploadToClientDB: ( - params: Omit, - ) => Promise; - internal_uploadToServer: (params: UploadWithProgressParams) => Promise; uploadWithProgress: ( params: UploadWithProgressParams, ) => Promise; @@ -57,51 +50,14 @@ export const createFileUploadSlice: StateCreator< [['zustand/devtools', never]], [], FileUploadAction -> = (set, get) => ({ - internal_uploadToClientDB: async ({ file, onStatusUpdate, skipCheckFileType }) => { - if (!skipCheckFileType && !file.type.startsWith('image')) { - onStatusUpdate?.({ id: file.name, type: 'removeFile' }); - message.info({ - content: t('upload.fileOnlySupportInServerMode', { - cloud: LOBE_CHAT_CLOUD, - ext: file.name.split('.').pop(), - ns: 'error', - }), - duration: 5, - }); - return; - } - - const fileArrayBuffer = await file.arrayBuffer(); - - const hash = sha256(fileArrayBuffer); - - const data = await uploadService.uploadToClientDB( - { fileType: file.type, hash, name: file.name, saveMode: 'local', size: file.size }, - file, - ); - - onStatusUpdate?.({ - id: file.name, - type: 'updateFile', - value: { - fileUrl: data.url, - id: data.id, - status: 'success', - uploadState: { progress: 100, restTime: 0, speed: 0 }, - }, - }); - - return data; - }, - - internal_uploadToServer: async ({ file, onStatusUpdate, knowledgeBaseId }) => { +> = () => ({ + uploadWithProgress: async ({ file, onStatusUpdate, knowledgeBaseId, skipCheckFileType }) => { const fileArrayBuffer = await file.arrayBuffer(); // 1. check file hash const hash = sha256(fileArrayBuffer); - const checkStatus = await serverFileService.checkFileHash(hash); + const checkStatus = await fileService.checkFileHash(hash); let metadata: FileMetadata; // 2. if file exist, just skip upload @@ -112,17 +68,37 @@ export const createFileUploadSlice: StateCreator< type: 'updateFile', value: { status: 'processing', uploadState: { progress: 100, restTime: 0, speed: 0 } }, }); - } else { - // 2. if file don't exist, need upload files - metadata = await uploadService.uploadWithProgress(file, { - onProgress: (status, upload) => { - onStatusUpdate?.({ - id: file.name, - type: 'updateFile', - value: { status: status === 'success' ? 'processing' : status, uploadState: upload }, + } + // 2. if file don't exist, need upload files + else { + // if is server mode, upload to server s3, or upload to client s3 + if (isServerMode) { + metadata = await uploadService.uploadWithProgress(file, { + onProgress: (status, upload) => { + onStatusUpdate?.({ + id: file.name, + type: 'updateFile', + value: { status: status === 'success' ? 'processing' : status, uploadState: upload }, + }); + }, + }); + } else { + if (!skipCheckFileType && !file.type.startsWith('image')) { + onStatusUpdate?.({ id: file.name, type: 'removeFile' }); + message.info({ + content: t('upload.fileOnlySupportInServerMode', { + cloud: LOBE_CHAT_CLOUD, + ext: file.name.split('.').pop(), + ns: 'error', + }), + duration: 5, }); - }, - }); + return; + } + + // Upload to the indexeddb in the browser + metadata = await uploadService.uploadToClientS3(hash, file); + } } // 3. use more powerful file type detector to get file type @@ -138,12 +114,10 @@ export const createFileUploadSlice: StateCreator< // 4. create file to db const data = await fileService.createFile( { - createdAt: Date.now(), fileType, hash, metadata, name: file.name, - saveMode: 'url', size: file.size, url: metadata.path, }, @@ -163,12 +137,4 @@ export const createFileUploadSlice: StateCreator< return data; }, - - uploadWithProgress: async (payload) => { - const { internal_uploadToServer, internal_uploadToClientDB } = get(); - - if (isServerMode) return internal_uploadToServer(payload); - - return internal_uploadToClientDB(payload); - }, }); diff --git a/src/store/global/actions/clientDb.ts b/src/store/global/actions/clientDb.ts new file mode 100644 index 000000000000..0f6cc465d0d5 --- /dev/null +++ b/src/store/global/actions/clientDb.ts @@ -0,0 +1,45 @@ +import { SWRResponse } from 'swr'; +import type { StateCreator } from 'zustand/vanilla'; + +import { useOnlyFetchOnceSWR } from '@/libs/swr'; +import type { GlobalStore } from '@/store/global'; +import { DatabaseLoadingState } from '@/types/clientDB'; + +/** + * 设置操作 + */ +export interface GlobalClientDBAction { + initializeClientDB: () => Promise; + markPgliteEnabled: () => void; + useInitClientDB: () => SWRResponse; +} + +export const clientDBSlice: StateCreator< + GlobalStore, + [['zustand/devtools', never]], + [], + GlobalClientDBAction +> = (set, get) => ({ + initializeClientDB: async () => { + // if the db has started initialized or not error, just skip. + if ( + get().initClientDBStage !== DatabaseLoadingState.Idle && + get().initClientDBStage !== DatabaseLoadingState.Error + ) + return; + + const { initializeDB } = await import('@/database/client/db'); + await initializeDB({ + onProgress: (data) => { + set({ initClientDBProcess: data }); + }, + onStateChange: (state) => { + set({ initClientDBStage: state }); + }, + }); + }, + markPgliteEnabled: () => { + get().updateSystemStatus({ isEnablePglite: true }); + }, + useInitClientDB: () => useOnlyFetchOnceSWR('initClientDB', () => get().initializeClientDB()), +}); diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts index 5da29090f9a3..5c28efc707ae 100644 --- a/src/store/global/initialState.ts +++ b/src/store/global/initialState.ts @@ -1,5 +1,6 @@ import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; +import { DatabaseLoadingState } from '@/types/clientDB'; import { SessionDefaultGroup } from '@/types/session'; import { AsyncLocalStorage } from '@/utils/localStorage'; @@ -37,6 +38,10 @@ export interface SystemStatus { hidePWAInstaller?: boolean; hideThreadLimitAlert?: boolean; inputHeight: number; + /** + * 应用初始化时不启用 PGLite,只有当用户手动开启时才启用 + */ + isEnablePglite?: boolean; mobileShowPortal?: boolean; mobileShowTopic?: boolean; sessionsWidth: number; @@ -50,6 +55,12 @@ export interface SystemStatus { export interface GlobalState { hasNewVersion?: boolean; + initClientDBProcess?: { costTime?: number; phase: 'wasm' | 'dependencies'; progress: number }; + /** + * 客户端数据库初始化状态 + * 启动时为 Idle,完成为 Ready,报错为 Error + */ + initClientDBStage: DatabaseLoadingState; isMobile?: boolean; isStatusInit?: boolean; latestVersion?: string; @@ -76,6 +87,7 @@ export const INITIAL_STATUS = { } satisfies SystemStatus; export const initialState: GlobalState = { + initClientDBStage: DatabaseLoadingState.Idle, isMobile: false, isStatusInit: false, sidebarKey: SidebarTabKey.Chat, diff --git a/src/store/global/selectors.ts b/src/store/global/selectors.ts index 65936ca26bb7..b80a8f340d3f 100644 --- a/src/store/global/selectors.ts +++ b/src/store/global/selectors.ts @@ -1,4 +1,6 @@ +import { isServerMode } from '@/const/version'; import { GlobalStore } from '@/store/global'; +import { DatabaseLoadingState } from '@/types/clientDB'; import { INITIAL_STATUS } from './initialState'; @@ -22,7 +24,27 @@ const filePanelWidth = (s: GlobalStore) => s.status.filePanelWidth; const inputHeight = (s: GlobalStore) => s.status.inputHeight; const threadInputHeight = (s: GlobalStore) => s.status.threadInputHeight; -const isPgliteNotEnabled = () => false; +const isPgliteNotEnabled = (s: GlobalStore) => + !isServerMode && s.isStatusInit && !s.status.isEnablePglite; + +/** + * 当且仅当 client db 模式,且 pglite 未初始化完成时返回 true + */ +const isPgliteNotInited = (s: GlobalStore) => + !isServerMode && + s.isStatusInit && + s.status.isEnablePglite && + s.initClientDBStage !== DatabaseLoadingState.Ready; + +/** + * 当且仅当 client db 模式,且 pglite 初始化完成时返回 true + */ +const isPgliteInited = (s: GlobalStore): boolean => + isServerMode || + (s.isStatusInit && + s.status.isEnablePglite && + s.initClientDBStage === DatabaseLoadingState.Ready) || + false; const isPgliteNotInited = () => false; diff --git a/src/store/global/store.ts b/src/store/global/store.ts index 5bf1ac439f19..ed3f28e9389a 100644 --- a/src/store/global/store.ts +++ b/src/store/global/store.ts @@ -5,15 +5,17 @@ import { StateCreator } from 'zustand/vanilla'; import { createDevtools } from '../middleware/createDevtools'; import { type GlobalStoreAction, globalActionSlice } from './action'; +import { type GlobalClientDBAction, clientDBSlice } from './actions/clientDb'; import { type GlobalState, initialState } from './initialState'; // =============== 聚合 createStoreFn ============ // -export type GlobalStore = GlobalState & GlobalStoreAction; +export type GlobalStore = GlobalState & GlobalStoreAction & GlobalClientDBAction; const createStore: StateCreator = (...parameters) => ({ ...initialState, ...globalActionSlice(...parameters), + ...clientDBSlice(...parameters), }); // =============== 实装 useStore ============ // diff --git a/src/store/session/slices/sessionGroup/reducer.test.ts b/src/store/session/slices/sessionGroup/reducer.test.ts index 17a947ca217a..52fe51b9e329 100644 --- a/src/store/session/slices/sessionGroup/reducer.test.ts +++ b/src/store/session/slices/sessionGroup/reducer.test.ts @@ -10,14 +10,14 @@ describe('sessionGroupsReducer', () => { { id: nanoid(), name: 'Group 1', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), }, { id: nanoid(), name: 'Group 2', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), sort: 1, }, ]; @@ -26,8 +26,8 @@ describe('sessionGroupsReducer', () => { const newItem: SessionGroupItem = { id: nanoid(), name: 'New Group', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), }; const result = sessionGroupsReducer(initialState, { diff --git a/src/types/clientDB.ts b/src/types/clientDB.ts new file mode 100644 index 000000000000..48737fc3575f --- /dev/null +++ b/src/types/clientDB.ts @@ -0,0 +1,17 @@ +// 定义加载状态类型 +export enum DatabaseLoadingState { + Idle, + Initializing, + LoadingDependencies, + LoadingWasm, + Migrating, + Ready, + Error, +} + +// 定义进度回调接口 +export interface ClientDBLoadingProgress { + costTime?: number; + phase: 'wasm' | 'dependencies'; + progress: number; +} diff --git a/src/types/files/upload.ts b/src/types/files/upload.ts index 83aa3d5be9ce..b94b69a5fdee 100644 --- a/src/types/files/upload.ts +++ b/src/types/files/upload.ts @@ -53,7 +53,6 @@ export const FileMetadataSchema = z.object({ export type FileMetadata = z.infer; export const UploadFileSchema = z.object({ - data: z.instanceof(ArrayBuffer).optional(), /** * file type * @example 'image/png' @@ -77,7 +76,6 @@ export const UploadFileSchema = z.object({ * local mean save the raw file into data * url mean upload the file to a cdn and then save the url */ - saveMode: z.enum(['local', 'url']), /** * file size */ @@ -89,3 +87,11 @@ export const UploadFileSchema = z.object({ }); export type UploadFileParams = z.infer; + +export interface CheckFileHashResult { + fileType?: string; + isExist: boolean; + metadata?: unknown; + size?: number; + url?: string; +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 0d4762a75d2a..074ec166c3a0 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -12,3 +12,12 @@ declare module 'antd-style' { declare module 'styled-components' { export interface DefaultTheme extends AntdToken, LobeCustomToken {} } + +declare global { + interface Window { + /** + * 客户端 db 模式下的用户 id + */ + __lobeClientUserId?: string; + } +} diff --git a/src/types/meta.ts b/src/types/meta.ts index 459aece85cdb..23ce2b941c2f 100644 --- a/src/types/meta.ts +++ b/src/types/meta.ts @@ -21,19 +21,10 @@ export const LobeMetaDataSchema = z.object({ export type MetaData = z.infer; export interface BaseDataModel { - /** - * @deprecated - */ - createAt?: number; - createdAt: number; id: string; meta: MetaData; - /** - * @deprecated - */ - updateAt?: number; updatedAt: number; } diff --git a/src/types/session/sessionGroup.ts b/src/types/session/sessionGroup.ts index 85fb3675021b..1c8dbcda048a 100644 --- a/src/types/session/sessionGroup.ts +++ b/src/types/session/sessionGroup.ts @@ -8,11 +8,11 @@ export enum SessionDefaultGroup { export type SessionGroupId = SessionDefaultGroup | string; export interface SessionGroupItem { - createdAt: number; + createdAt: Date; id: string; name: string; - sort?: number; - updatedAt: number; + sort?: number | null; + updatedAt: Date; } export type SessionGroups = SessionGroupItem[]; From 94b46c841a5991eaa44d65c2650db861f23a4848 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Tue, 17 Dec 2024 04:35:36 +0000 Subject: [PATCH 02/16] fix --- src/store/global/selectors.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/store/global/selectors.ts b/src/store/global/selectors.ts index b80a8f340d3f..97967d2ea14f 100644 --- a/src/store/global/selectors.ts +++ b/src/store/global/selectors.ts @@ -46,10 +46,6 @@ const isPgliteInited = (s: GlobalStore): boolean => s.initClientDBStage === DatabaseLoadingState.Ready) || false; -const isPgliteNotInited = () => false; - -const isPgliteInited = (): boolean => true; - export const systemStatusSelectors = { filePanelWidth, hidePWAInstaller, From 39ed3611475bb921dc99be9b3c1e587dc19bf395 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 18 Dec 2024 02:22:04 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=F0=9F=92=84=20style:=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20db=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/ChatList/Client.tsx | 25 ---- .../@conversation/features/ChatList/index.tsx | 4 +- src/app/(main)/chat/_layout/Desktop/index.tsx | 2 + src/app/(main)/chat/_layout/Mobile.tsx | 2 + .../chat/features/InitClientDB/index.tsx | 111 ++++++++++++++++++ 5 files changed, 117 insertions(+), 27 deletions(-) delete mode 100644 src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx create mode 100644 src/app/(main)/chat/features/InitClientDB/index.tsx diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx deleted file mode 100644 index a9c328bc6407..000000000000 --- a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/Client.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client'; - -import React, { memo } from 'react'; - -import InitClientDB from '@/features/InitClientDB'; -import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; - -import Content from './Content'; - -interface ListProps { - mobile?: boolean; -} - -const Client = memo(({ mobile }) => { - const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); - - if (isPgliteNotInited) return ; - - return ; -}); - -Client.displayName = 'ChatListClient'; - -export default Client; diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx index 82571c40f349..57c561cab0b2 100644 --- a/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/index.tsx @@ -3,7 +3,7 @@ import { Flexbox } from 'react-layout-kit'; import { SkeletonList } from '@/features/Conversation'; -const Client = lazy(() => import('./Client')); +const Content = lazy(() => import('./Content')); interface ChatListProps { mobile?: boolean; @@ -20,7 +20,7 @@ const ChatList = ({ mobile }: ChatListProps) => ( width={'100%'} > }> - + ); diff --git a/src/app/(main)/chat/_layout/Desktop/index.tsx b/src/app/(main)/chat/_layout/Desktop/index.tsx index 72e1d54950ae..145fa8d97a1c 100644 --- a/src/app/(main)/chat/_layout/Desktop/index.tsx +++ b/src/app/(main)/chat/_layout/Desktop/index.tsx @@ -1,6 +1,7 @@ import { Flexbox } from 'react-layout-kit'; import EnableClientDB from '../../features/EnableClientDB'; +import InitClientDB from '../../features/InitClientDB'; import Migration from '../../features/Migration'; import { LayoutProps } from '../type'; import SessionPanel from './SessionPanel'; @@ -21,6 +22,7 @@ const Layout = ({ children, session }: LayoutProps) => { + {/* ↓ cloud slot ↓ */} {/* ↑ cloud slot ↑ */} diff --git a/src/app/(main)/chat/_layout/Mobile.tsx b/src/app/(main)/chat/_layout/Mobile.tsx index 81fea2799d4f..2014301f6e0e 100644 --- a/src/app/(main)/chat/_layout/Mobile.tsx +++ b/src/app/(main)/chat/_layout/Mobile.tsx @@ -7,6 +7,7 @@ import { Flexbox } from 'react-layout-kit'; import Migration from '@/app/(main)/chat/features/Migration'; import { useQuery } from '@/hooks/useQuery'; +import InitClientDB from '../features/InitClientDB'; import { LayoutProps } from './type'; const useStyles = createStyles(({ css, token }) => ({ @@ -40,6 +41,7 @@ const Layout = memo(({ children, session }) => { {children} + ); }); diff --git a/src/app/(main)/chat/features/InitClientDB/index.tsx b/src/app/(main)/chat/features/InitClientDB/index.tsx new file mode 100644 index 000000000000..6558b6bedfa7 --- /dev/null +++ b/src/app/(main)/chat/features/InitClientDB/index.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { Progress } from 'antd'; +import { createStyles } from 'antd-style'; +import isEqual from 'fast-deep-equal'; +import { AnimatePresence, motion } from 'framer-motion'; +import { rgba } from 'polished'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; +import { DatabaseLoadingState } from '@/types/clientDB'; + +const useStyles = createStyles(({ css, token, prefixCls }) => ({ + bg: css` + padding-block: 8px; + padding-inline: 16px; + background: ${token.colorTextLightSolid}; + border-radius: 40px; + `, + container: css` + position: fixed; + z-index: 1000; + `, + progress: css` + .${prefixCls}-progress-text { + color: ${token.colorBgContainer} !important; + } + `, + text: css` + color: ${token.colorBgContainer}; + `, +})); + +interface InitClientDBProps { + bottom?: number; +} + +const InitClientDB = memo(({ bottom = 80 }) => { + const { styles, theme } = useStyles(); + const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); + const process = useGlobalStore((s) => s.initClientDBProcess, isEqual); + const { t } = useTranslation('common'); + const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); + const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); + + useInitClientDB(); + + const getStateMessage = (state: DatabaseLoadingState, progress?: number) => { + switch (state) { + case DatabaseLoadingState.Error: { + return t('clientDB.initing.error'); + } + case DatabaseLoadingState.Idle: { + return t('clientDB.initing.idle'); + } + case DatabaseLoadingState.Initializing: { + return t('clientDB.initing.initializing'); + } + case DatabaseLoadingState.LoadingDependencies: { + return t('clientDB.initing.loadingDependencies', { progress }); + } + + case DatabaseLoadingState.LoadingWasm: { + return t('clientDB.initing.loadingWasmModule', { progress }); + } + + case DatabaseLoadingState.Migrating: { + return t('clientDB.initing.migrating'); + } + + case DatabaseLoadingState.Ready: { + return t('clientDB.initing.ready'); + } + } + }; + + return ( + + {isPgliteNotInited && ( +
+ + + + + {getStateMessage(currentStage, process?.progress)} + + + +
+ )} +
+ ); +}); +export default InitClientDB; From 589ae13765a5d39f61627975e001363c9b2bc7f5 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 18 Dec 2024 03:12:55 +0800 Subject: [PATCH 04/16] =?UTF-8?q?=F0=9F=92=84=20style:=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20db=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/features/InitClientDB/Indicator.tsx | 132 ++++++++++++++++++ .../chat/features/InitClientDB/index.tsx | 97 +------------ src/locales/default/common.ts | 6 +- 3 files changed, 139 insertions(+), 96 deletions(-) create mode 100644 src/app/(main)/chat/features/InitClientDB/Indicator.tsx diff --git a/src/app/(main)/chat/features/InitClientDB/Indicator.tsx b/src/app/(main)/chat/features/InitClientDB/Indicator.tsx new file mode 100644 index 000000000000..a249e4a0c01e --- /dev/null +++ b/src/app/(main)/chat/features/InitClientDB/Indicator.tsx @@ -0,0 +1,132 @@ +'use client'; + +import { Progress } from 'antd'; +import { createStyles } from 'antd-style'; +import { AnimatePresence, motion } from 'framer-motion'; +import { rgba } from 'polished'; +import { memo, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { useGlobalStore } from '@/store/global'; +import { DatabaseLoadingState } from '@/types/clientDB'; + +const useStyles = createStyles(({ css, token, prefixCls }) => ({ + bg: css` + padding-block: 12px; + padding-inline: 12px 24px; + background: ${token.colorText}; + border-radius: 40px; + `, + container: css` + position: fixed; + z-index: 1000; + `, + progress: css` + .${prefixCls}-progress-text { + font-size: 12px; + color: ${token.colorBgContainer} !important; + } + `, + progressReady: css` + .${prefixCls}-progress-text { + color: ${token.colorSuccessBorder} !important; + } + `, + + text: css` + color: ${token.colorBgContainer}; + `, +})); + +interface IndicatorProps { + bottom?: number; +} + +const Indicator = memo(({ bottom = 80 }) => { + const { styles, theme, cx } = useStyles(); + const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); + const { t } = useTranslation('common'); + const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); + + useInitClientDB(); + const [shouldRender, setShouldRender] = useState(true); + + // 当状态变为 Ready 时,延迟隐藏 + useEffect(() => { + if (currentStage === DatabaseLoadingState.Ready) { + const timer = setTimeout(() => { + setShouldRender(false); + }, 800); // 延迟 800ms 后隐藏 + + return () => clearTimeout(timer); + } + }, [currentStage]); + + const getStateMessage = (state: DatabaseLoadingState) => { + switch (state) { + case DatabaseLoadingState.Ready: { + return t('clientDB.initing.ready'); + } + + case DatabaseLoadingState.Error: { + return t('clientDB.initing.error'); + } + case DatabaseLoadingState.Idle: { + return t('clientDB.initing.idle'); + } + case DatabaseLoadingState.Initializing: { + return t('clientDB.initing.initializing'); + } + case DatabaseLoadingState.LoadingDependencies: { + return t('clientDB.initing.loadingDependencies'); + } + + case DatabaseLoadingState.LoadingWasm: { + return t('clientDB.initing.loadingWasmModule'); + } + + case DatabaseLoadingState.Migrating: { + return t('clientDB.initing.migrating'); + } + } + }; + + return ( + + {shouldRender && ( +
+ + + + {getStateMessage(currentStage)} + + +
+ )} +
+ ); +}); +export default Indicator; diff --git a/src/app/(main)/chat/features/InitClientDB/index.tsx b/src/app/(main)/chat/features/InitClientDB/index.tsx index 6558b6bedfa7..ec304a472ea8 100644 --- a/src/app/(main)/chat/features/InitClientDB/index.tsx +++ b/src/app/(main)/chat/features/InitClientDB/index.tsx @@ -1,111 +1,20 @@ 'use client'; -import { Progress } from 'antd'; -import { createStyles } from 'antd-style'; -import isEqual from 'fast-deep-equal'; -import { AnimatePresence, motion } from 'framer-motion'; -import { rgba } from 'polished'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Center, Flexbox } from 'react-layout-kit'; import { useGlobalStore } from '@/store/global'; import { systemStatusSelectors } from '@/store/global/selectors'; -import { DatabaseLoadingState } from '@/types/clientDB'; -const useStyles = createStyles(({ css, token, prefixCls }) => ({ - bg: css` - padding-block: 8px; - padding-inline: 16px; - background: ${token.colorTextLightSolid}; - border-radius: 40px; - `, - container: css` - position: fixed; - z-index: 1000; - `, - progress: css` - .${prefixCls}-progress-text { - color: ${token.colorBgContainer} !important; - } - `, - text: css` - color: ${token.colorBgContainer}; - `, -})); +import Indicator from './Indicator'; interface InitClientDBProps { bottom?: number; } const InitClientDB = memo(({ bottom = 80 }) => { - const { styles, theme } = useStyles(); - const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); - const process = useGlobalStore((s) => s.initClientDBProcess, isEqual); - const { t } = useTranslation('common'); - const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); - useInitClientDB(); - - const getStateMessage = (state: DatabaseLoadingState, progress?: number) => { - switch (state) { - case DatabaseLoadingState.Error: { - return t('clientDB.initing.error'); - } - case DatabaseLoadingState.Idle: { - return t('clientDB.initing.idle'); - } - case DatabaseLoadingState.Initializing: { - return t('clientDB.initing.initializing'); - } - case DatabaseLoadingState.LoadingDependencies: { - return t('clientDB.initing.loadingDependencies', { progress }); - } - - case DatabaseLoadingState.LoadingWasm: { - return t('clientDB.initing.loadingWasmModule', { progress }); - } - - case DatabaseLoadingState.Migrating: { - return t('clientDB.initing.migrating'); - } - - case DatabaseLoadingState.Ready: { - return t('clientDB.initing.ready'); - } - } - }; - - return ( - - {isPgliteNotInited && ( -
- - - - - {getStateMessage(currentStage, process?.progress)} - - - -
- )} -
- ); + return isPgliteNotInited && ; }); + export default InitClientDB; diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 02ef5b2fb410..e2c4a756c49b 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -40,8 +40,10 @@ export default { error: '发生错误,请重试', idle: '等待初始化...', initializing: '正在初始化...', - loadingDependencies: `加载依赖中({{progress}}%)...`, - loadingWasmModule: '加载 WASM 模块中({{progress}}%)...', + loadingDependencies: `加载依赖中...`, + loadingDependenciesWithProgress: `加载依赖中({{progress}}%)...`, + loadingWasmModule: '加载 WASM 模块中...', + loadingWasmModuleWithProgress: '加载 WASM 模块中({{progress}}%)...', migrating: '执行数据表迁移...', ready: '数据库已就绪', }, From de256943ecbbcaa7834ad5f3c4448746e8cf2c64 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 18 Dec 2024 03:28:50 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=92=84=20style:=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20db=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/chat/_layout/Mobile.tsx | 6 +- .../chat/features/InitClientDB/Indicator.tsx | 132 ------------------ .../chat/features/InitClientDB/index.tsx | 107 +++++++++++++- src/database/client/db.ts | 4 + src/types/clientDB.ts | 1 + 5 files changed, 113 insertions(+), 137 deletions(-) delete mode 100644 src/app/(main)/chat/features/InitClientDB/Indicator.tsx diff --git a/src/app/(main)/chat/_layout/Mobile.tsx b/src/app/(main)/chat/_layout/Mobile.tsx index 2014301f6e0e..ca181e6be059 100644 --- a/src/app/(main)/chat/_layout/Mobile.tsx +++ b/src/app/(main)/chat/_layout/Mobile.tsx @@ -1,7 +1,7 @@ 'use client'; import { createStyles } from 'antd-style'; -import { memo } from 'react'; +import { Suspense, memo } from 'react'; import { Flexbox } from 'react-layout-kit'; import Migration from '@/app/(main)/chat/features/Migration'; @@ -41,7 +41,9 @@ const Layout = memo(({ children, session }) => { {children} - + + + ); }); diff --git a/src/app/(main)/chat/features/InitClientDB/Indicator.tsx b/src/app/(main)/chat/features/InitClientDB/Indicator.tsx deleted file mode 100644 index a249e4a0c01e..000000000000 --- a/src/app/(main)/chat/features/InitClientDB/Indicator.tsx +++ /dev/null @@ -1,132 +0,0 @@ -'use client'; - -import { Progress } from 'antd'; -import { createStyles } from 'antd-style'; -import { AnimatePresence, motion } from 'framer-motion'; -import { rgba } from 'polished'; -import { memo, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Center, Flexbox } from 'react-layout-kit'; - -import { useGlobalStore } from '@/store/global'; -import { DatabaseLoadingState } from '@/types/clientDB'; - -const useStyles = createStyles(({ css, token, prefixCls }) => ({ - bg: css` - padding-block: 12px; - padding-inline: 12px 24px; - background: ${token.colorText}; - border-radius: 40px; - `, - container: css` - position: fixed; - z-index: 1000; - `, - progress: css` - .${prefixCls}-progress-text { - font-size: 12px; - color: ${token.colorBgContainer} !important; - } - `, - progressReady: css` - .${prefixCls}-progress-text { - color: ${token.colorSuccessBorder} !important; - } - `, - - text: css` - color: ${token.colorBgContainer}; - `, -})); - -interface IndicatorProps { - bottom?: number; -} - -const Indicator = memo(({ bottom = 80 }) => { - const { styles, theme, cx } = useStyles(); - const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); - const { t } = useTranslation('common'); - const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); - - useInitClientDB(); - const [shouldRender, setShouldRender] = useState(true); - - // 当状态变为 Ready 时,延迟隐藏 - useEffect(() => { - if (currentStage === DatabaseLoadingState.Ready) { - const timer = setTimeout(() => { - setShouldRender(false); - }, 800); // 延迟 800ms 后隐藏 - - return () => clearTimeout(timer); - } - }, [currentStage]); - - const getStateMessage = (state: DatabaseLoadingState) => { - switch (state) { - case DatabaseLoadingState.Ready: { - return t('clientDB.initing.ready'); - } - - case DatabaseLoadingState.Error: { - return t('clientDB.initing.error'); - } - case DatabaseLoadingState.Idle: { - return t('clientDB.initing.idle'); - } - case DatabaseLoadingState.Initializing: { - return t('clientDB.initing.initializing'); - } - case DatabaseLoadingState.LoadingDependencies: { - return t('clientDB.initing.loadingDependencies'); - } - - case DatabaseLoadingState.LoadingWasm: { - return t('clientDB.initing.loadingWasmModule'); - } - - case DatabaseLoadingState.Migrating: { - return t('clientDB.initing.migrating'); - } - } - }; - - return ( - - {shouldRender && ( -
- - - - {getStateMessage(currentStage)} - - -
- )} -
- ); -}); -export default Indicator; diff --git a/src/app/(main)/chat/features/InitClientDB/index.tsx b/src/app/(main)/chat/features/InitClientDB/index.tsx index ec304a472ea8..ec01658db99b 100644 --- a/src/app/(main)/chat/features/InitClientDB/index.tsx +++ b/src/app/(main)/chat/features/InitClientDB/index.tsx @@ -1,20 +1,121 @@ 'use client'; +import { Progress } from 'antd'; +import { createStyles } from 'antd-style'; +import { AnimatePresence, motion } from 'framer-motion'; +import { rgba } from 'polished'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; import { useGlobalStore } from '@/store/global'; import { systemStatusSelectors } from '@/store/global/selectors'; +import { DatabaseLoadingState } from '@/types/clientDB'; -import Indicator from './Indicator'; +const useStyles = createStyles(({ css, token, prefixCls }) => ({ + bg: css` + padding-block: 12px; + padding-inline: 12px 24px; + background: ${token.colorText}; + border-radius: 40px; + `, + container: css` + position: fixed; + z-index: 1000; + `, + progress: css` + .${prefixCls}-progress-text { + font-size: 12px; + color: ${token.colorBgContainer} !important; + } + `, + progressReady: css` + .${prefixCls}-progress-text { + color: ${token.colorSuccessBorder} !important; + } + `, + + text: css` + color: ${token.colorBgContainer}; + `, +})); interface InitClientDBProps { bottom?: number; } const InitClientDB = memo(({ bottom = 80 }) => { + const { styles, theme, cx } = useStyles(); + const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); + const { t } = useTranslation('common'); + const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); + + useInitClientDB(); const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); - return isPgliteNotInited && ; -}); + const getStateMessage = (state: DatabaseLoadingState) => { + switch (state) { + case DatabaseLoadingState.Finished: { + return t('clientDB.initing.ready'); + } + + case DatabaseLoadingState.Error: { + return t('clientDB.initing.error'); + } + case DatabaseLoadingState.Idle: { + return t('clientDB.initing.idle'); + } + case DatabaseLoadingState.Initializing: { + return t('clientDB.initing.initializing'); + } + case DatabaseLoadingState.LoadingDependencies: { + return t('clientDB.initing.loadingDependencies'); + } + case DatabaseLoadingState.LoadingWasm: { + return t('clientDB.initing.loadingWasmModule'); + } + + case DatabaseLoadingState.Migrating: { + return t('clientDB.initing.migrating'); + } + } + }; + + return ( + + {isPgliteNotInited && ( +
+ + + + {getStateMessage(currentStage)} + + +
+ )} +
+ ); +}); export default InitClientDB; diff --git a/src/database/client/db.ts b/src/database/client/db.ts index d920c93b98c9..4f4af596ad5b 100644 --- a/src/database/client/db.ts +++ b/src/database/client/db.ts @@ -1,6 +1,7 @@ import type { PgliteDatabase } from 'drizzle-orm/pglite'; import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB'; +import { sleep } from '@/utils/sleep'; import * as schema from '../schemas'; import migrations from './migrations.json'; @@ -174,6 +175,9 @@ export class DatabaseManager { await this.migrate(true); + this.callbacks?.onStateChange?.(DatabaseLoadingState.Finished); + await sleep(50); + this.callbacks?.onStateChange?.(DatabaseLoadingState.Ready); return this.dbInstance as DrizzleInstance; diff --git a/src/types/clientDB.ts b/src/types/clientDB.ts index 48737fc3575f..30346f5beb9b 100644 --- a/src/types/clientDB.ts +++ b/src/types/clientDB.ts @@ -5,6 +5,7 @@ export enum DatabaseLoadingState { LoadingDependencies, LoadingWasm, Migrating, + Finished, Ready, Error, } From 061f6e19d8f8565a9e17ab1f168ed35a18feae86 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Wed, 18 Dec 2024 03:32:21 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=92=84=20style:=20improve=20?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/chat/features/InitClientDB/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(main)/chat/features/InitClientDB/index.tsx b/src/app/(main)/chat/features/InitClientDB/index.tsx index ec01658db99b..541aedb27b03 100644 --- a/src/app/(main)/chat/features/InitClientDB/index.tsx +++ b/src/app/(main)/chat/features/InitClientDB/index.tsx @@ -15,7 +15,7 @@ import { DatabaseLoadingState } from '@/types/clientDB'; const useStyles = createStyles(({ css, token, prefixCls }) => ({ bg: css` padding-block: 12px; - padding-inline: 12px 24px; + padding-inline: 20px 40px; background: ${token.colorText}; border-radius: 40px; `, @@ -106,7 +106,7 @@ const InitClientDB = memo(({ bottom = 80 }) => { : theme.colorBgContainer } strokeLinecap={'round'} - strokeWidth={12} + strokeWidth={15} trailColor={rgba(theme.colorBgContainer, 0.1)} type={'circle'} /> From 931b1667d7557ea9eb74800d5be2fcb9f6b77c56 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 02:27:17 +0000 Subject: [PATCH 07/16] fix test --- src/database/client/db.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/database/client/db.test.ts b/src/database/client/db.test.ts index 33996f04849f..fa11071d694e 100644 --- a/src/database/client/db.test.ts +++ b/src/database/client/db.test.ts @@ -65,6 +65,7 @@ describe('DatabaseManager', () => { DatabaseLoadingState.LoadingDependencies, DatabaseLoadingState.LoadingWasm, DatabaseLoadingState.Migrating, + DatabaseLoadingState.Finished, DatabaseLoadingState.Ready, ]); }); From 784bbd14d12c431d8456cba8d530807172212609 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 02:28:41 +0000 Subject: [PATCH 08/16] update locale --- locales/ar/common.json | 8 +++++--- locales/bg-BG/common.json | 8 +++++--- locales/de-DE/common.json | 8 +++++--- locales/en-US/common.json | 8 +++++--- locales/es-ES/common.json | 8 +++++--- locales/fa-IR/common.json | 8 +++++--- locales/fr-FR/common.json | 8 +++++--- locales/it-IT/common.json | 8 +++++--- locales/ja-JP/common.json | 8 +++++--- locales/ko-KR/common.json | 8 +++++--- locales/nl-NL/common.json | 8 +++++--- locales/pl-PL/common.json | 8 +++++--- locales/pt-BR/common.json | 8 +++++--- locales/ru-RU/common.json | 8 +++++--- locales/tr-TR/common.json | 8 +++++--- locales/vi-VN/common.json | 8 +++++--- locales/zh-CN/common.json | 8 +++++--- locales/zh-TW/common.json | 8 +++++--- 18 files changed, 90 insertions(+), 54 deletions(-) diff --git a/locales/ar/common.json b/locales/ar/common.json index 9061736f7270..83b4016f84ff 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -38,9 +38,11 @@ "error": "حدث خطأ، يرجى إعادة المحاولة", "idle": "في انتظار التهيئة...", "initializing": "جارٍ التهيئة...", - "loadingDependencies": "جارٍ تحميل التبعيات ({{progress}}%)...", - "loadingWasmModule": "جارٍ تحميل وحدة WASM ({{progress}}%)...", - "migrating": "جارٍ ترحيل البيانات...", + "loadingDependencies": "جارٍ تحميل الاعتماديات...", + "loadingDependenciesWithProgress": "جارٍ تحميل الاعتماديات ({{progress}}%)...", + "loadingWasmModule": "جارٍ تحميل وحدة WASM...", + "loadingWasmModuleWithProgress": "جارٍ تحميل وحدة WASM ({{progress}}%)...", + "migrating": "جارٍ تنفيذ ترحيل البيانات...", "ready": "قاعدة البيانات جاهزة" }, "modal": { diff --git a/locales/bg-BG/common.json b/locales/bg-BG/common.json index 361d7ceb3ae1..f133f549fcfe 100644 --- a/locales/bg-BG/common.json +++ b/locales/bg-BG/common.json @@ -38,9 +38,11 @@ "error": "Възникна грешка, моля опитайте отново", "idle": "Изчакване на инициализация...", "initializing": "Инициализиране...", - "loadingDependencies": "Зареждане на зависимости ({{progress}}%)...", - "loadingWasmModule": "Зареждане на WASM модула ({{progress}}%)...", - "migrating": "Мигриране на данни...", + "loadingDependencies": "Зареждане на зависимости...", + "loadingDependenciesWithProgress": "Зареждане на зависимости ({{progress}}%)...", + "loadingWasmModule": "Зареждане на WASM модула...", + "loadingWasmModuleWithProgress": "Зареждане на WASM модула ({{progress}}%)...", + "migrating": "Извършване на миграция на данни...", "ready": "Базата данни е готова" }, "modal": { diff --git a/locales/de-DE/common.json b/locales/de-DE/common.json index b0de9f1ccdd3..e0d3754cb84c 100644 --- a/locales/de-DE/common.json +++ b/locales/de-DE/common.json @@ -38,9 +38,11 @@ "error": "Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut", "idle": "Warte auf die Initialisierung...", "initializing": "Wird initialisiert...", - "loadingDependencies": "Lade Abhängigkeiten ({ {progress}}%)...", - "loadingWasmModule": "Lade WASM-Modul ({ {progress}}%)...", - "migrating": "Daten werden migriert...", + "loadingDependencies": "Abhängigkeiten werden geladen...", + "loadingDependenciesWithProgress": "Abhängigkeiten werden geladen ({{progress}}%)...", + "loadingWasmModule": "WASM-Modul wird geladen...", + "loadingWasmModuleWithProgress": "WASM-Modul wird geladen ({{progress}}%)...", + "migrating": "Datenbankmigration wird durchgeführt...", "ready": "Datenbank ist bereit" }, "modal": { diff --git a/locales/en-US/common.json b/locales/en-US/common.json index d7520f4c0d48..608e348f6527 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -38,9 +38,11 @@ "error": "An error occurred, please try again", "idle": "Waiting for initialization...", "initializing": "Initializing...", - "loadingDependencies": "Loading dependencies ({{progress}}%)...", - "loadingWasmModule": "Loading WASM module ({{progress}}%)...", - "migrating": "Migrating data...", + "loadingDependencies": "Loading dependencies...", + "loadingDependenciesWithProgress": "Loading dependencies ({{progress}}%)...", + "loadingWasmModule": "Loading WASM module...", + "loadingWasmModuleWithProgress": "Loading WASM module ({{progress}}%)...", + "migrating": "Performing database migration...", "ready": "Database is ready" }, "modal": { diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index db99cda4ff43..183850e1a535 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -38,9 +38,11 @@ "error": "Ha ocurrido un error, por favor reintente", "idle": "Esperando la inicialización...", "initializing": "Inicializando...", - "loadingDependencies": "Cargando dependencias ({{progress}}%)...", - "loadingWasmModule": "Cargando módulo WASM ({{progress}}%)...", - "migrating": "Migrando datos...", + "loadingDependencies": "Cargando dependencias...", + "loadingDependenciesWithProgress": "Cargando dependencias ({{progress}}%)...", + "loadingWasmModule": "Cargando módulo WASM...", + "loadingWasmModuleWithProgress": "Cargando módulo WASM ({{progress}}%)...", + "migrating": "Ejecutando migración de tablas de datos...", "ready": "La base de datos está lista" }, "modal": { diff --git a/locales/fa-IR/common.json b/locales/fa-IR/common.json index 4eff9772ccf9..0f097946fa50 100644 --- a/locales/fa-IR/common.json +++ b/locales/fa-IR/common.json @@ -38,9 +38,11 @@ "error": "خطایی رخ داده است، لطفاً دوباره تلاش کنید", "idle": "در حال انتظار برای راه‌اندازی...", "initializing": "در حال راه‌اندازی...", - "loadingDependencies": "در حال بارگذاری وابستگی‌ها ({{progress}}%)...", - "loadingWasmModule": "در حال بارگذاری ماژول WASM ({{progress}}%)...", - "migrating": "در حال انتقال داده‌ها...", + "loadingDependencies": "در حال بارگذاری وابستگی‌ها...", + "loadingDependenciesWithProgress": "در حال بارگذاری وابستگی‌ها ({{progress}}%)...", + "loadingWasmModule": "در حال بارگذاری ماژول WASM...", + "loadingWasmModuleWithProgress": "در حال بارگذاری ماژول WASM ({{progress}}%)...", + "migrating": "در حال انجام مهاجرت جدول داده‌ها...", "ready": "پایگاه داده آماده است" }, "modal": { diff --git a/locales/fr-FR/common.json b/locales/fr-FR/common.json index f44bbadbb90f..105d0d805574 100644 --- a/locales/fr-FR/common.json +++ b/locales/fr-FR/common.json @@ -38,9 +38,11 @@ "error": "Une erreur s'est produite, veuillez réessayer", "idle": "En attente d'initialisation...", "initializing": "Initialisation en cours...", - "loadingDependencies": "Chargement des dépendances ({{progress}}%)...", - "loadingWasmModule": "Chargement du module WASM ({{progress}}%)...", - "migrating": "Migration des données en cours...", + "loadingDependencies": "Chargement des dépendances...", + "loadingDependenciesWithProgress": "Chargement des dépendances ({{progress}}%)...", + "loadingWasmModule": "Chargement du module WASM...", + "loadingWasmModuleWithProgress": "Chargement du module WASM ({{progress}}%)...", + "migrating": "Exécution de la migration des tables de données...", "ready": "Base de données prête" }, "modal": { diff --git a/locales/it-IT/common.json b/locales/it-IT/common.json index 1233cd6a31a9..53020a14201f 100644 --- a/locales/it-IT/common.json +++ b/locales/it-IT/common.json @@ -38,9 +38,11 @@ "error": "Si è verificato un errore, si prega di riprovare", "idle": "In attesa di inizializzazione...", "initializing": "In fase di inizializzazione...", - "loadingDependencies": "Caricamento delle dipendenze ({ {progress}}%)...", - "loadingWasmModule": "Caricamento del modulo WASM ({ {progress}}%)...", - "migrating": "In fase di migrazione dei dati...", + "loadingDependencies": "Caricamento delle dipendenze in corso...", + "loadingDependenciesWithProgress": "Caricamento delle dipendenze in corso ({{progress}}%)...", + "loadingWasmModule": "Caricamento del modulo WASM in corso...", + "loadingWasmModuleWithProgress": "Caricamento del modulo WASM in corso ({{progress}}%)...", + "migrating": "Esecuzione della migrazione della tabella dati...", "ready": "Database pronto" }, "modal": { diff --git a/locales/ja-JP/common.json b/locales/ja-JP/common.json index 5b82d11414c7..306fbf8d6814 100644 --- a/locales/ja-JP/common.json +++ b/locales/ja-JP/common.json @@ -38,9 +38,11 @@ "error": "エラーが発生しました。再試行してください。", "idle": "初期化を待っています...", "initializing": "初期化中...", - "loadingDependencies": "依存関係を読み込んでいます({{progress}}%)...", - "loadingWasmModule": "WASM モジュールを読み込んでいます({{progress}}%)...", - "migrating": "データを移行中...", + "loadingDependencies": "依存関係を読み込んでいます...", + "loadingDependenciesWithProgress": "依存関係を読み込んでいます({{progress}}%)...", + "loadingWasmModule": "WASM モジュールを読み込んでいます...", + "loadingWasmModuleWithProgress": "WASM モジュールを読み込んでいます({{progress}}%)...", + "migrating": "データテーブルの移行を実行しています...", "ready": "データベースは準備完了です" }, "modal": { diff --git a/locales/ko-KR/common.json b/locales/ko-KR/common.json index e36c7aa5dbe5..6d1ebf447c8c 100644 --- a/locales/ko-KR/common.json +++ b/locales/ko-KR/common.json @@ -38,9 +38,11 @@ "error": "오류가 발생했습니다. 재시도해 주세요.", "idle": "초기화 대기 중...", "initializing": "초기화 중...", - "loadingDependencies": "의존성 로딩 중({{progress}}%)...", - "loadingWasmModule": "WASM 모듈 로딩 중({{progress}}%)...", - "migrating": "데이터 마이그레이션 중...", + "loadingDependencies": "의존성 로드 중...", + "loadingDependenciesWithProgress": "의존성 로드 중({{progress}}%)...", + "loadingWasmModule": "WASM 모듈 로드 중...", + "loadingWasmModuleWithProgress": "WASM 모듈 로드 중({{progress}}%)...", + "migrating": "데이터베이스 마이그레이션 실행 중...", "ready": "데이터베이스 준비 완료" }, "modal": { diff --git a/locales/nl-NL/common.json b/locales/nl-NL/common.json index 2a3bd1d66f62..6d31cde4955c 100644 --- a/locales/nl-NL/common.json +++ b/locales/nl-NL/common.json @@ -38,9 +38,11 @@ "error": "Er is een fout opgetreden, probeer het opnieuw", "idle": "Wachten op initialisatie...", "initializing": "Bezig met initialiseren...", - "loadingDependencies": "Afhankelijkheden laden ({{progress}}%)...", - "loadingWasmModule": "WASM-module laden ({{progress}}%)...", - "migrating": "Bezig met gegevens migreren...", + "loadingDependencies": "Afhankelijkheden laden...", + "loadingDependenciesWithProgress": "Afhankelijkheden laden ({{progress}}%)...", + "loadingWasmModule": "WASM-module laden...", + "loadingWasmModuleWithProgress": "WASM-module laden ({{progress}}%)...", + "migrating": "Gegevens migreren...", "ready": "Database is gereed" }, "modal": { diff --git a/locales/pl-PL/common.json b/locales/pl-PL/common.json index ad08b65bf3cd..7975aaaa64ef 100644 --- a/locales/pl-PL/common.json +++ b/locales/pl-PL/common.json @@ -38,9 +38,11 @@ "error": "Wystąpił błąd, proszę spróbować ponownie", "idle": "Oczekiwanie na inicjalizację...", "initializing": "Inicjalizowanie...", - "loadingDependencies": "Ładowanie zależności ({{progress}}%)...", - "loadingWasmModule": "Ładowanie modułu WASM ({{progress}}%)...", - "migrating": "Migracja danych...", + "loadingDependencies": "Ładowanie zależności...", + "loadingDependenciesWithProgress": "Ładowanie zależności ({{progress}}%)...", + "loadingWasmModule": "Ładowanie modułu WASM...", + "loadingWasmModuleWithProgress": "Ładowanie modułu WASM ({{progress}}%)...", + "migrating": "Wykonywanie migracji tabeli danych...", "ready": "Baza danych gotowa" }, "modal": { diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index a73e404015ba..0103c2d23ab9 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -38,9 +38,11 @@ "error": "Ocorreu um erro, por favor tente novamente", "idle": "Aguardando inicialização...", "initializing": "Inicializando...", - "loadingDependencies": "Carregando dependências({{progress}}%)...", - "loadingWasmModule": "Carregando módulo WASM({{progress}}%)...", - "migrating": "Migrando dados...", + "loadingDependencies": "Carregando dependências...", + "loadingDependenciesWithProgress": "Carregando dependências ({{progress}}%)...", + "loadingWasmModule": "Carregando módulo WASM...", + "loadingWasmModuleWithProgress": "Carregando módulo WASM ({{progress}}%)...", + "migrating": "Executando migração de tabela de dados...", "ready": "Banco de dados pronto" }, "modal": { diff --git a/locales/ru-RU/common.json b/locales/ru-RU/common.json index 922d55de3bec..33a85cf2a372 100644 --- a/locales/ru-RU/common.json +++ b/locales/ru-RU/common.json @@ -38,9 +38,11 @@ "error": "Произошла ошибка, пожалуйста, повторите попытку", "idle": "Ожидание инициализации...", "initializing": "Инициализация...", - "loadingDependencies": "Загрузка зависимостей ({{progress}}%)...", - "loadingWasmModule": "Загрузка модуля WASM ({{progress}}%)...", - "migrating": "Миграция данных...", + "loadingDependencies": "Загрузка зависимостей...", + "loadingDependenciesWithProgress": "Загрузка зависимостей ({{progress}}%)...", + "loadingWasmModule": "Загрузка модуля WASM...", + "loadingWasmModuleWithProgress": "Загрузка модуля WASM ({{progress}}%)...", + "migrating": "Выполнение миграции таблицы данных...", "ready": "База данных готова" }, "modal": { diff --git a/locales/tr-TR/common.json b/locales/tr-TR/common.json index 0e6ab2da8086..58f4c280ecf9 100644 --- a/locales/tr-TR/common.json +++ b/locales/tr-TR/common.json @@ -38,9 +38,11 @@ "error": "Hata oluştu, lütfen tekrar deneyin", "idle": "Başlatma bekleniyor...", "initializing": "Başlatılıyor...", - "loadingDependencies": "Bağımlılıklar yükleniyor ({{progress}}%)...", - "loadingWasmModule": "WASM modülü yükleniyor ({{progress}}%)...", - "migrating": "Veri taşınıyor...", + "loadingDependencies": "Bağlantılar yükleniyor...", + "loadingDependenciesWithProgress": "Bağlantılar yükleniyor ({{progress}}%)...", + "loadingWasmModule": "WASM modülü yükleniyor...", + "loadingWasmModuleWithProgress": "WASM modülü yükleniyor ({{progress}}%)...", + "migrating": "Veri tablosu taşınıyor...", "ready": "Veritabanı hazır" }, "modal": { diff --git a/locales/vi-VN/common.json b/locales/vi-VN/common.json index 3fc81321578e..b7df7e45c315 100644 --- a/locales/vi-VN/common.json +++ b/locales/vi-VN/common.json @@ -38,9 +38,11 @@ "error": "Đã xảy ra lỗi, vui lòng thử lại", "idle": "Đang chờ khởi tạo...", "initializing": "Đang khởi tạo...", - "loadingDependencies": "Đang tải phụ thuộc({{progress}}%)...", - "loadingWasmModule": "Đang tải mô-đun WASM({{progress}}%)...", - "migrating": "Đang di chuyển dữ liệu...", + "loadingDependencies": "Đang tải phụ thuộc...", + "loadingDependenciesWithProgress": "Đang tải phụ thuộc ({{progress}}%)...", + "loadingWasmModule": "Đang tải mô-đun WASM...", + "loadingWasmModuleWithProgress": "Đang tải mô-đun WASM ({{progress}}%)...", + "migrating": "Đang thực hiện di chuyển bảng dữ liệu...", "ready": "Cơ sở dữ liệu đã sẵn sàng" }, "modal": { diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json index 2bb3ae7a11dc..9195e3bf6c68 100644 --- a/locales/zh-CN/common.json +++ b/locales/zh-CN/common.json @@ -38,9 +38,11 @@ "error": "发生错误,请重试", "idle": "等待初始化...", "initializing": "正在初始化...", - "loadingDependencies": "加载依赖中({{progress}}%)...", - "loadingWasmModule": "加载 WASM 模块中({{progress}}%)...", - "migrating": "正在迁移数据...", + "loadingDependencies": "加载依赖中...", + "loadingDependenciesWithProgress": "加载依赖中({{progress}}%)...", + "loadingWasmModule": "加载 WASM 模块中...", + "loadingWasmModuleWithProgress": "加载 WASM 模块中({{progress}}%)...", + "migrating": "执行数据表迁移...", "ready": "数据库已就绪" }, "modal": { diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 60bf44a00455..db0f1216dee6 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -38,9 +38,11 @@ "error": "發生錯誤,請重試", "idle": "等待初始化...", "initializing": "正在初始化...", - "loadingDependencies": "加載依賴中({{progress}}%)...", - "loadingWasmModule": "加載 WASM 模塊中({{progress}}%)...", - "migrating": "正在遷移數據...", + "loadingDependencies": "載入依賴中...", + "loadingDependenciesWithProgress": "載入依賴中({{progress}}%)...", + "loadingWasmModule": "載入 WASM 模組中...", + "loadingWasmModuleWithProgress": "載入 WASM 模組中({{progress}}%)...", + "migrating": "執行資料表遷移...", "ready": "數據庫已就緒" }, "modal": { From e3630920cbc738b1a609f13b057fec628f4ecde4 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 04:54:02 +0000 Subject: [PATCH 09/16] update --- src/app/(main)/chat/_layout/Desktop/index.tsx | 2 - .../chat/features/EnableClientDB/Modal.tsx | 63 --------- .../chat/features/EnableClientDB/const.ts | 15 --- .../chat/features/EnableClientDB/index.tsx | 18 --- .../Idle.tsx => InitClientDB/EnableModal.tsx} | 74 +++++------ .../FailedModal.tsx} | 9 +- .../features/InitClientDB/InitIndicator.tsx | 123 ++++++++++++++++++ .../chat/features/InitClientDB/index.tsx | 120 +++-------------- 8 files changed, 183 insertions(+), 241 deletions(-) delete mode 100644 src/app/(main)/chat/features/EnableClientDB/Modal.tsx delete mode 100644 src/app/(main)/chat/features/EnableClientDB/const.ts delete mode 100644 src/app/(main)/chat/features/EnableClientDB/index.tsx rename src/app/(main)/chat/features/{EnableClientDB/Idle.tsx => InitClientDB/EnableModal.tsx} (51%) rename src/app/(main)/chat/features/{EnableClientDB/Failed.tsx => InitClientDB/FailedModal.tsx} (93%) create mode 100644 src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx diff --git a/src/app/(main)/chat/_layout/Desktop/index.tsx b/src/app/(main)/chat/_layout/Desktop/index.tsx index 145fa8d97a1c..b12f5acd479e 100644 --- a/src/app/(main)/chat/_layout/Desktop/index.tsx +++ b/src/app/(main)/chat/_layout/Desktop/index.tsx @@ -1,6 +1,5 @@ import { Flexbox } from 'react-layout-kit'; -import EnableClientDB from '../../features/EnableClientDB'; import InitClientDB from '../../features/InitClientDB'; import Migration from '../../features/Migration'; import { LayoutProps } from '../type'; @@ -21,7 +20,6 @@ const Layout = ({ children, session }: LayoutProps) => { - {/* ↓ cloud slot ↓ */} diff --git a/src/app/(main)/chat/features/EnableClientDB/Modal.tsx b/src/app/(main)/chat/features/EnableClientDB/Modal.tsx deleted file mode 100644 index afb2ed979cf7..000000000000 --- a/src/app/(main)/chat/features/EnableClientDB/Modal.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Icon } from '@lobehub/ui'; -import { Button, Result } from 'antd'; -import { CheckCircle, CpuIcon } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import DataStyleModal from '@/components/DataStyleModal'; -import InitProgress from '@/features/InitClientDB/InitProgress'; -import { useGlobalStore } from '@/store/global'; -import { DatabaseLoadingState } from '@/types/clientDB'; - -import Failed from './Failed'; -import Idle from './Idle'; - -interface EnableClientDBModalProps { - open: boolean; -} - -const EnableClientDBModal = memo(({ open }) => { - const { t } = useTranslation('common'); - - const [initClientDBStage, markPgliteEnabled] = useGlobalStore((s) => [ - s.initClientDBStage, - s.markPgliteEnabled, - ]); - - const renderContent = () => { - if (initClientDBStage === DatabaseLoadingState.Idle) return ; - - if (initClientDBStage === DatabaseLoadingState.Ready) - return ( - - {t('clientDB.ready.button')} - - } - icon={} - status={'success'} - style={{ paddingBlock: 24, paddingTop: 0 }} - subTitle={t('clientDB.ready.desc')} - title={t('clientDB.ready.title')} - /> - ); - - if (initClientDBStage === DatabaseLoadingState.Error) return ; - - return ; - }; - - return ( - - {renderContent()} - - ); -}); - -export default EnableClientDBModal; diff --git a/src/app/(main)/chat/features/EnableClientDB/const.ts b/src/app/(main)/chat/features/EnableClientDB/const.ts deleted file mode 100644 index 2006283d4407..000000000000 --- a/src/app/(main)/chat/features/EnableClientDB/const.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const MIGRATE_KEY = 'migrated'; -export enum UpgradeStatus { - START, - UPGRADING, - UPGRADED, - UPGRADE_FAILED, -} - -export const V1DB_NAME = 'LobeHub'; -export const V1DB_TABLE_NAME = 'LOBE_CHAT'; - -export interface MigrationError { - message: string; - stack: string; -} diff --git a/src/app/(main)/chat/features/EnableClientDB/index.tsx b/src/app/(main)/chat/features/EnableClientDB/index.tsx deleted file mode 100644 index dd9eff20066b..000000000000 --- a/src/app/(main)/chat/features/EnableClientDB/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Spin } from 'antd'; -import dynamic from 'next/dynamic'; -import { memo } from 'react'; - -import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; - -const Modal = dynamic(() => import('./Modal'), { loading: () => , ssr: false }); - -const Migration = memo(() => { - const showModal = useGlobalStore(systemStatusSelectors.isPgliteNotEnabled); - - return showModal && ; -}); - -export default Migration; diff --git a/src/app/(main)/chat/features/EnableClientDB/Idle.tsx b/src/app/(main)/chat/features/InitClientDB/EnableModal.tsx similarity index 51% rename from src/app/(main)/chat/features/EnableClientDB/Idle.tsx rename to src/app/(main)/chat/features/InitClientDB/EnableModal.tsx index 596e9e7a1769..8f7ea275a21d 100644 --- a/src/app/(main)/chat/features/EnableClientDB/Idle.tsx +++ b/src/app/(main)/chat/features/InitClientDB/EnableModal.tsx @@ -1,11 +1,12 @@ import { Icon } from '@lobehub/ui'; import { Button } from 'antd'; import { createStyles } from 'antd-style'; -import { Database, SearchCheck, Zap } from 'lucide-react'; -import { useState } from 'react'; +import { CpuIcon, Database, SearchCheck, Zap } from 'lucide-react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Center, Flexbox } from 'react-layout-kit'; +import DataStyleModal from '@/components/DataStyleModal'; import { useGlobalStore } from '@/store/global'; const useStyles = createStyles(({ css, token, isDarkMode, responsive }) => ({ @@ -46,11 +47,14 @@ const useStyles = createStyles(({ css, token, isDarkMode, responsive }) => ({ `, })); -const Idle = () => { +interface EnableClientDBModalProps { + open: boolean; +} + +const EnableClientDBModal = memo(({ open }) => { const { t } = useTranslation('common'); const { styles } = useStyles(); - const [initializeClientDB] = useGlobalStore((s) => [s.initializeClientDB]); - const [loading, setLoading] = useState(false); + const markPgliteEnabled = useGlobalStore((s) => s.markPgliteEnabled); const features = [ { avatar: Database, @@ -70,42 +74,36 @@ const Idle = () => { ]; return ( -
- - - {t('clientDB.modal.desc')} + +
+ + + {t('clientDB.modal.desc')} + - - - {features.map((item) => ( - -
- -
- -

{item.title}

-

{item.desc}

+ + {features.map((item) => ( + +
+ +
+ +

{item.title}

+

{item.desc}

+
+ ))} +
+ + + - ))} - - - - - -
+
+ ); -}; +}); -export default Idle; +export default EnableClientDBModal; diff --git a/src/app/(main)/chat/features/EnableClientDB/Failed.tsx b/src/app/(main)/chat/features/InitClientDB/FailedModal.tsx similarity index 93% rename from src/app/(main)/chat/features/EnableClientDB/Failed.tsx rename to src/app/(main)/chat/features/InitClientDB/FailedModal.tsx index eb40760adb53..99d4df553185 100644 --- a/src/app/(main)/chat/features/EnableClientDB/Failed.tsx +++ b/src/app/(main)/chat/features/InitClientDB/FailedModal.tsx @@ -11,12 +11,15 @@ import { GITHUB_ISSUES } from '@/const/url'; import { githubService } from '@/services/github'; import { useGlobalStore } from '@/store/global'; -import { MigrationError } from './const'; +interface MigrationError { + message: string; + stack: string; +} interface FailedProps { error?: MigrationError; } -const Failed = memo(({ error }) => { +const FailedModal = memo(({ error }) => { const { t } = useTranslation('common'); const [initializeClientDB] = useGlobalStore((s) => [s.initializeClientDB]); @@ -79,4 +82,4 @@ const Failed = memo(({ error }) => { ); }); -export default Failed; +export default FailedModal; diff --git a/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx b/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx new file mode 100644 index 000000000000..caf58c765bf1 --- /dev/null +++ b/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { Progress } from 'antd'; +import { createStyles } from 'antd-style'; +import { AnimatePresence, motion } from 'framer-motion'; +import { rgba } from 'polished'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; + +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; +import { DatabaseLoadingState } from '@/types/clientDB'; + +const useStyles = createStyles(({ css, token, prefixCls }) => ({ + bg: css` + padding-block: 12px; + padding-inline: 20px 40px; + background: ${token.colorText}; + border-radius: 40px; + `, + container: css` + position: fixed; + z-index: 1000; + `, + progress: css` + .${prefixCls}-progress-text { + font-size: 12px; + color: ${token.colorBgContainer} !important; + } + `, + progressReady: css` + .${prefixCls}-progress-text { + color: ${token.colorSuccessBorder} !important; + } + `, + + text: css` + font-size: 16px; + color: ${token.colorBgContainer}; + `, +})); + +interface InitClientDBProps { + bottom?: number; +} + +const InitClientDB = memo(({ bottom = 80 }) => { + const { styles, theme, cx } = useStyles(); + const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); + const { t } = useTranslation('common'); + const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); + const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); + + useInitClientDB(); + + const getStateMessage = (state: DatabaseLoadingState) => { + switch (state) { + case DatabaseLoadingState.Finished: { + return t('clientDB.initing.ready'); + } + + case DatabaseLoadingState.Error: { + return t('clientDB.initing.error'); + } + case DatabaseLoadingState.Idle: { + return t('clientDB.initing.idle'); + } + case DatabaseLoadingState.Initializing: { + return t('clientDB.initing.initializing'); + } + case DatabaseLoadingState.LoadingDependencies: { + return t('clientDB.initing.loadingDependencies'); + } + + case DatabaseLoadingState.LoadingWasm: { + return t('clientDB.initing.loadingWasmModule'); + } + + case DatabaseLoadingState.Migrating: { + return t('clientDB.initing.migrating'); + } + } + }; + + return ( + + {isPgliteNotInited && true && ( +
+ + + percent} + percent={parseInt(((currentStage / DatabaseLoadingState.Ready) * 100).toFixed(0))} + size={40} + strokeColor={ + currentStage === DatabaseLoadingState.Finished + ? theme.colorSuccessActive + : theme.colorBgContainer + } + strokeLinecap={'round'} + strokeWidth={12} + trailColor={rgba(theme.colorBgContainer, 0.1)} + type={'circle'} + /> + {getStateMessage(currentStage)} + + +
+ )} +
+ ); +}); +export default InitClientDB; diff --git a/src/app/(main)/chat/features/InitClientDB/index.tsx b/src/app/(main)/chat/features/InitClientDB/index.tsx index 541aedb27b03..c7cb656e5de0 100644 --- a/src/app/(main)/chat/features/InitClientDB/index.tsx +++ b/src/app/(main)/chat/features/InitClientDB/index.tsx @@ -1,121 +1,37 @@ 'use client'; -import { Progress } from 'antd'; -import { createStyles } from 'antd-style'; -import { AnimatePresence, motion } from 'framer-motion'; -import { rgba } from 'polished'; +import { Spin } from 'antd'; +import dynamic from 'next/dynamic'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Center, Flexbox } from 'react-layout-kit'; import { useGlobalStore } from '@/store/global'; import { systemStatusSelectors } from '@/store/global/selectors'; -import { DatabaseLoadingState } from '@/types/clientDB'; -const useStyles = createStyles(({ css, token, prefixCls }) => ({ - bg: css` - padding-block: 12px; - padding-inline: 20px 40px; - background: ${token.colorText}; - border-radius: 40px; - `, - container: css` - position: fixed; - z-index: 1000; - `, - progress: css` - .${prefixCls}-progress-text { - font-size: 12px; - color: ${token.colorBgContainer} !important; - } - `, - progressReady: css` - .${prefixCls}-progress-text { - color: ${token.colorSuccessBorder} !important; - } - `, +const Modal = dynamic(() => import('./EnableModal'), { + loading: () => , + ssr: false, +}); - text: css` - color: ${token.colorBgContainer}; - `, -})); +const InitIndicator = dynamic(() => import('./InitIndicator'), { + ssr: false, +}); interface InitClientDBProps { bottom?: number; } -const InitClientDB = memo(({ bottom = 80 }) => { - const { styles, theme, cx } = useStyles(); - const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); - const { t } = useTranslation('common'); - const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); - - useInitClientDB(); +const InitClientDB = memo(({ bottom }) => { + const isPgliteNotEnabled = useGlobalStore(systemStatusSelectors.isPgliteNotEnabled); const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); - const getStateMessage = (state: DatabaseLoadingState) => { - switch (state) { - case DatabaseLoadingState.Finished: { - return t('clientDB.initing.ready'); - } - - case DatabaseLoadingState.Error: { - return t('clientDB.initing.error'); - } - case DatabaseLoadingState.Idle: { - return t('clientDB.initing.idle'); - } - case DatabaseLoadingState.Initializing: { - return t('clientDB.initing.initializing'); - } - case DatabaseLoadingState.LoadingDependencies: { - return t('clientDB.initing.loadingDependencies'); - } - - case DatabaseLoadingState.LoadingWasm: { - return t('clientDB.initing.loadingWasmModule'); - } - - case DatabaseLoadingState.Migrating: { - return t('clientDB.initing.migrating'); - } - } - }; - return ( - - {isPgliteNotInited && ( -
- - - - {getStateMessage(currentStage)} - - -
- )} -
+ <> + {/* 当用户没有设置启用 pglite 时,强弹窗引导用户来开启弹窗 */} + {isPgliteNotEnabled && } + {/* 当用户已经启用 pglite 但没有初始化时,展示初始化指示器 */} + {isPgliteNotInited && } + ); }); + export default InitClientDB; From 782e169018ee738982f9514793d09f6b778cdc5e Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 06:16:28 +0000 Subject: [PATCH 10/16] fix style --- .../features/InitClientDB/InitIndicator.tsx | 20 +++++++++---------- .../chat/features/InitClientDB/index.tsx | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx b/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx index caf58c765bf1..7daec9675f0f 100644 --- a/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx +++ b/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx @@ -9,13 +9,12 @@ import { useTranslation } from 'react-i18next'; import { Center, Flexbox } from 'react-layout-kit'; import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; import { DatabaseLoadingState } from '@/types/clientDB'; const useStyles = createStyles(({ css, token, prefixCls }) => ({ bg: css` - padding-block: 12px; - padding-inline: 20px 40px; + padding-block: 8px; + padding-inline: 8px 32px; background: ${token.colorText}; border-radius: 40px; `, @@ -36,27 +35,28 @@ const useStyles = createStyles(({ css, token, prefixCls }) => ({ `, text: css` - font-size: 16px; + font-size: 15px; color: ${token.colorBgContainer}; `, })); interface InitClientDBProps { bottom?: number; + show: boolean; } -const InitClientDB = memo(({ bottom = 80 }) => { +const InitClientDB = memo(({ bottom = 80, show }) => { const { styles, theme, cx } = useStyles(); const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); const { t } = useTranslation('common'); const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); - const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); useInitClientDB(); const getStateMessage = (state: DatabaseLoadingState) => { switch (state) { - case DatabaseLoadingState.Finished: { + case DatabaseLoadingState.Finished: + case DatabaseLoadingState.Ready: { return t('clientDB.initing.ready'); } @@ -85,7 +85,7 @@ const InitClientDB = memo(({ bottom = 80 }) => { return ( - {isPgliteNotInited && true && ( + {show && (
(({ bottom = 80 }) => { currentStage === DatabaseLoadingState.Finished && styles.progressReady, )} format={(percent) => percent} - percent={parseInt(((currentStage / DatabaseLoadingState.Ready) * 100).toFixed(0))} + percent={parseInt(((currentStage / DatabaseLoadingState.Finished) * 100).toFixed(0))} size={40} strokeColor={ currentStage === DatabaseLoadingState.Finished @@ -108,7 +108,7 @@ const InitClientDB = memo(({ bottom = 80 }) => { : theme.colorBgContainer } strokeLinecap={'round'} - strokeWidth={12} + strokeWidth={10} trailColor={rgba(theme.colorBgContainer, 0.1)} type={'circle'} /> diff --git a/src/app/(main)/chat/features/InitClientDB/index.tsx b/src/app/(main)/chat/features/InitClientDB/index.tsx index c7cb656e5de0..19064e475cfc 100644 --- a/src/app/(main)/chat/features/InitClientDB/index.tsx +++ b/src/app/(main)/chat/features/InitClientDB/index.tsx @@ -29,7 +29,7 @@ const InitClientDB = memo(({ bottom }) => { {/* 当用户没有设置启用 pglite 时,强弹窗引导用户来开启弹窗 */} {isPgliteNotEnabled && } {/* 当用户已经启用 pglite 但没有初始化时,展示初始化指示器 */} - {isPgliteNotInited && } + {isPgliteNotInited && } ); }); From 5c90ae537bcff2d04044a191fa91e88f5b802db9 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 07:07:57 +0000 Subject: [PATCH 11/16] perf: cache schema hash --- src/database/client/db.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/database/client/db.ts b/src/database/client/db.ts index 4f4af596ad5b..4c3516d8d5d2 100644 --- a/src/database/client/db.ts +++ b/src/database/client/db.ts @@ -1,4 +1,5 @@ import type { PgliteDatabase } from 'drizzle-orm/pglite'; +import { Md5 } from 'ts-md5'; import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB'; import { sleep } from '@/utils/sleep'; @@ -6,6 +7,8 @@ import { sleep } from '@/utils/sleep'; import * as schema from '../schemas'; import migrations from './migrations.json'; +const pgliteSchemaHashCache = 'LOBE_CHAT_PGLITE_SCHEMA_HASH'; + type DrizzleInstance = PgliteDatabase; export interface DatabaseLoadingCallbacks { @@ -127,6 +130,15 @@ export class DatabaseManager { private async migrate(skipMultiRun = false): Promise { if (this.isLocalDBSchemaSynced && skipMultiRun) return this.db; + const cacheHash = localStorage.getItem(pgliteSchemaHashCache); + const hash = Md5.hashStr(JSON.stringify(migrations)); + + // if hash is the same, no need to migrate + if (hash === cacheHash) { + this.isLocalDBSchemaSynced = true; + return this.db; + } + const start = Date.now(); try { this.callbacks?.onStateChange?.(DatabaseLoadingState.Migrating); @@ -134,9 +146,10 @@ export class DatabaseManager { // refs: https://github.com/drizzle-team/drizzle-orm/discussions/2532 // @ts-expect-error await this.db.dialect.migrate(migrations, this.db.session, {}); + localStorage.setItem(pgliteSchemaHashCache, hash); this.isLocalDBSchemaSynced = true; - console.info(`✅ Local database ready in ${Date.now() - start}ms`); + console.info(`🗂 Migration success, take ${Date.now() - start}ms`); } catch (cause) { console.error('❌ Local database schema migration failed', cause); throw cause; @@ -155,6 +168,7 @@ export class DatabaseManager { try { if (this.dbInstance) return this.dbInstance; + const time = Date.now(); // 初始化数据库 this.callbacks?.onStateChange?.(DatabaseLoadingState.Initializing); @@ -176,6 +190,8 @@ export class DatabaseManager { await this.migrate(true); this.callbacks?.onStateChange?.(DatabaseLoadingState.Finished); + console.log(`✅ Database initialized in ${Date.now() - time}ms`); + await sleep(50); this.callbacks?.onStateChange?.(DatabaseLoadingState.Ready); From 2a4e52c95d98ede461a9fa51671b79e373554010 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 09:14:12 +0000 Subject: [PATCH 12/16] refactor to improve home loading --- locales/ar/common.json | 10 +- locales/bg-BG/common.json | 10 +- locales/de-DE/common.json | 10 +- locales/en-US/common.json | 10 +- locales/es-ES/common.json | 10 +- locales/fa-IR/common.json | 10 +- locales/fr-FR/common.json | 10 +- locales/it-IT/common.json | 10 +- locales/ja-JP/common.json | 10 +- locales/ko-KR/common.json | 10 +- locales/nl-NL/common.json | 10 +- locales/pl-PL/common.json | 10 +- locales/pt-BR/common.json | 10 +- src/app/loading/Content.tsx | 25 ---- src/app/loading/InitClientDB.tsx | 25 ---- src/app/loading/Redirect.tsx | 70 ----------- src/app/loading/type.ts | 6 - src/features/InitClientDB/InitProgress.tsx | 132 --------------------- src/features/InitClientDB/index.tsx | 24 ---- 19 files changed, 91 insertions(+), 321 deletions(-) delete mode 100644 src/app/loading/Content.tsx delete mode 100644 src/app/loading/InitClientDB.tsx delete mode 100644 src/app/loading/Redirect.tsx delete mode 100644 src/app/loading/type.ts delete mode 100644 src/features/InitClientDB/InitProgress.tsx delete mode 100644 src/features/InitClientDB/index.tsx diff --git a/locales/ar/common.json b/locales/ar/common.json index 83b4016f84ff..4fabd9b6752b 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "جارٍ تشغيل التطبيق...", + "finished": "تم الانتهاء من تهيئة قاعدة البيانات", "goToChat": "(4/4) جارٍ تحميل صفحة الدردشة...", "initAuth": "(2/4) جاري تهيئة خدمة المصادقة...", "initUser": "(3/4) جاري تهيئة حالة المستخدم...", - "initializing": "(1/4) جارٍ تشغيل التطبيق..." + "initializing": "(1/4) جارٍ تشغيل التطبيق...", + "loadingDependencies": "جارٍ تهيئة الاعتمادات...", + "loadingWasm": "جارٍ تحميل وحدة WASM...", + "migrating": "جارٍ تنفيذ ترحيل الجداول...", + "ready": "قاعدة البيانات جاهزة" }, "autoGenerate": "توليد تلقائي", "autoGenerateTooltip": "إكمال تلقائي بناءً على الكلمات المقترحة لوصف المساعد", @@ -39,9 +45,7 @@ "idle": "في انتظار التهيئة...", "initializing": "جارٍ التهيئة...", "loadingDependencies": "جارٍ تحميل الاعتماديات...", - "loadingDependenciesWithProgress": "جارٍ تحميل الاعتماديات ({{progress}}%)...", "loadingWasmModule": "جارٍ تحميل وحدة WASM...", - "loadingWasmModuleWithProgress": "جارٍ تحميل وحدة WASM ({{progress}}%)...", "migrating": "جارٍ تنفيذ ترحيل البيانات...", "ready": "قاعدة البيانات جاهزة" }, diff --git a/locales/bg-BG/common.json b/locales/bg-BG/common.json index f133f549fcfe..852bab12e329 100644 --- a/locales/bg-BG/common.json +++ b/locales/bg-BG/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Приложението се стартира...", + "finished": "Инициализацията на базата данни е завършена", "goToChat": "(4/4) Зареждане на страницата за разговори...", "initAuth": "(2/4) Инициализиране на услугата за удостоверяване...", "initUser": "(3/4) Инициализиране на състоянието на потребителя...", - "initializing": "(1/4) Стартиране на приложението..." + "initializing": "(1/4) Стартиране на приложението...", + "loadingDependencies": "Инициализиране на зависимостите...", + "loadingWasm": "Зареждане на WASM модула...", + "migrating": "Извършване на миграция на таблиците...", + "ready": "Базата данни е готова" }, "autoGenerate": "Автоматично генериране", "autoGenerateTooltip": "Автоматично генериране на описание на агент въз основа на подкани", @@ -39,9 +45,7 @@ "idle": "Изчакване на инициализация...", "initializing": "Инициализиране...", "loadingDependencies": "Зареждане на зависимости...", - "loadingDependenciesWithProgress": "Зареждане на зависимости ({{progress}}%)...", "loadingWasmModule": "Зареждане на WASM модула...", - "loadingWasmModuleWithProgress": "Зареждане на WASM модула ({{progress}}%)...", "migrating": "Извършване на миграция на данни...", "ready": "Базата данни е готова" }, diff --git a/locales/de-DE/common.json b/locales/de-DE/common.json index e0d3754cb84c..95ffe36f245e 100644 --- a/locales/de-DE/common.json +++ b/locales/de-DE/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Anwendung wird gestartet...", + "finished": "Datenbankinitialisierung abgeschlossen", "goToChat": "(4/4) Lade die Chat-Seite...", "initAuth": "(2/4) Authentifizierungsdienst wird initialisiert...", "initUser": "(3/4) Benutzerstatus wird initialisiert...", - "initializing": "(1/4) Anwendung wird gestartet..." + "initializing": "(1/4) Anwendung wird gestartet...", + "loadingDependencies": "Abhängigkeiten werden initialisiert...", + "loadingWasm": "WASM-Module werden geladen...", + "migrating": "Datenbankmigration wird durchgeführt...", + "ready": "Datenbank ist bereit" }, "autoGenerate": "Automatisch generieren", "autoGenerateTooltip": "Assistentenbeschreibung automatisch auf Basis von Vorschlägen vervollständigen", @@ -39,9 +45,7 @@ "idle": "Warte auf die Initialisierung...", "initializing": "Wird initialisiert...", "loadingDependencies": "Abhängigkeiten werden geladen...", - "loadingDependenciesWithProgress": "Abhängigkeiten werden geladen ({{progress}}%)...", "loadingWasmModule": "WASM-Modul wird geladen...", - "loadingWasmModuleWithProgress": "WASM-Modul wird geladen ({{progress}}%)...", "migrating": "Datenbankmigration wird durchgeführt...", "ready": "Datenbank ist bereit" }, diff --git a/locales/en-US/common.json b/locales/en-US/common.json index 608e348f6527..d01e9d868b2b 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Application is starting...", + "finished": "Database initialization completed", "goToChat": "(4/4) Loading chat page...", "initAuth": "(2/4) Initializing authentication service...", "initUser": "(3/4) Initializing user status...", - "initializing": "(1/4) Starting up application..." + "initializing": "(1/4) Starting up application...", + "loadingDependencies": "Initializing dependencies...", + "loadingWasm": "Loading WASM module...", + "migrating": "Performing database migration...", + "ready": "Database is ready" }, "autoGenerate": "Auto Generate", "autoGenerateTooltip": "Auto-generate assistant description based on prompts", @@ -39,9 +45,7 @@ "idle": "Waiting for initialization...", "initializing": "Initializing...", "loadingDependencies": "Loading dependencies...", - "loadingDependenciesWithProgress": "Loading dependencies ({{progress}}%)...", "loadingWasmModule": "Loading WASM module...", - "loadingWasmModuleWithProgress": "Loading WASM module ({{progress}}%)...", "migrating": "Performing database migration...", "ready": "Database is ready" }, diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 183850e1a535..ecfe02008357 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Iniciando la aplicación...", + "finished": "Inicialización de la base de datos completada", "goToChat": "(4/4) Cargando la página de chat...", "initAuth": "(2/4) Inicializando el servicio de autenticación...", "initUser": "(3/4) Inicializando el estado del usuario...", - "initializing": "(1/4) Iniciando la aplicación..." + "initializing": "(1/4) Iniciando la aplicación...", + "loadingDependencies": "Inicializando dependencias...", + "loadingWasm": "Cargando módulo WASM...", + "migrating": "Ejecutando migración de tablas de datos...", + "ready": "Base de datos lista" }, "autoGenerate": "Generación automática", "autoGenerateTooltip": "Completar automáticamente la descripción del asistente basándose en las sugerencias", @@ -39,9 +45,7 @@ "idle": "Esperando la inicialización...", "initializing": "Inicializando...", "loadingDependencies": "Cargando dependencias...", - "loadingDependenciesWithProgress": "Cargando dependencias ({{progress}}%)...", "loadingWasmModule": "Cargando módulo WASM...", - "loadingWasmModuleWithProgress": "Cargando módulo WASM ({{progress}}%)...", "migrating": "Ejecutando migración de tablas de datos...", "ready": "La base de datos está lista" }, diff --git a/locales/fa-IR/common.json b/locales/fa-IR/common.json index 0f097946fa50..b24b72be6cee 100644 --- a/locales/fa-IR/common.json +++ b/locales/fa-IR/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "در حال راه‌اندازی برنامه...", + "finished": "راه‌اندازی پایگاه داده کامل شد", "goToChat": "(4/4) در حال بارگذاری صفحه گفتگو...", "initAuth": "(2/4) در حال راه‌اندازی سرویس احراز هویت...", "initUser": "(3/4) در حال راه‌اندازی وضعیت کاربر...", - "initializing": "(1/4) در حال راه‌اندازی برنامه..." + "initializing": "(1/4) در حال راه‌اندازی برنامه...", + "loadingDependencies": "در حال بارگذاری وابستگی‌ها...", + "loadingWasm": "در حال بارگذاری ماژول WASM...", + "migrating": "در حال اجرای مهاجرت جداول داده...", + "ready": "پایگاه داده آماده است" }, "autoGenerate": "تکمیل خودکار", "autoGenerateTooltip": "تکمیل خودکار توضیحات دستیار بر اساس کلمات راهنما", @@ -39,9 +45,7 @@ "idle": "در حال انتظار برای راه‌اندازی...", "initializing": "در حال راه‌اندازی...", "loadingDependencies": "در حال بارگذاری وابستگی‌ها...", - "loadingDependenciesWithProgress": "در حال بارگذاری وابستگی‌ها ({{progress}}%)...", "loadingWasmModule": "در حال بارگذاری ماژول WASM...", - "loadingWasmModuleWithProgress": "در حال بارگذاری ماژول WASM ({{progress}}%)...", "migrating": "در حال انجام مهاجرت جدول داده‌ها...", "ready": "پایگاه داده آماده است" }, diff --git a/locales/fr-FR/common.json b/locales/fr-FR/common.json index 105d0d805574..caa8093002a0 100644 --- a/locales/fr-FR/common.json +++ b/locales/fr-FR/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "L'application se charge...", + "finished": "Initialisation de la base de données terminée", "goToChat": "(4/4) Chargement de la page de conversation...", "initAuth": "(2/4) Initialisation du service d'authentification...", "initUser": "(3/4) Initialisation de l'état de l'utilisateur...", - "initializing": "(1/4) L'application se lance..." + "initializing": "(1/4) L'application se lance...", + "loadingDependencies": "Chargement des dépendances...", + "loadingWasm": "Chargement du module WASM...", + "migrating": "Exécution de la migration des tables de données...", + "ready": "La base de données est prête" }, "autoGenerate": "Générer automatiquement", "autoGenerateTooltip": "Générer automatiquement la description de l'agent basée sur les suggestions", @@ -39,9 +45,7 @@ "idle": "En attente d'initialisation...", "initializing": "Initialisation en cours...", "loadingDependencies": "Chargement des dépendances...", - "loadingDependenciesWithProgress": "Chargement des dépendances ({{progress}}%)...", "loadingWasmModule": "Chargement du module WASM...", - "loadingWasmModuleWithProgress": "Chargement du module WASM ({{progress}}%)...", "migrating": "Exécution de la migration des tables de données...", "ready": "Base de données prête" }, diff --git a/locales/it-IT/common.json b/locales/it-IT/common.json index 53020a14201f..06e2ab71f721 100644 --- a/locales/it-IT/common.json +++ b/locales/it-IT/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Avvio dell'app in corso...", + "finished": "Inizializzazione del database completata", "goToChat": "(4/4) Caricamento della pagina della chat in corso...", "initAuth": "(2/4) Inizializzazione del servizio di autenticazione...", "initUser": "(3/4) Inizializzazione dello stato utente...", - "initializing": "(1/4) Avvio dell'app in corso..." + "initializing": "(1/4) Avvio dell'app in corso...", + "loadingDependencies": "Inizializzazione delle dipendenze...", + "loadingWasm": "Caricamento del modulo WASM...", + "migrating": "Esecuzione della migrazione delle tabelle dati...", + "ready": "Database pronto" }, "autoGenerate": "Generazione automatica", "autoGenerateTooltip": "Completamento automatico basato su suggerimenti", @@ -39,9 +45,7 @@ "idle": "In attesa di inizializzazione...", "initializing": "In fase di inizializzazione...", "loadingDependencies": "Caricamento delle dipendenze in corso...", - "loadingDependenciesWithProgress": "Caricamento delle dipendenze in corso ({{progress}}%)...", "loadingWasmModule": "Caricamento del modulo WASM in corso...", - "loadingWasmModuleWithProgress": "Caricamento del modulo WASM in corso ({{progress}}%)...", "migrating": "Esecuzione della migrazione della tabella dati...", "ready": "Database pronto" }, diff --git a/locales/ja-JP/common.json b/locales/ja-JP/common.json index 306fbf8d6814..74540fb47e1d 100644 --- a/locales/ja-JP/common.json +++ b/locales/ja-JP/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "アプリケーションを起動しています...", + "finished": "データベースの初期化が完了しました", "goToChat": "(4/4) チャットページを読み込んでいます...", "initAuth": "(2/4) 認証サービスの初期化...", "initUser": "(3/4) ユーザー状態の初期化...", - "initializing": "(1/4) アプリを起動しています..." + "initializing": "(1/4) アプリを起動しています...", + "loadingDependencies": "依存関係を初期化しています...", + "loadingWasm": "WASM モジュールを読み込んでいます...", + "migrating": "データテーブルの移行を実行しています...", + "ready": "データベースは準備完了です" }, "autoGenerate": "自動生成", "autoGenerateTooltip": "ヒントに基づいてエージェントの説明を自動生成します", @@ -39,9 +45,7 @@ "idle": "初期化を待っています...", "initializing": "初期化中...", "loadingDependencies": "依存関係を読み込んでいます...", - "loadingDependenciesWithProgress": "依存関係を読み込んでいます({{progress}}%)...", "loadingWasmModule": "WASM モジュールを読み込んでいます...", - "loadingWasmModuleWithProgress": "WASM モジュールを読み込んでいます({{progress}}%)...", "migrating": "データテーブルの移行を実行しています...", "ready": "データベースは準備完了です" }, diff --git a/locales/ko-KR/common.json b/locales/ko-KR/common.json index 6d1ebf447c8c..62aab301c915 100644 --- a/locales/ko-KR/common.json +++ b/locales/ko-KR/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "앱을 초기화하는 중...", + "finished": "데이터베이스 초기화 완료", "goToChat": "(4/4) 대화 페이지 로딩 중...", "initAuth": "(2/4) 인증 서비스 초기화 중...", "initUser": "(3/4) 사용자 상태 초기화 중...", - "initializing": "(1/4) 애플리케이션 시작 중..." + "initializing": "(1/4) 애플리케이션 시작 중...", + "loadingDependencies": "의존성 초기화 중...", + "loadingWasm": "WASM 모듈 로드 중...", + "migrating": "데이터베이스 테이블 마이그레이션 중...", + "ready": "데이터베이스 준비 완료" }, "autoGenerate": "자동 생성", "autoGenerateTooltip": "힌트 단어를 기반으로 에이전트 설명을 자동으로 완성합니다", @@ -39,9 +45,7 @@ "idle": "초기화 대기 중...", "initializing": "초기화 중...", "loadingDependencies": "의존성 로드 중...", - "loadingDependenciesWithProgress": "의존성 로드 중({{progress}}%)...", "loadingWasmModule": "WASM 모듈 로드 중...", - "loadingWasmModuleWithProgress": "WASM 모듈 로드 중({{progress}}%)...", "migrating": "데이터베이스 마이그레이션 실행 중...", "ready": "데이터베이스 준비 완료" }, diff --git a/locales/nl-NL/common.json b/locales/nl-NL/common.json index 6d31cde4955c..a5fb8a8b20ef 100644 --- a/locales/nl-NL/common.json +++ b/locales/nl-NL/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Applicatie wordt gestart...", + "finished": "Database-initialisatie voltooid", "goToChat": "(4/4) De chatpagina wordt geladen...", "initAuth": "(2/4) Initialisatie van de autorisatiedienst...", "initUser": "(3/4) Initialisatie van de gebruikersstatus...", - "initializing": "(1/4) De applicatie wordt opgestart..." + "initializing": "(1/4) De applicatie wordt opgestart...", + "loadingDependencies": "Afhankelijkheden worden geïnitialiseerd...", + "loadingWasm": "WASM-modules worden geladen...", + "migrating": "Gegevens migreren...", + "ready": "Database is gereed" }, "autoGenerate": "Automatisch genereren", "autoGenerateTooltip": "Automatisch assistentbeschrijving genereren op basis van suggesties", @@ -39,9 +45,7 @@ "idle": "Wachten op initialisatie...", "initializing": "Bezig met initialiseren...", "loadingDependencies": "Afhankelijkheden laden...", - "loadingDependenciesWithProgress": "Afhankelijkheden laden ({{progress}}%)...", "loadingWasmModule": "WASM-module laden...", - "loadingWasmModuleWithProgress": "WASM-module laden ({{progress}}%)...", "migrating": "Gegevens migreren...", "ready": "Database is gereed" }, diff --git a/locales/pl-PL/common.json b/locales/pl-PL/common.json index 7975aaaa64ef..fd695a60ea4b 100644 --- a/locales/pl-PL/common.json +++ b/locales/pl-PL/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Aplikacja się uruchamia...", + "finished": "Inicjalizacja bazy danych zakończona", "goToChat": "(4/4) Ładowanie strony czatu...", "initAuth": "(2/4) Inicjalizacja usługi autoryzacji...", "initUser": "(3/4) Inicjalizacja stanu użytkownika...", - "initializing": "(1/4) Uruchamianie aplikacji..." + "initializing": "(1/4) Uruchamianie aplikacji...", + "loadingDependencies": "Inicjalizacja zależności...", + "loadingWasm": "Ładowanie modułu WASM...", + "migrating": "Wykonywanie migracji tabeli danych...", + "ready": "Baza danych jest gotowa" }, "autoGenerate": "Automatyczne generowanie", "autoGenerateTooltip": "Automatyczne uzupełnianie opisu asystenta na podstawie sugestii", @@ -39,9 +45,7 @@ "idle": "Oczekiwanie na inicjalizację...", "initializing": "Inicjalizowanie...", "loadingDependencies": "Ładowanie zależności...", - "loadingDependenciesWithProgress": "Ładowanie zależności ({{progress}}%)...", "loadingWasmModule": "Ładowanie modułu WASM...", - "loadingWasmModuleWithProgress": "Ładowanie modułu WASM ({{progress}}%)...", "migrating": "Wykonywanie migracji tabeli danych...", "ready": "Baza danych gotowa" }, diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 0103c2d23ab9..38fd3277bcb1 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Iniciando o aplicativo...", + "finished": "Inicialização do banco de dados concluída", "goToChat": "(4/4) Carregando página de conversa...", "initAuth": "(2/4) Inicializando o serviço de autenticação...", "initUser": "(3/4) Inicializando o estado do usuário...", - "initializing": "(1/4) Iniciando aplicativo..." + "initializing": "(1/4) Iniciando aplicativo...", + "loadingDependencies": "Inicializando dependências...", + "loadingWasm": "Carregando módulo WASM...", + "migrating": "Executando migração de tabelas de dados...", + "ready": "Banco de dados pronto" }, "autoGenerate": "Auto completar", "autoGenerateTooltip": "Auto completar descrição do assistente com base em sugestões", @@ -39,9 +45,7 @@ "idle": "Aguardando inicialização...", "initializing": "Inicializando...", "loadingDependencies": "Carregando dependências...", - "loadingDependenciesWithProgress": "Carregando dependências ({{progress}}%)...", "loadingWasmModule": "Carregando módulo WASM...", - "loadingWasmModuleWithProgress": "Carregando módulo WASM ({{progress}}%)...", "migrating": "Executando migração de tabela de dados...", "ready": "Banco de dados pronto" }, diff --git a/src/app/loading/Content.tsx b/src/app/loading/Content.tsx deleted file mode 100644 index d679a0ce1511..000000000000 --- a/src/app/loading/Content.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import FullscreenLoading from '@/components/FullscreenLoading'; -import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; - -import InitClientDB from './InitClientDB'; - -import { AppLoadingStage } from './type'; - -interface ContentProps { - loadingStage: AppLoadingStage; -} -const Content = memo(({ loadingStage }) => { - const { t } = useTranslation('common'); - const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); - - return isPgliteNotInited ? ( - - ) : ( - ); -}); - -export default Content; diff --git a/src/app/loading/InitClientDB.tsx b/src/app/loading/InitClientDB.tsx deleted file mode 100644 index 2d83f589e63f..000000000000 --- a/src/app/loading/InitClientDB.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { memo } from 'react'; -import { Center, Flexbox } from 'react-layout-kit'; - -import { ProductLogo } from '@/components/Branding'; -import InitProgress from '@/features/InitClientDB/InitProgress'; -import { useGlobalStore } from '@/store/global'; - -const InitClientDB = memo(() => { - const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); - - useInitClientDB(); - - return ( - -
- -
- -
-
-
- ); -}); - -export default InitClientDB; diff --git a/src/app/loading/Redirect.tsx b/src/app/loading/Redirect.tsx deleted file mode 100644 index e7f120a5d81d..000000000000 --- a/src/app/loading/Redirect.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { memo, useEffect } from 'react'; - -import { useGlobalStore } from '@/store/global'; -import { systemStatusSelectors } from '@/store/global/selectors'; -import { useUserStore } from '@/store/user'; -import { authSelectors } from '@/store/user/selectors'; - -import { AppLoadingStage } from './type'; - -interface RedirectProps { - setLoadingStage: (value: AppLoadingStage) => void; -} - -const Redirect = memo(({ setLoadingStage }) => { - const router = useRouter(); - const [isLogin, isLoaded, isUserStateInit, isOnboard] = useUserStore((s) => [ - authSelectors.isLogin(s), - authSelectors.isLoaded(s), - s.isUserStateInit, - s.isOnboard, - ]); - const isPgliteNotEnabled = useGlobalStore(systemStatusSelectors.isPgliteNotEnabled); - - const navToChat = () => { - setLoadingStage(AppLoadingStage.GoToChat); - router.replace('/chat'); - }; - - useEffect(() => { - // if pglite is not enabled, redirect to chat - if (isPgliteNotEnabled) { - navToChat(); - return; - } - - // if user auth state is not ready, wait for loading - if (!isLoaded) { - setLoadingStage(AppLoadingStage.InitAuth); - return; - } - - // this mean user is definitely not login - if (!isLogin) { - navToChat(); - return; - } - - // if user state not init, wait for loading - if (!isUserStateInit) { - setLoadingStage(AppLoadingStage.InitUser); - return; - } - - // user need to onboard - if (!isOnboard) { - router.replace('/onboard'); - return; - } - - // finally check the conversation status - navToChat(); - }, [isUserStateInit, isLoaded, isOnboard, isLogin, isPgliteNotEnabled]); - - return null; -}); - -export default Redirect; diff --git a/src/app/loading/type.ts b/src/app/loading/type.ts deleted file mode 100644 index 28e401e5abac..000000000000 --- a/src/app/loading/type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum AppLoadingStage { - GoToChat = 'goToChat', - InitAuth = 'initAuth', - InitUser = 'initUser', - Initializing = 'initializing', -} diff --git a/src/features/InitClientDB/InitProgress.tsx b/src/features/InitClientDB/InitProgress.tsx deleted file mode 100644 index 0b48cd3426c9..000000000000 --- a/src/features/InitClientDB/InitProgress.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { CheckCircleFilled } from '@ant-design/icons'; -import { Icon } from '@lobehub/ui'; -import { Progress, Typography } from 'antd'; -import { createStyles } from 'antd-style'; -import isEqual from 'fast-deep-equal'; -import { AlertCircle, Loader2 } from 'lucide-react'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Flexbox } from 'react-layout-kit'; - -import { useGlobalStore } from '@/store/global'; -import { DatabaseLoadingState } from '@/types/clientDB'; - -const useStyles = createStyles(({ css, token, isDarkMode, responsive }) => ({ - desc: css` - width: 280px; - color: ${token.colorTextSecondary}; - - ${responsive.mobile} { - line-height: ${token.lineHeight}; - } - `, - hint: css` - font-size: ${token.fontSizeSM}px; - color: ${token.colorTextTertiary}; - text-align: center; - `, - icon: css` - color: ${isDarkMode ? token.blue : token.geekblue}; - `, - iconCtn: css` - width: 72px; - height: 72px; - background: ${isDarkMode ? token.blue1 : token.geekblue1}; - border-radius: 50%; - `, - intro: css` - margin-block-end: 12px; - - ${responsive.mobile} { - width: 350px; - margin-block-start: 24px; - line-height: ${token.lineHeight}; - } - `, - - title: css` - margin-block-end: 0; - font-size: ${token.fontSizeLG}px; - font-weight: bold; - `, -})); - -interface InitingProps { - title: string | boolean; -} -const InitProgress = memo(({ title }) => { - const { t } = useTranslation('common'); - const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); - const process = useGlobalStore((s) => s.initClientDBProcess, isEqual); - const { styles, theme } = useStyles(); - - const getStateMessage = (state: DatabaseLoadingState, progress?: number) => { - switch (state) { - case DatabaseLoadingState.Error: { - return t('clientDB.initing.error'); - } - case DatabaseLoadingState.Idle: { - return t('clientDB.initing.idle'); - } - case DatabaseLoadingState.Initializing: { - return t('clientDB.initing.initializing'); - } - case DatabaseLoadingState.LoadingDependencies: { - return t('clientDB.initing.loadingDependencies', { progress }); - } - - case DatabaseLoadingState.LoadingWasm: { - return t('clientDB.initing.loadingWasmModule', { progress }); - } - - case DatabaseLoadingState.Migrating: { - return t('clientDB.initing.migrating'); - } - - case DatabaseLoadingState.Ready: { - return t('clientDB.initing.ready'); - } - } - }; - - return ( - <> - - {title} - - - - {currentStage === DatabaseLoadingState.Error ? ( - - ) : currentStage === DatabaseLoadingState.Ready ? ( - - ) : currentStage === DatabaseLoadingState.LoadingDependencies ? ( - - ) : currentStage === DatabaseLoadingState.LoadingWasm ? ( - - ) : ( - - )} - - {getStateMessage(currentStage, process?.progress)} - - - - ); -}); - -export default InitProgress; diff --git a/src/features/InitClientDB/index.tsx b/src/features/InitClientDB/index.tsx deleted file mode 100644 index 64fd43d17efb..000000000000 --- a/src/features/InitClientDB/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Center } from 'react-layout-kit'; - -import { useGlobalStore } from '@/store/global'; - -import InitProgress from './InitProgress'; - -const InitClientDB = memo(() => { - const { t } = useTranslation('common'); - const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); - - useInitClientDB(); - - return ( -
-
- -
-
- ); -}); - -export default InitClientDB; From b8a97594a065d45b700855ce7a1992e2ef01973c Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 09:18:36 +0000 Subject: [PATCH 13/16] refactor to improve home loading --- locales/ru-RU/common.json | 10 ++- locales/tr-TR/common.json | 10 ++- locales/vi-VN/common.json | 10 ++- locales/zh-CN/common.json | 16 +++-- locales/zh-TW/common.json | 10 ++- .../features/InitClientDB/FailedModal.tsx | 4 +- .../features/InitClientDB/InitIndicator.tsx | 9 ++- src/app/loading/Client/Content.tsx | 42 ++++++++++++ src/app/loading/Client/Redirect.tsx | 64 +++++++++++++++++++ src/app/loading/Client/index.tsx | 21 ++++++ src/app/loading/Server/Content.tsx | 21 ++++++ src/app/loading/Server/Redirect.tsx | 61 ++++++++++++++++++ src/app/loading/Server/index.tsx | 21 ++++++ src/app/loading/index.tsx | 20 ++---- src/app/loading/stage.ts | 30 +++++++++ src/components/FullscreenLoading/index.tsx | 17 +++-- src/components/InitProgress/index.tsx | 42 ++++++++++++ src/features/MobileSwitchLoading/index.tsx | 27 ++++++-- src/locales/default/common.ts | 15 +++-- src/store/global/actions/clientDb.ts | 13 ++-- src/types/clientDB.ts | 28 +++++--- 21 files changed, 418 insertions(+), 73 deletions(-) create mode 100644 src/app/loading/Client/Content.tsx create mode 100644 src/app/loading/Client/Redirect.tsx create mode 100644 src/app/loading/Client/index.tsx create mode 100644 src/app/loading/Server/Content.tsx create mode 100644 src/app/loading/Server/Redirect.tsx create mode 100644 src/app/loading/Server/index.tsx create mode 100644 src/app/loading/stage.ts create mode 100644 src/components/InitProgress/index.tsx diff --git a/locales/ru-RU/common.json b/locales/ru-RU/common.json index 33a85cf2a372..f36d7f7c331a 100644 --- a/locales/ru-RU/common.json +++ b/locales/ru-RU/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Инициализация приложения...", + "finished": "Инициализация базы данных завершена", "goToChat": "(4/4) Загрузка страницы чата...", "initAuth": "(2/4) Инициализация службы аутентификации...", "initUser": "(3/4) Инициализация состояния пользователя...", - "initializing": "(1/4) Запуск приложения..." + "initializing": "(1/4) Запуск приложения...", + "loadingDependencies": "Инициализация зависимостей...", + "loadingWasm": "Загрузка модуля WASM...", + "migrating": "Выполнение миграции таблиц данных...", + "ready": "База данных готова" }, "autoGenerate": "Автозаполнение", "autoGenerateTooltip": "Автоматическое дополнение описания агента на основе подсказок", @@ -39,9 +45,7 @@ "idle": "Ожидание инициализации...", "initializing": "Инициализация...", "loadingDependencies": "Загрузка зависимостей...", - "loadingDependenciesWithProgress": "Загрузка зависимостей ({{progress}}%)...", "loadingWasmModule": "Загрузка модуля WASM...", - "loadingWasmModuleWithProgress": "Загрузка модуля WASM ({{progress}}%)...", "migrating": "Выполнение миграции таблицы данных...", "ready": "База данных готова" }, diff --git a/locales/tr-TR/common.json b/locales/tr-TR/common.json index 58f4c280ecf9..2cd88278743f 100644 --- a/locales/tr-TR/common.json +++ b/locales/tr-TR/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Uygulama başlatılıyor...", + "finished": "Veritabanı başlatma tamamlandı", "goToChat": "(4/4) Sohbet sayfası yükleniyor...", "initAuth": "(2/4) Kimlik doğrulama hizmeti başlatılıyor...", "initUser": "(3/4) Kullanıcı durumu başlatılıyor...", - "initializing": "(1/4) Uygulama başlatılıyor..." + "initializing": "(1/4) Uygulama başlatılıyor...", + "loadingDependencies": "Bağımlılıklar yükleniyor...", + "loadingWasm": "WASM modülü yükleniyor...", + "migrating": "Veri tablosu taşınıyor...", + "ready": "Veritabanı hazır" }, "autoGenerate": "Otomatik Oluştur", "autoGenerateTooltip": "Auto-generate agent description based on prompts", @@ -39,9 +45,7 @@ "idle": "Başlatma bekleniyor...", "initializing": "Başlatılıyor...", "loadingDependencies": "Bağlantılar yükleniyor...", - "loadingDependenciesWithProgress": "Bağlantılar yükleniyor ({{progress}}%)...", "loadingWasmModule": "WASM modülü yükleniyor...", - "loadingWasmModuleWithProgress": "WASM modülü yükleniyor ({{progress}}%)...", "migrating": "Veri tablosu taşınıyor...", "ready": "Veritabanı hazır" }, diff --git a/locales/vi-VN/common.json b/locales/vi-VN/common.json index b7df7e45c315..12abf895a4ef 100644 --- a/locales/vi-VN/common.json +++ b/locales/vi-VN/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "Đang khởi động ứng dụng...", + "finished": "Khởi tạo cơ sở dữ liệu hoàn tất", "goToChat": "(4/4) Đang tải trang trò chuyện...", "initAuth": "(2/4) Đang khởi tạo dịch vụ xác thực...", "initUser": "(3/4) Đang khởi tạo trạng thái người dùng...", - "initializing": "(1/4) Đang khởi động ứng dụng..." + "initializing": "(1/4) Đang khởi động ứng dụng...", + "loadingDependencies": "Đang khởi tạo phụ thuộc...", + "loadingWasm": "Đang tải mô-đun WASM...", + "migrating": "Đang thực hiện di chuyển bảng dữ liệu...", + "ready": "Cơ sở dữ liệu đã sẵn sàng" }, "autoGenerate": "Tự động tạo", "autoGenerateTooltip": "Tự động hoàn thành mô tả trợ lý dựa trên từ gợi ý", @@ -39,9 +45,7 @@ "idle": "Đang chờ khởi tạo...", "initializing": "Đang khởi tạo...", "loadingDependencies": "Đang tải phụ thuộc...", - "loadingDependenciesWithProgress": "Đang tải phụ thuộc ({{progress}}%)...", "loadingWasmModule": "Đang tải mô-đun WASM...", - "loadingWasmModuleWithProgress": "Đang tải mô-đun WASM ({{progress}}%)...", "migrating": "Đang thực hiện di chuyển bảng dữ liệu...", "ready": "Cơ sở dữ liệu đã sẵn sàng" }, diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json index 9195e3bf6c68..a09d38281ae6 100644 --- a/locales/zh-CN/common.json +++ b/locales/zh-CN/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { - "goToChat": "(4/4) 对话页面加载中...", - "initAuth": "(2/4) 鉴权服务初始化...", - "initUser": "(3/4) 用户状态初始化...", - "initializing": "(1/4) 应用启动中..." + "appInitializing": "应用启动中...", + "finished": "数据库初始化完成", + "goToChat": "对话页面加载中...", + "initAuth": "鉴权服务初始化...", + "initUser": "用户状态初始化...", + "initializing": "PGlite 数据库初始化...", + "loadingDependencies": "初始化依赖...", + "loadingWasm": "加载 WASM 模块...", + "migrating": "执行数据表迁移...", + "ready": "数据库已就绪" }, "autoGenerate": "自动补全", "autoGenerateTooltip": "基于提示词自动补全助手描述", @@ -39,9 +45,7 @@ "idle": "等待初始化...", "initializing": "正在初始化...", "loadingDependencies": "加载依赖中...", - "loadingDependenciesWithProgress": "加载依赖中({{progress}}%)...", "loadingWasmModule": "加载 WASM 模块中...", - "loadingWasmModuleWithProgress": "加载 WASM 模块中({{progress}}%)...", "migrating": "执行数据表迁移...", "ready": "数据库已就绪" }, diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index db0f1216dee6..086dcccabed4 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -10,10 +10,16 @@ } }, "appLoading": { + "appInitializing": "應用啟動中...", + "finished": "資料庫初始化完成", "goToChat": "(4/4) 對話頁面加載中...", "initAuth": "(2/4) 鑑權服務初始化...", "initUser": "(3/4) 使用者狀態初始化...", - "initializing": "(1/4) 應用啟動中..." + "initializing": "(1/4) 應用啟動中...", + "loadingDependencies": "初始化依賴...", + "loadingWasm": "加載 WASM 模組...", + "migrating": "執行資料表遷移...", + "ready": "資料庫已就緒" }, "autoGenerate": "自動生成", "autoGenerateTooltip": "基於提示詞自動生成助手描述", @@ -39,9 +45,7 @@ "idle": "等待初始化...", "initializing": "正在初始化...", "loadingDependencies": "載入依賴中...", - "loadingDependenciesWithProgress": "載入依賴中({{progress}}%)...", "loadingWasmModule": "載入 WASM 模組中...", - "loadingWasmModuleWithProgress": "載入 WASM 模組中({{progress}}%)...", "migrating": "執行資料表遷移...", "ready": "數據庫已就緒" }, diff --git a/src/app/(main)/chat/features/InitClientDB/FailedModal.tsx b/src/app/(main)/chat/features/InitClientDB/FailedModal.tsx index 99d4df553185..7c2db9dfeacc 100644 --- a/src/app/(main)/chat/features/InitClientDB/FailedModal.tsx +++ b/src/app/(main)/chat/features/InitClientDB/FailedModal.tsx @@ -49,7 +49,7 @@ const FailedModal = memo(({ error }) => { flexWrap: 'wrap', }} > - @@ -61,7 +61,7 @@ const FailedModal = memo(({ error }) => { subTitle={ - 非常抱歉,数据库升级过程发生异常。请重试升级,或 + 非常抱歉,数据库初始化过程发生异常。请尝试重试,或 ({ bg: css` @@ -47,7 +47,7 @@ interface InitClientDBProps { const InitClientDB = memo(({ bottom = 80, show }) => { const { styles, theme, cx } = useStyles(); - const currentStage = useGlobalStore((s) => s.initClientDBStage || 0); + const currentStage = useGlobalStore((s) => s.initClientDBStage || DatabaseLoadingState.Idle); const { t } = useTranslation('common'); const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); @@ -83,6 +83,7 @@ const InitClientDB = memo(({ bottom = 80, show }) => { } }; + const currentStageIndex = ClientDatabaseInitStages.indexOf(currentStage); return ( {show && ( @@ -100,7 +101,9 @@ const InitClientDB = memo(({ bottom = 80, show }) => { currentStage === DatabaseLoadingState.Finished && styles.progressReady, )} format={(percent) => percent} - percent={parseInt(((currentStage / DatabaseLoadingState.Finished) * 100).toFixed(0))} + percent={parseInt( + ((currentStageIndex / (ClientDatabaseInitStages.length - 1)) * 100).toFixed(0), + )} size={40} strokeColor={ currentStage === DatabaseLoadingState.Finished diff --git a/src/app/loading/Client/Content.tsx b/src/app/loading/Client/Content.tsx new file mode 100644 index 000000000000..b2c0647870f8 --- /dev/null +++ b/src/app/loading/Client/Content.tsx @@ -0,0 +1,42 @@ +import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import FullscreenLoading from '@/components/FullscreenLoading'; +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; + +import { CLIENT_LOADING_STAGES } from '../stage'; + +interface InitProps { + setActiveStage: (value: string) => void; +} + +const Init = memo(({ setActiveStage }) => { + const useInitClientDB = useGlobalStore((s) => s.useInitClientDB); + + useInitClientDB({ onStateChange: setActiveStage }); + + return null; +}); + +interface ContentProps { + loadingStage: string; + setActiveStage: (value: string) => void; +} + +const Content = memo(({ loadingStage, setActiveStage }) => { + const { t } = useTranslation('common'); + const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited); + + return ( + <> + {isPgliteNotInited && } + t(`appLoading.${key}` as any))} + /> + + ); +}); + +export default Content; diff --git a/src/app/loading/Client/Redirect.tsx b/src/app/loading/Client/Redirect.tsx new file mode 100644 index 000000000000..59444318d73b --- /dev/null +++ b/src/app/loading/Client/Redirect.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { memo, useEffect } from 'react'; + +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; +import { useUserStore } from '@/store/user'; +import { authSelectors } from '@/store/user/selectors'; + +import { AppLoadingStage } from '../stage'; + +interface RedirectProps { + setActiveStage: (value: AppLoadingStage) => void; +} + +const Redirect = memo(({ setActiveStage }) => { + const router = useRouter(); + const [isLogin, isLoaded, isUserStateInit] = useUserStore((s) => [ + authSelectors.isLogin(s), + authSelectors.isLoaded(s), + s.isUserStateInit, + ]); + + const isPgliteNotEnabled = useGlobalStore(systemStatusSelectors.isPgliteNotEnabled); + + const navToChat = () => { + setActiveStage(AppLoadingStage.GoToChat); + router.replace('/chat'); + }; + + useEffect(() => { + // if pglite is not enabled, redirect to chat + if (isPgliteNotEnabled) { + navToChat(); + return; + } + + // if user auth state is not ready, wait for loading + if (!isLoaded) { + setActiveStage(AppLoadingStage.InitAuth); + return; + } + + // this mean user is definitely not login + if (!isLogin) { + navToChat(); + return; + } + + // if user state not init, wait for loading + if (!isUserStateInit) { + setActiveStage(AppLoadingStage.InitUser); + return; + } + + // finally check the conversation status + navToChat(); + }, [isUserStateInit, isLoaded, isLogin, isPgliteNotEnabled]); + + return null; +}); + +export default Redirect; diff --git a/src/app/loading/Client/index.tsx b/src/app/loading/Client/index.tsx new file mode 100644 index 000000000000..7edb702086e3 --- /dev/null +++ b/src/app/loading/Client/index.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { useState } from 'react'; + +import { AppLoadingStage } from '../stage'; +import Client from './Content'; +import Redirect from './Redirect'; + +const ScreenLoading = () => { + const [activeStage, setActiveStage] = useState(AppLoadingStage.Initializing); + return ( + <> + + + + ); +}; + +ScreenLoading.displayName = 'ScreenLoading'; + +export default ScreenLoading; diff --git a/src/app/loading/Server/Content.tsx b/src/app/loading/Server/Content.tsx new file mode 100644 index 000000000000..8652d1a2d526 --- /dev/null +++ b/src/app/loading/Server/Content.tsx @@ -0,0 +1,21 @@ +import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import FullscreenLoading from '@/components/FullscreenLoading'; + +import { AppLoadingStage, SERVER_LOADING_STAGES } from '../stage'; + +interface ContentProps { + loadingStage: AppLoadingStage; +} + +const Content = memo(({ loadingStage }) => { + const { t } = useTranslation('common'); + const activeStage = SERVER_LOADING_STAGES.indexOf(loadingStage); + + const stages = SERVER_LOADING_STAGES.map((key) => t(`appLoading.${key}` as any)); + + return ; +}); + +export default Content; diff --git a/src/app/loading/Server/Redirect.tsx b/src/app/loading/Server/Redirect.tsx new file mode 100644 index 000000000000..4d270737a6ee --- /dev/null +++ b/src/app/loading/Server/Redirect.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { memo, useEffect } from 'react'; + +import { useUserStore } from '@/store/user'; +import { authSelectors } from '@/store/user/selectors'; + +import { AppLoadingStage } from '../stage'; + +interface RedirectProps { + setLoadingStage: (value: AppLoadingStage) => void; +} + +const Redirect = memo(({ setLoadingStage }) => { + const router = useRouter(); + const [isLogin, isLoaded, isUserStateInit, isOnboard] = useUserStore((s) => [ + authSelectors.isLogin(s), + authSelectors.isLoaded(s), + s.isUserStateInit, + s.isOnboard, + ]); + + const navToChat = () => { + setLoadingStage(AppLoadingStage.GoToChat); + router.replace('/chat'); + }; + + useEffect(() => { + // if user auth state is not ready, wait for loading + if (!isLoaded) { + setLoadingStage(AppLoadingStage.InitAuth); + return; + } + + // this mean user is definitely not login + if (!isLogin) { + navToChat(); + return; + } + + // if user state not init, wait for loading + if (!isUserStateInit) { + setLoadingStage(AppLoadingStage.InitUser); + return; + } + + // user need to onboard + if (!isOnboard) { + router.replace('/onboard'); + return; + } + + // finally go to chat + navToChat(); + }, [isUserStateInit, isLoaded, isOnboard, isLogin]); + + return null; +}); + +export default Redirect; diff --git a/src/app/loading/Server/index.tsx b/src/app/loading/Server/index.tsx new file mode 100644 index 000000000000..3b207cd2f711 --- /dev/null +++ b/src/app/loading/Server/index.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { useState } from 'react'; + +import { AppLoadingStage } from '../stage'; +import Client from './Content'; +import Redirect from './Redirect'; + +const ScreenLoading = () => { + const [loadingStage, setLoadingStage] = useState(AppLoadingStage.Initializing); + return ( + <> + + + + ); +}; + +ScreenLoading.displayName = 'ScreenLoading'; + +export default ScreenLoading; diff --git a/src/app/loading/index.tsx b/src/app/loading/index.tsx index 27d7376f9816..d66fd36b7917 100644 --- a/src/app/loading/index.tsx +++ b/src/app/loading/index.tsx @@ -1,21 +1,9 @@ -'use client'; +import { isServerMode } from '@/const/version'; -import { useState } from 'react'; +import Client from './Client'; +import Server from './Server'; -import { AppLoadingStage } from '@/app/loading/type'; - -import Client from './Content'; -import Redirect from './Redirect'; - -const ScreenLoading = () => { - const [loadingStage, setLoadingStage] = useState(AppLoadingStage.Initializing); - return ( - <> - - - - ); -}; +const ScreenLoading = () => (isServerMode ? : ); ScreenLoading.displayName = 'ScreenLoading'; diff --git a/src/app/loading/stage.ts b/src/app/loading/stage.ts new file mode 100644 index 000000000000..e77c3dcd2643 --- /dev/null +++ b/src/app/loading/stage.ts @@ -0,0 +1,30 @@ +import { DatabaseLoadingState } from '@/types/clientDB'; + +export enum AppLoadingStage { + GoToChat = 'goToChat', + Idle = 'appIdle', + InitAuth = 'initAuth', + InitUser = 'initUser', + Initializing = 'appInitializing', +} + +export const SERVER_LOADING_STAGES = [ + AppLoadingStage.Initializing, + AppLoadingStage.InitAuth, + AppLoadingStage.InitUser, + AppLoadingStage.GoToChat, +]; + +export const CLIENT_LOADING_STAGES = [ + AppLoadingStage.Idle, + AppLoadingStage.Initializing, + AppLoadingStage.InitAuth, + DatabaseLoadingState.Initializing, + DatabaseLoadingState.LoadingDependencies, + DatabaseLoadingState.LoadingWasm, + DatabaseLoadingState.Migrating, + DatabaseLoadingState.Finished, + DatabaseLoadingState.Ready, + AppLoadingStage.InitUser, + AppLoadingStage.GoToChat, +] as string[]; diff --git a/src/components/FullscreenLoading/index.tsx b/src/components/FullscreenLoading/index.tsx index b5868ddafac8..1cb29d2d67d5 100644 --- a/src/components/FullscreenLoading/index.tsx +++ b/src/components/FullscreenLoading/index.tsx @@ -1,18 +1,21 @@ -import { Icon } from '@lobehub/ui'; -import { Loader2 } from 'lucide-react'; -import { ReactNode, memo } from 'react'; +import React, { memo } from 'react'; import { Center, Flexbox } from 'react-layout-kit'; import { ProductLogo } from '@/components/Branding'; +import InitProgress, { StageItem } from '@/components/InitProgress'; -const FullscreenLoading = memo<{ title?: ReactNode }>(({ title }) => { +interface FullscreenLoadingProps { + activeStage: number; + stages: StageItem[]; +} + +const FullscreenLoading = memo(({ activeStage, stages }) => { return (
-
- - {title} +
+
diff --git a/src/components/InitProgress/index.tsx b/src/components/InitProgress/index.tsx new file mode 100644 index 000000000000..4019f47a7106 --- /dev/null +++ b/src/components/InitProgress/index.tsx @@ -0,0 +1,42 @@ +import { Icon } from '@lobehub/ui'; +import { Progress, Typography } from 'antd'; +import { useTheme } from 'antd-style'; +import { Loader2 } from 'lucide-react'; +import { ReactNode, memo } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +export interface StageObjectItem { + icon?: ReactNode; + text: string; +} +export type StageItem = string | StageObjectItem; + +interface InitingProps { + activeStage: number; + stages: StageItem[]; +} + +const InitProgress = memo(({ activeStage, stages }) => { + const theme = useTheme(); + + const outStage = stages[activeStage]; + const percent = (activeStage / (stages.length - 1)) * 100; + + const stage = typeof outStage === 'string' ? { text: outStage } : outStage; + + return ( + <> + + + {stage?.icon ? stage?.icon : } + {stage?.text} + + + ); +}); + +export default InitProgress; diff --git a/src/features/MobileSwitchLoading/index.tsx b/src/features/MobileSwitchLoading/index.tsx index a8f3303dca44..a25069d097e0 100644 --- a/src/features/MobileSwitchLoading/index.tsx +++ b/src/features/MobileSwitchLoading/index.tsx @@ -1,13 +1,26 @@ -'use client'; - +import { Icon } from '@lobehub/ui'; +import { Loader2 } from 'lucide-react'; +import { DynamicOptions } from 'next/dist/shared/lib/dynamic'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { Center, Flexbox } from 'react-layout-kit'; -import FullscreenLoading from '@/components/FullscreenLoading'; +import { ProductLogo } from '@/components/Branding'; -const MobileSwitchLoading = () => { +// @ts-expect-error +const MobileSwitchLoading: DynamicOptions['loading'] = memo(() => { const { t } = useTranslation('common'); - - return ; -}; + return ( + +
+ +
+ + {t('layoutInitializing')} +
+
+
+ ); +}); export default MobileSwitchLoading; diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index e2c4a756c49b..9a600600cb6b 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -11,10 +11,13 @@ export default { }, }, appLoading: { - goToChat: '(4/4) 对话页面加载中...', - initAuth: '(2/4) 鉴权服务初始化...', - initUser: '(3/4) 用户状态初始化...', - initializing: '(1/4) 应用启动中...', + appInitializing: '应用启动中...', + finished: '数据库初始化完成', + + loadingDependencies: '初始化依赖...', + loadingWasm: '加载 WASM 模块...', + migrating: '执行数据表迁移...', + ready: '数据库已就绪', }, autoGenerate: '自动补全', autoGenerateTooltip: '基于提示词自动补全助手描述', @@ -40,10 +43,8 @@ export default { error: '发生错误,请重试', idle: '等待初始化...', initializing: '正在初始化...', - loadingDependencies: `加载依赖中...`, - loadingDependenciesWithProgress: `加载依赖中({{progress}}%)...`, + loadingDependencies: '加载依赖中...', loadingWasmModule: '加载 WASM 模块中...', - loadingWasmModuleWithProgress: '加载 WASM 模块中({{progress}}%)...', migrating: '执行数据表迁移...', ready: '数据库已就绪', }, diff --git a/src/store/global/actions/clientDb.ts b/src/store/global/actions/clientDb.ts index 0f6cc465d0d5..d8ebc238ef6b 100644 --- a/src/store/global/actions/clientDb.ts +++ b/src/store/global/actions/clientDb.ts @@ -3,15 +3,16 @@ import type { StateCreator } from 'zustand/vanilla'; import { useOnlyFetchOnceSWR } from '@/libs/swr'; import type { GlobalStore } from '@/store/global'; -import { DatabaseLoadingState } from '@/types/clientDB'; +import { DatabaseLoadingState, OnStageChange } from '@/types/clientDB'; +type InitClientDBParams = { onStateChange: OnStageChange }; /** * 设置操作 */ export interface GlobalClientDBAction { - initializeClientDB: () => Promise; + initializeClientDB: (params?: InitClientDBParams) => Promise; markPgliteEnabled: () => void; - useInitClientDB: () => SWRResponse; + useInitClientDB: (params?: InitClientDBParams) => SWRResponse; } export const clientDBSlice: StateCreator< @@ -20,7 +21,7 @@ export const clientDBSlice: StateCreator< [], GlobalClientDBAction > = (set, get) => ({ - initializeClientDB: async () => { + initializeClientDB: async (params) => { // if the db has started initialized or not error, just skip. if ( get().initClientDBStage !== DatabaseLoadingState.Idle && @@ -35,11 +36,13 @@ export const clientDBSlice: StateCreator< }, onStateChange: (state) => { set({ initClientDBStage: state }); + params?.onStateChange?.(state); }, }); }, markPgliteEnabled: () => { get().updateSystemStatus({ isEnablePglite: true }); }, - useInitClientDB: () => useOnlyFetchOnceSWR('initClientDB', () => get().initializeClientDB()), + useInitClientDB: (params) => + useOnlyFetchOnceSWR('initClientDB', () => get().initializeClientDB(params)), }); diff --git a/src/types/clientDB.ts b/src/types/clientDB.ts index 30346f5beb9b..70f45dbfd386 100644 --- a/src/types/clientDB.ts +++ b/src/types/clientDB.ts @@ -1,18 +1,30 @@ // 定义加载状态类型 export enum DatabaseLoadingState { - Idle, - Initializing, - LoadingDependencies, - LoadingWasm, - Migrating, - Finished, - Ready, - Error, + Error = 'error', + Finished = 'finished', + Idle = 'idle', + Initializing = 'initializing', + LoadingDependencies = 'loadingDependencies', + LoadingWasm = 'loadingWasm', + Migrating = 'migrating', + Ready = 'ready', } +export const ClientDatabaseInitStages = [ + DatabaseLoadingState.Idle, + DatabaseLoadingState.Initializing, + DatabaseLoadingState.LoadingDependencies, + DatabaseLoadingState.LoadingWasm, + DatabaseLoadingState.Migrating, + DatabaseLoadingState.Finished, + DatabaseLoadingState.Ready, +]; + // 定义进度回调接口 export interface ClientDBLoadingProgress { costTime?: number; phase: 'wasm' | 'dependencies'; progress: number; } + +export type OnStageChange = (state: DatabaseLoadingState) => void; From f6411487478d8b1f77964e6e2bb35acc61ed5bc5 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 09:23:37 +0000 Subject: [PATCH 14/16] update locale --- locales/ar/common.json | 8 ++++---- locales/bg-BG/common.json | 8 ++++---- locales/de-DE/common.json | 8 ++++---- locales/en-US/common.json | 8 ++++---- locales/es-ES/common.json | 8 ++++---- locales/fa-IR/common.json | 8 ++++---- locales/fr-FR/common.json | 8 ++++---- locales/it-IT/common.json | 8 ++++---- locales/ja-JP/common.json | 8 ++++---- locales/ko-KR/common.json | 8 ++++---- locales/nl-NL/common.json | 8 ++++---- locales/pl-PL/common.json | 8 ++++---- locales/pt-BR/common.json | 8 ++++---- locales/ru-RU/common.json | 8 ++++---- locales/tr-TR/common.json | 8 ++++---- locales/vi-VN/common.json | 8 ++++---- locales/zh-TW/common.json | 8 ++++---- src/locales/default/common.ts | 5 ++++- 18 files changed, 72 insertions(+), 69 deletions(-) diff --git a/locales/ar/common.json b/locales/ar/common.json index 4fabd9b6752b..84d1670baabb 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "جارٍ تشغيل التطبيق...", "finished": "تم الانتهاء من تهيئة قاعدة البيانات", - "goToChat": "(4/4) جارٍ تحميل صفحة الدردشة...", - "initAuth": "(2/4) جاري تهيئة خدمة المصادقة...", - "initUser": "(3/4) جاري تهيئة حالة المستخدم...", - "initializing": "(1/4) جارٍ تشغيل التطبيق...", + "goToChat": "جارٍ تحميل صفحة الدردشة...", + "initAuth": "جارٍ تهيئة خدمة المصادقة...", + "initUser": "جارٍ تهيئة حالة المستخدم...", + "initializing": "جارٍ تهيئة قاعدة بيانات PGlite...", "loadingDependencies": "جارٍ تهيئة الاعتمادات...", "loadingWasm": "جارٍ تحميل وحدة WASM...", "migrating": "جارٍ تنفيذ ترحيل الجداول...", diff --git a/locales/bg-BG/common.json b/locales/bg-BG/common.json index 852bab12e329..0eff3554e4a7 100644 --- a/locales/bg-BG/common.json +++ b/locales/bg-BG/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Приложението се стартира...", "finished": "Инициализацията на базата данни е завършена", - "goToChat": "(4/4) Зареждане на страницата за разговори...", - "initAuth": "(2/4) Инициализиране на услугата за удостоверяване...", - "initUser": "(3/4) Инициализиране на състоянието на потребителя...", - "initializing": "(1/4) Стартиране на приложението...", + "goToChat": "Зареждане на страницата за разговори...", + "initAuth": "Инициализиране на услугата за удостоверяване...", + "initUser": "Инициализиране на състоянието на потребителя...", + "initializing": "Инициализиране на PGlite базата данни...", "loadingDependencies": "Инициализиране на зависимостите...", "loadingWasm": "Зареждане на WASM модула...", "migrating": "Извършване на миграция на таблиците...", diff --git a/locales/de-DE/common.json b/locales/de-DE/common.json index 95ffe36f245e..114537420ef7 100644 --- a/locales/de-DE/common.json +++ b/locales/de-DE/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Anwendung wird gestartet...", "finished": "Datenbankinitialisierung abgeschlossen", - "goToChat": "(4/4) Lade die Chat-Seite...", - "initAuth": "(2/4) Authentifizierungsdienst wird initialisiert...", - "initUser": "(3/4) Benutzerstatus wird initialisiert...", - "initializing": "(1/4) Anwendung wird gestartet...", + "goToChat": "Lade die Chat-Seite...", + "initAuth": "Initialisiere den Authentifizierungsdienst...", + "initUser": "Initialisiere den Benutzerstatus...", + "initializing": "Initialisiere die PGlite-Datenbank...", "loadingDependencies": "Abhängigkeiten werden initialisiert...", "loadingWasm": "WASM-Module werden geladen...", "migrating": "Datenbankmigration wird durchgeführt...", diff --git a/locales/en-US/common.json b/locales/en-US/common.json index d01e9d868b2b..75d5dcc229fa 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Application is starting...", "finished": "Database initialization completed", - "goToChat": "(4/4) Loading chat page...", - "initAuth": "(2/4) Initializing authentication service...", - "initUser": "(3/4) Initializing user status...", - "initializing": "(1/4) Starting up application...", + "goToChat": "Loading chat page...", + "initAuth": "Initializing authentication service...", + "initUser": "Initializing user status...", + "initializing": "Initializing PGlite database...", "loadingDependencies": "Initializing dependencies...", "loadingWasm": "Loading WASM module...", "migrating": "Performing database migration...", diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index ecfe02008357..93d59b83426a 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Iniciando la aplicación...", "finished": "Inicialización de la base de datos completada", - "goToChat": "(4/4) Cargando la página de chat...", - "initAuth": "(2/4) Inicializando el servicio de autenticación...", - "initUser": "(3/4) Inicializando el estado del usuario...", - "initializing": "(1/4) Iniciando la aplicación...", + "goToChat": "Cargando la página de chat...", + "initAuth": "Inicializando el servicio de autenticación...", + "initUser": "Inicializando el estado del usuario...", + "initializing": "Inicializando la base de datos PGlite...", "loadingDependencies": "Inicializando dependencias...", "loadingWasm": "Cargando módulo WASM...", "migrating": "Ejecutando migración de tablas de datos...", diff --git a/locales/fa-IR/common.json b/locales/fa-IR/common.json index b24b72be6cee..1565b08b40ee 100644 --- a/locales/fa-IR/common.json +++ b/locales/fa-IR/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "در حال راه‌اندازی برنامه...", "finished": "راه‌اندازی پایگاه داده کامل شد", - "goToChat": "(4/4) در حال بارگذاری صفحه گفتگو...", - "initAuth": "(2/4) در حال راه‌اندازی سرویس احراز هویت...", - "initUser": "(3/4) در حال راه‌اندازی وضعیت کاربر...", - "initializing": "(1/4) در حال راه‌اندازی برنامه...", + "goToChat": "در حال بارگذاری صفحه گفتگو...", + "initAuth": "در حال راه‌اندازی سرویس احراز هویت...", + "initUser": "در حال راه‌اندازی وضعیت کاربر...", + "initializing": "در حال راه‌اندازی پایگاه داده PGlite...", "loadingDependencies": "در حال بارگذاری وابستگی‌ها...", "loadingWasm": "در حال بارگذاری ماژول WASM...", "migrating": "در حال اجرای مهاجرت جداول داده...", diff --git a/locales/fr-FR/common.json b/locales/fr-FR/common.json index caa8093002a0..e04c9b9cedae 100644 --- a/locales/fr-FR/common.json +++ b/locales/fr-FR/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "L'application se charge...", "finished": "Initialisation de la base de données terminée", - "goToChat": "(4/4) Chargement de la page de conversation...", - "initAuth": "(2/4) Initialisation du service d'authentification...", - "initUser": "(3/4) Initialisation de l'état de l'utilisateur...", - "initializing": "(1/4) L'application se lance...", + "goToChat": "Chargement de la page de chat...", + "initAuth": "Initialisation du service d'authentification...", + "initUser": "Initialisation de l'état de l'utilisateur...", + "initializing": "Initialisation de la base de données PGlite...", "loadingDependencies": "Chargement des dépendances...", "loadingWasm": "Chargement du module WASM...", "migrating": "Exécution de la migration des tables de données...", diff --git a/locales/it-IT/common.json b/locales/it-IT/common.json index 06e2ab71f721..4f2af16a0cc5 100644 --- a/locales/it-IT/common.json +++ b/locales/it-IT/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Avvio dell'app in corso...", "finished": "Inizializzazione del database completata", - "goToChat": "(4/4) Caricamento della pagina della chat in corso...", - "initAuth": "(2/4) Inizializzazione del servizio di autenticazione...", - "initUser": "(3/4) Inizializzazione dello stato utente...", - "initializing": "(1/4) Avvio dell'app in corso...", + "goToChat": "Caricamento della pagina di chat in corso...", + "initAuth": "Inizializzazione del servizio di autenticazione...", + "initUser": "Inizializzazione dello stato utente...", + "initializing": "Inizializzazione del database PGlite...", "loadingDependencies": "Inizializzazione delle dipendenze...", "loadingWasm": "Caricamento del modulo WASM...", "migrating": "Esecuzione della migrazione delle tabelle dati...", diff --git a/locales/ja-JP/common.json b/locales/ja-JP/common.json index 74540fb47e1d..7f23c3b1d6e8 100644 --- a/locales/ja-JP/common.json +++ b/locales/ja-JP/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "アプリケーションを起動しています...", "finished": "データベースの初期化が完了しました", - "goToChat": "(4/4) チャットページを読み込んでいます...", - "initAuth": "(2/4) 認証サービスの初期化...", - "initUser": "(3/4) ユーザー状態の初期化...", - "initializing": "(1/4) アプリを起動しています...", + "goToChat": "チャットページを読み込んでいます...", + "initAuth": "認証サービスを初期化しています...", + "initUser": "ユーザー状態を初期化しています...", + "initializing": "PGliteデータベースを初期化しています...", "loadingDependencies": "依存関係を初期化しています...", "loadingWasm": "WASM モジュールを読み込んでいます...", "migrating": "データテーブルの移行を実行しています...", diff --git a/locales/ko-KR/common.json b/locales/ko-KR/common.json index 62aab301c915..d9f3e1a7bd5b 100644 --- a/locales/ko-KR/common.json +++ b/locales/ko-KR/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "앱을 초기화하는 중...", "finished": "데이터베이스 초기화 완료", - "goToChat": "(4/4) 대화 페이지 로딩 중...", - "initAuth": "(2/4) 인증 서비스 초기화 중...", - "initUser": "(3/4) 사용자 상태 초기화 중...", - "initializing": "(1/4) 애플리케이션 시작 중...", + "goToChat": "대화 페이지 로딩 중...", + "initAuth": "인증 서비스 초기화 중...", + "initUser": "사용자 상태 초기화 중...", + "initializing": "PGlite 데이터베이스 초기화 중...", "loadingDependencies": "의존성 초기화 중...", "loadingWasm": "WASM 모듈 로드 중...", "migrating": "데이터베이스 테이블 마이그레이션 중...", diff --git a/locales/nl-NL/common.json b/locales/nl-NL/common.json index a5fb8a8b20ef..7d05b4d76fc3 100644 --- a/locales/nl-NL/common.json +++ b/locales/nl-NL/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Applicatie wordt gestart...", "finished": "Database-initialisatie voltooid", - "goToChat": "(4/4) De chatpagina wordt geladen...", - "initAuth": "(2/4) Initialisatie van de autorisatiedienst...", - "initUser": "(3/4) Initialisatie van de gebruikersstatus...", - "initializing": "(1/4) De applicatie wordt opgestart...", + "goToChat": "De chatpagina wordt geladen...", + "initAuth": "Authenticatiedienst wordt geïnitialiseerd...", + "initUser": "Gebruikersstatus wordt geïnitialiseerd...", + "initializing": "PGlite-database wordt geïnitialiseerd...", "loadingDependencies": "Afhankelijkheden worden geïnitialiseerd...", "loadingWasm": "WASM-modules worden geladen...", "migrating": "Gegevens migreren...", diff --git a/locales/pl-PL/common.json b/locales/pl-PL/common.json index fd695a60ea4b..9b38b1928bbe 100644 --- a/locales/pl-PL/common.json +++ b/locales/pl-PL/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Aplikacja się uruchamia...", "finished": "Inicjalizacja bazy danych zakończona", - "goToChat": "(4/4) Ładowanie strony czatu...", - "initAuth": "(2/4) Inicjalizacja usługi autoryzacji...", - "initUser": "(3/4) Inicjalizacja stanu użytkownika...", - "initializing": "(1/4) Uruchamianie aplikacji...", + "goToChat": "Ładowanie strony czatu...", + "initAuth": "Inicjalizacja usługi autoryzacji...", + "initUser": "Inicjalizacja stanu użytkownika...", + "initializing": "Inicjalizacja bazy danych PGlite...", "loadingDependencies": "Inicjalizacja zależności...", "loadingWasm": "Ładowanie modułu WASM...", "migrating": "Wykonywanie migracji tabeli danych...", diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 38fd3277bcb1..08748ef8a032 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Iniciando o aplicativo...", "finished": "Inicialização do banco de dados concluída", - "goToChat": "(4/4) Carregando página de conversa...", - "initAuth": "(2/4) Inicializando o serviço de autenticação...", - "initUser": "(3/4) Inicializando o estado do usuário...", - "initializing": "(1/4) Iniciando aplicativo...", + "goToChat": "Carregando página de conversa...", + "initAuth": "Inicializando serviço de autenticação...", + "initUser": "Inicializando estado do usuário...", + "initializing": "Inicializando banco de dados PGlite...", "loadingDependencies": "Inicializando dependências...", "loadingWasm": "Carregando módulo WASM...", "migrating": "Executando migração de tabelas de dados...", diff --git a/locales/ru-RU/common.json b/locales/ru-RU/common.json index f36d7f7c331a..d08cd8f3f6c4 100644 --- a/locales/ru-RU/common.json +++ b/locales/ru-RU/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Инициализация приложения...", "finished": "Инициализация базы данных завершена", - "goToChat": "(4/4) Загрузка страницы чата...", - "initAuth": "(2/4) Инициализация службы аутентификации...", - "initUser": "(3/4) Инициализация состояния пользователя...", - "initializing": "(1/4) Запуск приложения...", + "goToChat": "Загрузка страницы чата...", + "initAuth": "Инициализация службы аутентификации...", + "initUser": "Инициализация состояния пользователя...", + "initializing": "Инициализация базы данных PGlite...", "loadingDependencies": "Инициализация зависимостей...", "loadingWasm": "Загрузка модуля WASM...", "migrating": "Выполнение миграции таблиц данных...", diff --git a/locales/tr-TR/common.json b/locales/tr-TR/common.json index 2cd88278743f..66f49467b4ca 100644 --- a/locales/tr-TR/common.json +++ b/locales/tr-TR/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Uygulama başlatılıyor...", "finished": "Veritabanı başlatma tamamlandı", - "goToChat": "(4/4) Sohbet sayfası yükleniyor...", - "initAuth": "(2/4) Kimlik doğrulama hizmeti başlatılıyor...", - "initUser": "(3/4) Kullanıcı durumu başlatılıyor...", - "initializing": "(1/4) Uygulama başlatılıyor...", + "goToChat": "Sohbet sayfası yükleniyor...", + "initAuth": "Kimlik doğrulama servisi başlatılıyor...", + "initUser": "Kullanıcı durumu başlatılıyor...", + "initializing": "PGlite veritabanı başlatılıyor...", "loadingDependencies": "Bağımlılıklar yükleniyor...", "loadingWasm": "WASM modülü yükleniyor...", "migrating": "Veri tablosu taşınıyor...", diff --git a/locales/vi-VN/common.json b/locales/vi-VN/common.json index 12abf895a4ef..db2e2408bc90 100644 --- a/locales/vi-VN/common.json +++ b/locales/vi-VN/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "Đang khởi động ứng dụng...", "finished": "Khởi tạo cơ sở dữ liệu hoàn tất", - "goToChat": "(4/4) Đang tải trang trò chuyện...", - "initAuth": "(2/4) Đang khởi tạo dịch vụ xác thực...", - "initUser": "(3/4) Đang khởi tạo trạng thái người dùng...", - "initializing": "(1/4) Đang khởi động ứng dụng...", + "goToChat": "Đang tải trang trò chuyện...", + "initAuth": "Đang khởi tạo dịch vụ xác thực...", + "initUser": "Đang khởi tạo trạng thái người dùng...", + "initializing": "Đang khởi tạo cơ sở dữ liệu PGlite...", "loadingDependencies": "Đang khởi tạo phụ thuộc...", "loadingWasm": "Đang tải mô-đun WASM...", "migrating": "Đang thực hiện di chuyển bảng dữ liệu...", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 086dcccabed4..d1c15f28c4ed 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -12,10 +12,10 @@ "appLoading": { "appInitializing": "應用啟動中...", "finished": "資料庫初始化完成", - "goToChat": "(4/4) 對話頁面加載中...", - "initAuth": "(2/4) 鑑權服務初始化...", - "initUser": "(3/4) 使用者狀態初始化...", - "initializing": "(1/4) 應用啟動中...", + "goToChat": "對話頁面加載中...", + "initAuth": "鑑權服務初始化...", + "initUser": "用戶狀態初始化...", + "initializing": "PGlite 資料庫初始化...", "loadingDependencies": "初始化依賴...", "loadingWasm": "加載 WASM 模組...", "migrating": "執行資料表遷移...", diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 9a600600cb6b..16f41dd049e9 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -13,7 +13,10 @@ export default { appLoading: { appInitializing: '应用启动中...', finished: '数据库初始化完成', - + goToChat: '对话页面加载中...', + initAuth: '鉴权服务初始化...', + initUser: '用户状态初始化...', + initializing: 'PGlite 数据库初始化...', loadingDependencies: '初始化依赖...', loadingWasm: '加载 WASM 模块...', migrating: '执行数据表迁移...', From cf3ddc556867fb0e486c0fd46a460c3e477ed220 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 09:27:05 +0000 Subject: [PATCH 15/16] fix style --- src/app/loading/Client/Redirect.tsx | 21 ++------------------- src/app/loading/stage.ts | 1 - 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/app/loading/Client/Redirect.tsx b/src/app/loading/Client/Redirect.tsx index 59444318d73b..69fcf09e9cc6 100644 --- a/src/app/loading/Client/Redirect.tsx +++ b/src/app/loading/Client/Redirect.tsx @@ -6,7 +6,6 @@ import { memo, useEffect } from 'react'; import { useGlobalStore } from '@/store/global'; import { systemStatusSelectors } from '@/store/global/selectors'; import { useUserStore } from '@/store/user'; -import { authSelectors } from '@/store/user/selectors'; import { AppLoadingStage } from '../stage'; @@ -16,11 +15,7 @@ interface RedirectProps { const Redirect = memo(({ setActiveStage }) => { const router = useRouter(); - const [isLogin, isLoaded, isUserStateInit] = useUserStore((s) => [ - authSelectors.isLogin(s), - authSelectors.isLoaded(s), - s.isUserStateInit, - ]); + const isUserStateInit = useUserStore((s) => s.isUserStateInit); const isPgliteNotEnabled = useGlobalStore(systemStatusSelectors.isPgliteNotEnabled); @@ -36,18 +31,6 @@ const Redirect = memo(({ setActiveStage }) => { return; } - // if user auth state is not ready, wait for loading - if (!isLoaded) { - setActiveStage(AppLoadingStage.InitAuth); - return; - } - - // this mean user is definitely not login - if (!isLogin) { - navToChat(); - return; - } - // if user state not init, wait for loading if (!isUserStateInit) { setActiveStage(AppLoadingStage.InitUser); @@ -56,7 +39,7 @@ const Redirect = memo(({ setActiveStage }) => { // finally check the conversation status navToChat(); - }, [isUserStateInit, isLoaded, isLogin, isPgliteNotEnabled]); + }, [isUserStateInit, isPgliteNotEnabled]); return null; }); diff --git a/src/app/loading/stage.ts b/src/app/loading/stage.ts index e77c3dcd2643..1977352cc23d 100644 --- a/src/app/loading/stage.ts +++ b/src/app/loading/stage.ts @@ -18,7 +18,6 @@ export const SERVER_LOADING_STAGES = [ export const CLIENT_LOADING_STAGES = [ AppLoadingStage.Idle, AppLoadingStage.Initializing, - AppLoadingStage.InitAuth, DatabaseLoadingState.Initializing, DatabaseLoadingState.LoadingDependencies, DatabaseLoadingState.LoadingWasm, From 13db9a84574970dd468e5c5d7e69dd4882cf97fd Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Wed, 18 Dec 2024 10:00:45 +0000 Subject: [PATCH 16/16] fix porgress --- .../chat/features/InitClientDB/InitIndicator.tsx | 14 ++++---------- src/types/clientDB.ts | 1 - 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx b/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx index f80143e69609..584a2f7c1bc0 100644 --- a/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx +++ b/src/app/(main)/chat/features/InitClientDB/InitIndicator.tsx @@ -84,6 +84,7 @@ const InitClientDB = memo(({ bottom = 80, show }) => { }; const currentStageIndex = ClientDatabaseInitStages.indexOf(currentStage); + const isReady = currentStage === DatabaseLoadingState.Finished; return ( {show && ( @@ -96,20 +97,13 @@ const InitClientDB = memo(({ bottom = 80, show }) => { > percent} + className={cx(styles.progress, isReady && styles.progressReady)} + format={isReady ? undefined : (percent) => percent} percent={parseInt( ((currentStageIndex / (ClientDatabaseInitStages.length - 1)) * 100).toFixed(0), )} size={40} - strokeColor={ - currentStage === DatabaseLoadingState.Finished - ? theme.colorSuccessActive - : theme.colorBgContainer - } + strokeColor={isReady ? theme.colorSuccessActive : theme.colorBgContainer} strokeLinecap={'round'} strokeWidth={10} trailColor={rgba(theme.colorBgContainer, 0.1)} diff --git a/src/types/clientDB.ts b/src/types/clientDB.ts index 70f45dbfd386..20febdf542a7 100644 --- a/src/types/clientDB.ts +++ b/src/types/clientDB.ts @@ -17,7 +17,6 @@ export const ClientDatabaseInitStages = [ DatabaseLoadingState.LoadingWasm, DatabaseLoadingState.Migrating, DatabaseLoadingState.Finished, - DatabaseLoadingState.Ready, ]; // 定义进度回调接口