@lobehub/chat 0.100.4 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +2 -2
  5. package/__mocks__/zustand/traditional.ts +25 -0
  6. package/package.json +7 -3
  7. package/src/app/api/files/image/imgur.ts +72 -0
  8. package/src/app/api/files/image/route.ts +42 -0
  9. package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
  10. package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
  11. package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
  12. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
  13. package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +69 -0
  14. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +60 -19
  15. package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +79 -0
  16. package/src/app/chat/(mobile)/features/{ChatInput.tsx → ChatInput/index.tsx} +19 -3
  17. package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +19 -0
  18. package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
  19. package/src/app/chat/components/FileList/FileItem.tsx +69 -0
  20. package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
  21. package/src/app/chat/components/FileList/index.tsx +63 -0
  22. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +48 -0
  23. package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
  24. package/src/app/chat/features/ChatInput/ActionBar/History.tsx +64 -0
  25. package/src/app/chat/features/ChatInput/ActionBar/ModelSwitch.tsx +42 -0
  26. package/src/app/chat/features/ChatInput/ActionBar/Temperature.tsx +47 -0
  27. package/src/app/chat/features/{ChatInputContent/ActionBar/Token.tsx → ChatInput/ActionBar/Token/TokenTag.tsx} +4 -4
  28. package/src/app/chat/features/ChatInput/ActionBar/Token/index.tsx +21 -0
  29. package/src/app/chat/features/ChatInput/ActionBar/config.ts +21 -0
  30. package/src/app/chat/features/ChatInput/ActionBar/index.tsx +35 -0
  31. package/src/app/chat/features/ChatInput/useSend.ts +21 -0
  32. package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
  33. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
  34. package/src/config/__tests__/client.test.ts +76 -0
  35. package/src/config/__tests__/server.test.ts +52 -0
  36. package/src/config/server.ts +8 -0
  37. package/src/const/layoutTokens.ts +1 -1
  38. package/src/const/llm.ts +3 -0
  39. package/src/const/settings.ts +2 -0
  40. package/src/database/core/__tests__/db.test.ts +77 -0
  41. package/src/database/core/__tests__/model.test.ts +55 -0
  42. package/src/database/core/db.ts +25 -0
  43. package/src/database/core/index.ts +1 -0
  44. package/src/database/core/model.ts +49 -0
  45. package/src/database/core/schema.ts +9 -0
  46. package/src/database/models/file.test.ts +71 -0
  47. package/src/database/models/file.ts +26 -0
  48. package/src/locales/default/chat.ts +5 -0
  49. package/src/services/__tests__/file.test.ts +88 -0
  50. package/src/services/chatModel.ts +11 -6
  51. package/src/services/file.ts +36 -0
  52. package/src/store/files/index.ts +2 -0
  53. package/src/store/files/initialState.ts +7 -0
  54. package/src/store/files/selectors.test.ts +75 -0
  55. package/src/store/files/selectors.ts +34 -0
  56. package/src/store/files/slices/images/action.test.ts +186 -0
  57. package/src/store/files/slices/images/action.ts +83 -0
  58. package/src/store/files/slices/images/index.ts +3 -0
  59. package/src/store/files/slices/images/initialState.ts +11 -0
  60. package/src/store/files/store.ts +27 -0
  61. package/src/store/global/selectors/settings.test.ts +1 -3
  62. package/src/store/session/slices/agentConfig/selectors.ts +7 -1
  63. package/src/store/session/slices/chat/actions/message.ts +83 -18
  64. package/src/store/session/slices/chat/initialState.ts +4 -1
  65. package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
  66. package/src/store/session/slices/chat/reducers/files.ts +37 -0
  67. package/src/store/session/slices/chat/selectors/chat.ts +3 -2
  68. package/src/store/session/slices/session/initialState.ts +1 -0
  69. package/src/store/session/slices/session/reducers/session.test.ts +98 -1
  70. package/src/store/session/slices/session/reducers/session.ts +26 -1
  71. package/src/types/chatMessage.ts +3 -2
  72. package/src/types/database/db.ts +4 -0
  73. package/src/types/database/files.ts +38 -0
  74. package/src/types/files.ts +8 -0
  75. package/src/types/openai/chat.ts +15 -1
  76. package/src/types/session.ts +1 -0
  77. package/tests/setup.ts +8 -0
  78. package/vitest.config.ts +2 -0
  79. package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -35
  80. package/src/app/chat/features/ChatInputContent/ActionBar/ActionLeft.tsx +0 -117
  81. package/src/app/chat/features/ChatInputContent/ActionBar/ActionRight.tsx +0 -48
  82. package/src/app/chat/features/ChatInputContent/index.tsx +0 -71
  83. /package/src/app/chat/features/{ChatInputContent → ChatInput}/Topic/index.tsx +0 -0
@@ -0,0 +1,42 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Dropdown } from 'antd';
3
+ import { BrainCog } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { settingsSelectors, useGlobalStore } from '@/store/global';
8
+ import { useSessionStore } from '@/store/session';
9
+ import { agentSelectors } from '@/store/session/selectors';
10
+ import { LanguageModel } from '@/types/llm';
11
+
12
+ const ModelSwitch = memo(() => {
13
+ const { t } = useTranslation('setting');
14
+
15
+ const [model, updateAgentConfig] = useSessionStore((s) => {
16
+ const config = agentSelectors.currentAgentConfig(s);
17
+ return [config.model, s.updateAgentConfig];
18
+ });
19
+
20
+ const modelList = useGlobalStore(settingsSelectors.modelList);
21
+
22
+ return (
23
+ <Dropdown
24
+ menu={{
25
+ activeKey: model,
26
+ items: modelList.map((i) => ({ key: i, label: i })),
27
+ onClick: (e) => {
28
+ updateAgentConfig({ model: e.key as LanguageModel });
29
+ },
30
+ style: {
31
+ maxHeight: 400,
32
+ overflow: 'scroll',
33
+ },
34
+ }}
35
+ trigger={['click']}
36
+ >
37
+ <ActionIcon icon={BrainCog} placement={'bottom'} title={t('settingModel.model.title')} />
38
+ </Dropdown>
39
+ );
40
+ });
41
+
42
+ export default ModelSwitch;
@@ -0,0 +1,47 @@
1
+ import { ActionIcon, SliderWithInput } from '@lobehub/ui';
2
+ import { Popover } from 'antd';
3
+ import { Thermometer } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useSessionStore } from '@/store/session';
8
+ import { agentSelectors } from '@/store/session/selectors';
9
+
10
+ const Temperature = memo(() => {
11
+ const { t } = useTranslation('setting');
12
+
13
+ const [temperature, updateAgentConfig] = useSessionStore((s) => {
14
+ const config = agentSelectors.currentAgentConfig(s);
15
+ return [config.params.temperature, s.updateAgentConfig];
16
+ });
17
+
18
+ return (
19
+ <Popover
20
+ arrow={false}
21
+ content={
22
+ <SliderWithInput
23
+ controls={false}
24
+ max={1}
25
+ min={0}
26
+ onChange={(v) => {
27
+ updateAgentConfig({ params: { temperature: v } });
28
+ }}
29
+ size={'small'}
30
+ step={0.1}
31
+ style={{ width: 160 }}
32
+ value={temperature}
33
+ />
34
+ }
35
+ placement={'top'}
36
+ trigger={'click'}
37
+ >
38
+ <ActionIcon
39
+ icon={Thermometer}
40
+ placement={'bottom'}
41
+ title={t('settingModel.temperature.titleWithValue', { value: temperature })}
42
+ />
43
+ </Popover>
44
+ );
45
+ });
46
+
47
+ export default Temperature;
@@ -8,16 +8,16 @@ import { useSessionStore } from '@/store/session';
8
8
  import { agentSelectors, chatSelectors } from '@/store/session/selectors';
9
9
  import { LanguageModel } from '@/types/llm';
10
10
 
11
- const Token = memo<{ input: string }>(({ input }) => {
11
+ const Token = memo(() => {
12
12
  const { t } = useTranslation('chat');
13
13
 
14
- const inputTokenCount = useTokenCount(input);
15
-
16
- const [messageString, systemRole, model] = useSessionStore((s) => [
14
+ const [input, messageString, systemRole, model] = useSessionStore((s) => [
15
+ s.inputMessage,
17
16
  chatSelectors.chatsMessageString(s),
18
17
  agentSelectors.currentAgentSystemRole(s),
19
18
  agentSelectors.currentAgentModel(s) as LanguageModel,
20
19
  ]);
20
+ const inputTokenCount = useTokenCount(input);
21
21
 
22
22
  const systemRoleToken = useTokenCount(systemRole);
23
23
  const chatsToken = useTokenCount(messageString);
@@ -0,0 +1,21 @@
1
+ import dynamic from 'next/dynamic';
2
+ import { Suspense, memo } from 'react';
3
+
4
+ import { useSessionStore } from '@/store/session';
5
+ import { agentSelectors } from '@/store/session/selectors';
6
+
7
+ const LargeTokenContent = dynamic(() => import('./TokenTag'), { ssr: false });
8
+
9
+ const Token = memo(() => {
10
+ const [showTokenTag] = useSessionStore((s) => [agentSelectors.showTokenTag(s)]);
11
+
12
+ return (
13
+ showTokenTag && (
14
+ <Suspense>
15
+ <LargeTokenContent />
16
+ </Suspense>
17
+ )
18
+ );
19
+ });
20
+
21
+ export default Token;
@@ -0,0 +1,21 @@
1
+ import { FC } from 'react';
2
+
3
+ import Clear from './Clear';
4
+ import FileUpload from './FileUpload';
5
+ import History from './History';
6
+ import ModelSwitch from './ModelSwitch';
7
+ import Temperature from './Temperature';
8
+ import Token from './Token';
9
+
10
+ export const actionMap: Record<string, FC> = {
11
+ clear: Clear,
12
+ fileUpload: FileUpload,
13
+ history: History,
14
+ model: ModelSwitch,
15
+ temperature: Temperature,
16
+ token: Token,
17
+ };
18
+
19
+ // we can make these action lists configurable in the future
20
+ export const leftActionList = ['model', 'fileUpload', 'temperature', 'history', 'token'];
21
+ export const rightActionList = ['clear'];
@@ -0,0 +1,35 @@
1
+ import { ReactNode, memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import { actionMap, leftActionList, rightActionList } from './config';
5
+
6
+ const RenderActionList = ({ dataSource }: { dataSource: string[] }) => (
7
+ <>
8
+ {dataSource.map((key) => {
9
+ const Render = actionMap[key];
10
+ return <Render key={key} />;
11
+ })}
12
+ </>
13
+ );
14
+
15
+ export interface ActionBarProps {
16
+ rightAreaEndRender?: ReactNode;
17
+ rightAreaStartRender?: ReactNode;
18
+ }
19
+
20
+ const ActionBar = memo<ActionBarProps>(({ rightAreaStartRender, rightAreaEndRender }) => {
21
+ return (
22
+ <Flexbox align={'center'} flex={'none'} horizontal justify={'space-between'} padding={'0 16px'}>
23
+ <Flexbox align={'center'} flex={1} gap={4} horizontal>
24
+ <RenderActionList dataSource={leftActionList} />
25
+ </Flexbox>
26
+ <Flexbox align={'center'} flex={0} gap={4} horizontal justify={'flex-end'}>
27
+ {rightAreaStartRender}
28
+ <RenderActionList dataSource={rightActionList} />
29
+ {rightAreaEndRender}
30
+ </Flexbox>
31
+ </Flexbox>
32
+ );
33
+ });
34
+
35
+ export default ActionBar;
@@ -0,0 +1,21 @@
1
+ import { useCallback } from 'react';
2
+
3
+ import { filesSelectors, useFileStore } from '@/store/files';
4
+ import { useSessionStore } from '@/store/session';
5
+
6
+ export const useSendMessage = () => {
7
+ const [sendMessage, updateInputMessage] = useSessionStore((s) => [
8
+ s.sendMessage,
9
+ s.updateInputMessage,
10
+ ]);
11
+
12
+ return useCallback(() => {
13
+ const store = useSessionStore.getState();
14
+ if (!!store.chatLoadingId) return;
15
+ const imageList = filesSelectors.imageUrlOrBase64List(useFileStore.getState());
16
+
17
+ sendMessage(store.inputMessage, imageList);
18
+ updateInputMessage('');
19
+ useFileStore.getState().clearImageList();
20
+ }, []);
21
+ };
@@ -0,0 +1,23 @@
1
+ import { ReactNode, memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import FileList from '@/app/chat/components/FileList';
5
+ import { LOADING_FLAT } from '@/const/message';
6
+ import { ChatMessage } from '@/types/chatMessage';
7
+
8
+ import BubblesLoading from '../Loading';
9
+
10
+ export const UserMessage = memo<
11
+ ChatMessage & {
12
+ editableContent: ReactNode;
13
+ }
14
+ >(({ id, editableContent, content, ...res }) => {
15
+ if (content === LOADING_FLAT) return <BubblesLoading />;
16
+
17
+ return (
18
+ <Flexbox gap={8} id={id}>
19
+ {editableContent}
20
+ {res.files && <FileList editable={false} items={res.files} />}
21
+ </Flexbox>
22
+ );
23
+ });
@@ -10,11 +10,13 @@ import { pathString } from '@/utils/url';
10
10
  import { AssistantMessage } from './Assistant';
11
11
  import { DefaultMessage } from './Default';
12
12
  import { FunctionMessage } from './Function';
13
+ import { UserMessage } from './User';
13
14
 
14
15
  export const renderMessages: ChatListProps['renderMessages'] = {
15
16
  assistant: AssistantMessage,
16
17
  default: DefaultMessage,
17
18
  function: FunctionMessage,
19
+ user: UserMessage,
18
20
  };
19
21
 
20
22
  export const useAvatarsClick = (): ChatListProps['onAvatarsClick'] => {
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getClientConfig } from '../client';
4
+
5
+ // 测试前重置 process.env
6
+ vi.stubGlobal('process', {
7
+ ...process, // 保持原有的 process 对象
8
+ env: { ...process.env }, // 克隆环境变量对象,以便修改
9
+ });
10
+
11
+ describe('getClientConfig', () => {
12
+ it('should return default URLs when no environment variables are set', () => {
13
+ const config = getClientConfig();
14
+ expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
15
+ expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
16
+ });
17
+
18
+ it('should return custom URLs when environment variables are set', () => {
19
+ process.env.AGENTS_INDEX_URL = 'https://custom-agents-url.com';
20
+ process.env.PLUGINS_INDEX_URL = 'https://custom-plugins-url.com';
21
+ const config = getClientConfig();
22
+ expect(config.AGENTS_INDEX_URL).toBe('https://custom-agents-url.com');
23
+ expect(config.PLUGINS_INDEX_URL).toBe('https://custom-plugins-url.com');
24
+ });
25
+
26
+ it('should return default URLs when environment variables are empty string', () => {
27
+ process.env.AGENTS_INDEX_URL = '';
28
+ process.env.PLUGINS_INDEX_URL = '';
29
+
30
+ const config = getClientConfig();
31
+ expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
32
+ expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
33
+ });
34
+
35
+ it('should correctly reflect boolean values for analytics flags', () => {
36
+ process.env.NEXT_PUBLIC_ANALYTICS_VERCEL = '1';
37
+ process.env.NEXT_PUBLIC_VERCEL_DEBUG = '1';
38
+ process.env.NEXT_PUBLIC_ANALYTICS_PLAUSIBLE = '1';
39
+ process.env.NEXT_PUBLIC_ANALYTICS_POSTHOG = '1';
40
+ process.env.NEXT_PUBLIC_POSTHOG_DEBUG = '1';
41
+ process.env.NEXT_PUBLIC_I18N_DEBUG = '1';
42
+ process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER = '1';
43
+ process.env.NEXT_PUBLIC_I18N_DEBUG_SERVER = '1';
44
+
45
+ const config = getClientConfig();
46
+ expect(config.ANALYTICS_VERCEL).toBe(true);
47
+ expect(config.VERCEL_DEBUG).toBe(true);
48
+ expect(config.ANALYTICS_PLAUSIBLE).toBe(true);
49
+ expect(config.ANALYTICS_POSTHOG).toBe(true);
50
+ expect(config.POSTHOG_DEBUG).toBe(true);
51
+ expect(config.I18N_DEBUG).toBe(true);
52
+ expect(config.I18N_DEBUG_BROWSER).toBe(true);
53
+ expect(config.I18N_DEBUG_SERVER).toBe(true);
54
+ });
55
+
56
+ it('should correctly handle falsy values for analytics flags', () => {
57
+ process.env.NEXT_PUBLIC_ANALYTICS_VERCEL = '0';
58
+ process.env.NEXT_PUBLIC_VERCEL_DEBUG = '0';
59
+ process.env.NEXT_PUBLIC_ANALYTICS_PLAUSIBLE = '0';
60
+ process.env.NEXT_PUBLIC_ANALYTICS_POSTHOG = '0';
61
+ process.env.NEXT_PUBLIC_POSTHOG_DEBUG = '0';
62
+ process.env.NEXT_PUBLIC_I18N_DEBUG = '0';
63
+ process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER = '0';
64
+ process.env.NEXT_PUBLIC_I18N_DEBUG_SERVER = '0';
65
+
66
+ const config = getClientConfig();
67
+ expect(config.ANALYTICS_VERCEL).toBe(false);
68
+ expect(config.VERCEL_DEBUG).toBe(false);
69
+ expect(config.ANALYTICS_PLAUSIBLE).toBe(false);
70
+ expect(config.ANALYTICS_POSTHOG).toBe(false);
71
+ expect(config.POSTHOG_DEBUG).toBe(false);
72
+ expect(config.I18N_DEBUG).toBe(false);
73
+ expect(config.I18N_DEBUG_BROWSER).toBe(false);
74
+ expect(config.I18N_DEBUG_SERVER).toBe(false);
75
+ });
76
+ });
@@ -0,0 +1,52 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getServerConfig } from '../server';
4
+
5
+ // Stub the global process object to safely mock environment variables
6
+ vi.stubGlobal('process', {
7
+ ...process, // Preserve the original process object
8
+ env: { ...process.env }, // Clone the environment variables object for modification
9
+ });
10
+
11
+ describe('getServerConfig', () => {
12
+ beforeEach(() => {
13
+ // Reset environment variables before each test case
14
+ vi.restoreAllMocks();
15
+ });
16
+
17
+ it('throws an error if process is undefined', () => {
18
+ const originalProcess = global.process;
19
+ // To simulate the error condition, temporarily set the global process to `undefined`,
20
+ // @ts-ignore
21
+ global.process = undefined;
22
+
23
+ expect(() => getServerConfig()).toThrow(
24
+ '[Server Config] you are importing a nodejs-only module outside of nodejs',
25
+ );
26
+
27
+ global.process = originalProcess; // Restore the original process object
28
+ });
29
+
30
+ it('correctly reflects boolean value for USE_AZURE_OPENAI', () => {
31
+ process.env.USE_AZURE_OPENAI = '1';
32
+ const config = getServerConfig();
33
+ expect(config.USE_AZURE_OPENAI).toBe(true);
34
+ });
35
+
36
+ it('correctly handles falsy values for USE_AZURE_OPENAI', () => {
37
+ process.env.USE_AZURE_OPENAI = '0';
38
+ const config = getServerConfig();
39
+ expect(config.USE_AZURE_OPENAI).toBe(false);
40
+ });
41
+
42
+ it('returns default IMGUR_CLIENT_ID when no environment variable is set', () => {
43
+ const config = getServerConfig();
44
+ expect(config.IMGUR_CLIENT_ID).toBe('e415f320d6e24f9');
45
+ });
46
+
47
+ it('returns custom IMGUR_CLIENT_ID when environment variable is set', () => {
48
+ process.env.IMGUR_CLIENT_ID = 'custom-client-id';
49
+ const config = getServerConfig();
50
+ expect(config.IMGUR_CLIENT_ID).toBe('custom-client-id');
51
+ });
52
+ });
@@ -12,10 +12,16 @@ declare global {
12
12
  AZURE_API_KEY?: string;
13
13
  AZURE_API_VERSION?: string;
14
14
  USE_AZURE_OPENAI?: string;
15
+
16
+ IMGUR_CLIENT_ID?: string;
15
17
  }
16
18
  }
17
19
  }
18
20
 
21
+ // we apply a free imgur app to get a client id
22
+ // refs: https://apidocs.imgur.com/
23
+ const DEFAULT_IMAGUR_CLIENT_ID = 'e415f320d6e24f9';
24
+
19
25
  export const getServerConfig = () => {
20
26
  if (typeof process === 'undefined') {
21
27
  throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs');
@@ -30,5 +36,7 @@ export const getServerConfig = () => {
30
36
  AZURE_API_KEY: process.env.AZURE_API_KEY,
31
37
  AZURE_API_VERSION: process.env.AZURE_API_VERSION,
32
38
  USE_AZURE_OPENAI: process.env.USE_AZURE_OPENAI === '1',
39
+
40
+ IMGUR_CLIENT_ID: process.env.IMGUR_CLIENT_ID || DEFAULT_IMAGUR_CLIENT_ID,
33
41
  };
34
42
  };
@@ -3,7 +3,7 @@ import type { FormProps } from '@lobehub/ui';
3
3
  export const HEADER_HEIGHT = 64;
4
4
  export const MOBILE_NABBAR_HEIGHT = 44;
5
5
  export const MOBILE_TABBAR_HEIGHT = 48;
6
- export const CHAT_TEXTAREA_HEIGHT = 200;
6
+ export const CHAT_TEXTAREA_HEIGHT = 230;
7
7
  export const CHAT_TEXTAREA_HEIGHT_MOBILE = 108;
8
8
  export const CHAT_SIDEBAR_WIDTH = 280;
9
9
  export const MARKET_SIDEBAR_WIDTH = 400;
package/src/const/llm.ts CHANGED
@@ -12,3 +12,6 @@ export const LanguageModelWhiteList = [
12
12
  ];
13
13
 
14
14
  export const DEFAULT_OPENAI_MODEL_LIST: string[] = Object.values(LanguageModel);
15
+
16
+ // vision model white list, these models will change the content from string to array
17
+ export const VISION_MODEL_WHITE_LIST = ['gpt-4-vision-preview'];
@@ -18,6 +18,8 @@ export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = {
18
18
  themeMode: 'auto',
19
19
  };
20
20
 
21
+ export const VISION_MODEL_DEFAULT_MAX_TOKENS = 1000;
22
+
21
23
  export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
22
24
  displayMode: 'chat',
23
25
  historyCount: 1,
@@ -0,0 +1,77 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { DBModel } from '@/types/database/db';
4
+ import { LocalFile } from '@/types/database/files';
5
+
6
+ import { LocalDB } from '../db';
7
+
8
+ describe('LocalDB', () => {
9
+ let db: LocalDB;
10
+
11
+ beforeEach(() => {
12
+ db = new LocalDB();
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await db.delete();
17
+ db.close();
18
+ });
19
+
20
+ it('should be instantiated with the correct schema', () => {
21
+ const filesTable = db.files;
22
+
23
+ expect(filesTable).toBeDefined();
24
+ });
25
+
26
+ it('should allow adding a file', async () => {
27
+ const file: DBModel<LocalFile> = {
28
+ id: 'file1',
29
+ name: 'testfile.txt',
30
+ data: new ArrayBuffer(3),
31
+ saveMode: 'local',
32
+ fileType: 'plain/text',
33
+ size: 3,
34
+ createdAt: Date.now(),
35
+ };
36
+
37
+ await db.files.add(file);
38
+
39
+ expect(await db.files.get(file.id)).toEqual(file);
40
+ });
41
+
42
+ it('should allow updating a file', async () => {
43
+ const file: DBModel<LocalFile> = {
44
+ id: 'file1',
45
+ name: 'testfile.txt',
46
+ data: new ArrayBuffer(3),
47
+ saveMode: 'local',
48
+ fileType: 'plain/text',
49
+ size: 3,
50
+ createdAt: Date.now(),
51
+ };
52
+ await db.files.add(file);
53
+
54
+ // Act
55
+ await db.files.update(file.id, { name: 'update.txt' });
56
+
57
+ // Assert
58
+ expect(await db.files.get(file.id)).toHaveProperty('name', 'update.txt');
59
+ });
60
+
61
+ it('should allow deleting a file', async () => {
62
+ const file: DBModel<LocalFile> = {
63
+ id: 'file1',
64
+ name: 'testfile.txt',
65
+ data: new ArrayBuffer(3),
66
+ saveMode: 'local',
67
+ fileType: 'plain/text',
68
+ size: 3,
69
+ createdAt: Date.now(),
70
+ };
71
+ await db.files.add(file);
72
+
73
+ await db.files.delete(file.id);
74
+
75
+ expect(await db.files.get(file.id)).toBeUndefined();
76
+ });
77
+ });
@@ -0,0 +1,55 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ZodSchema, z } from 'zod';
3
+
4
+ import { BaseModel } from '../model';
5
+
6
+ // Define a mock schema for testing
7
+ const mockSchema = z.object({
8
+ name: z.string(),
9
+ content: z.string(),
10
+ });
11
+
12
+ // Define a mock table name
13
+ const mockTableName = 'files';
14
+
15
+ describe('BaseModel', () => {
16
+ let baseModel: BaseModel<typeof mockTableName>;
17
+
18
+ beforeEach(() => {
19
+ baseModel = new BaseModel(mockTableName, mockSchema);
20
+ // Mock the console.error to test error logging
21
+ vi.spyOn(console, 'error').mockImplementation(() => {});
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ // console.error = originalConsoleError;
27
+ });
28
+
29
+ it('should have a table property', () => {
30
+ expect(baseModel.table).toBeDefined();
31
+ });
32
+
33
+ describe('add method', () => {
34
+ it('should add a valid record to the database', async () => {
35
+ const validData = {
36
+ name: 'testfile.txt',
37
+ content: 'Hello, World!',
38
+ };
39
+
40
+ const result = await baseModel['add'](validData);
41
+
42
+ expect(result).toHaveProperty('id');
43
+ expect(console.error).not.toHaveBeenCalled();
44
+ });
45
+
46
+ it('should throw an error and log to console when adding an invalid record', async () => {
47
+ const invalidData = {
48
+ name: 123, // Invalid type, should be a string
49
+ content: 'Hello, World!',
50
+ };
51
+
52
+ await expect(baseModel['add'](invalidData)).rejects.toThrow(TypeError);
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,25 @@
1
+ import Dexie from 'dexie';
2
+
3
+ import { LobeDBSchemaMap as SchemaMap, lobeDBSchema } from '@/database/core/schema';
4
+ import { DBModel } from '@/types/database/db';
5
+
6
+ export type LocalDBSchema = {
7
+ [t in keyof SchemaMap]: {
8
+ model: SchemaMap[t];
9
+ table: Dexie.Table<DBModel<SchemaMap[t]>, string>;
10
+ };
11
+ };
12
+
13
+ // Define a local DB
14
+ export class LocalDB extends Dexie {
15
+ public files: LocalDBSchema['files']['table'];
16
+
17
+ constructor() {
18
+ super('LOBE_CHAT_DB');
19
+ this.version(1).stores(lobeDBSchema);
20
+
21
+ this.files = this.table('files');
22
+ }
23
+ }
24
+
25
+ export const LocalDBInstance = new LocalDB();
@@ -0,0 +1 @@
1
+ export * from './model';
@@ -0,0 +1,49 @@
1
+ import { ZodSchema } from 'zod';
2
+
3
+ import { DBModel } from '@/types/database/db';
4
+ import { LocalFile } from '@/types/database/files';
5
+ import { nanoid } from '@/utils/uuid';
6
+
7
+ import { LocalDB, LocalDBInstance, LocalDBSchema } from './db';
8
+
9
+ export class BaseModel<N extends keyof LocalDBSchema = any> {
10
+ private readonly db: LocalDB;
11
+ private readonly schema: ZodSchema;
12
+ private readonly _tableName: keyof LocalDBSchema;
13
+
14
+ constructor(table: N, schema: ZodSchema, db = LocalDBInstance) {
15
+ this.db = db;
16
+ this.schema = schema;
17
+ this._tableName = table;
18
+ }
19
+
20
+ get table() {
21
+ return this.db[this._tableName];
22
+ }
23
+
24
+ /**
25
+ * create a new record
26
+ * @param data
27
+ * @param id
28
+ */
29
+ protected async add<T = LocalDBSchema[N]['model']>(data: T, id: string | number = nanoid()) {
30
+ const result = this.schema.safeParse(data);
31
+
32
+ if (!result.success) {
33
+ const errorMsg = `[${this.db.name}][${this._tableName}] Failed to create new record. Error: ${result.error}`;
34
+
35
+ const newError = new TypeError(errorMsg);
36
+ // make this error show on console to help debug
37
+ console.error(newError);
38
+ throw newError;
39
+ }
40
+
41
+ const tableName = this._tableName;
42
+
43
+ const record: DBModel<LocalFile> = { ...result.data, createdAt: Date.now(), id };
44
+
45
+ const newId = await this.db[tableName].add(record);
46
+
47
+ return { id: newId };
48
+ }
49
+ }
@@ -0,0 +1,9 @@
1
+ import { LocalFile } from '@/types/database/files';
2
+
3
+ export const lobeDBSchema = {
4
+ files: '&id, name, fileType, saveMode',
5
+ };
6
+
7
+ export interface LobeDBSchemaMap {
8
+ files: LocalFile;
9
+ }