@lobehub/chat 1.0.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 (142) hide show
  1. package/.changelogrc.js +1 -0
  2. package/.commitlintrc.js +1 -0
  3. package/.editorconfig +16 -0
  4. package/.eslintignore +32 -0
  5. package/.eslintrc.js +6 -0
  6. package/.github/ISSUE_TEMPLATE/1_bug_report.yml +45 -0
  7. package/.github/ISSUE_TEMPLATE/2_feature_request.yml +21 -0
  8. package/.github/ISSUE_TEMPLATE/3_question.yml +15 -0
  9. package/.github/ISSUE_TEMPLATE/4_other.md +7 -0
  10. package/.github/PULL_REQUEST_TEMPLATE.md +17 -0
  11. package/.github/dependabot.yml +17 -0
  12. package/.github/workflows/auto-merge.yml +32 -0
  13. package/.github/workflows/contributor-help.yml +29 -0
  14. package/.github/workflows/issue-check-inactive.yml +22 -0
  15. package/.github/workflows/issue-close-require.yml +46 -0
  16. package/.github/workflows/issue-remove-inactive.yml +25 -0
  17. package/.github/workflows/release.yml +34 -0
  18. package/.github/workflows/test.yml +30 -0
  19. package/.gitpod.yml +3 -0
  20. package/.husky/commit-msg +4 -0
  21. package/.husky/pre-commit +5 -0
  22. package/.i18nrc.js +13 -0
  23. package/.prettierignore +63 -0
  24. package/.prettierrc.js +1 -0
  25. package/.releaserc.js +1 -0
  26. package/.remarkrc.js +1 -0
  27. package/.stylelintrc.js +8 -0
  28. package/CHANGELOG.md +80 -0
  29. package/README.md +147 -0
  30. package/locales/en_US/common.json +40 -0
  31. package/locales/en_US/setting.json +97 -0
  32. package/locales/zh_CN/common.json +40 -0
  33. package/locales/zh_CN/setting.json +98 -0
  34. package/next.config.mjs +32 -0
  35. package/package.json +138 -0
  36. package/public/next.svg +1 -0
  37. package/public/vercel.svg +1 -0
  38. package/scripts/genDefaultLocale.mjs +12 -0
  39. package/scripts/toc.mjs +40 -0
  40. package/src/const/fetch.ts +1 -0
  41. package/src/const/modelTokens.ts +8 -0
  42. package/src/features/FolderPanel/index.tsx +55 -0
  43. package/src/helpers/prompt.test.ts +36 -0
  44. package/src/helpers/prompt.ts +36 -0
  45. package/src/helpers/url.ts +17 -0
  46. package/src/layout/index.tsx +42 -0
  47. package/src/layout/style.ts +18 -0
  48. package/src/locales/create.ts +48 -0
  49. package/src/locales/default/common.ts +41 -0
  50. package/src/locales/default/setting.ts +97 -0
  51. package/src/locales/index.ts +5 -0
  52. package/src/locales/resources/en_US.ts +9 -0
  53. package/src/locales/resources/index.ts +7 -0
  54. package/src/locales/resources/zh_CN.ts +9 -0
  55. package/src/migrations/FromV0ToV1.ts +12 -0
  56. package/src/migrations/index.ts +13 -0
  57. package/src/pages/Sidebar.tsx +36 -0
  58. package/src/pages/_app.page.tsx +13 -0
  59. package/src/pages/_document.page.tsx +70 -0
  60. package/src/pages/api/LangChainStream.ts +95 -0
  61. package/src/pages/api/chain.api.ts +17 -0
  62. package/src/pages/api/openai.api.ts +31 -0
  63. package/src/pages/chat/SessionList/Header.tsx +56 -0
  64. package/src/pages/chat/SessionList/List/SessionItem.tsx +90 -0
  65. package/src/pages/chat/SessionList/List/index.tsx +31 -0
  66. package/src/pages/chat/SessionList/List/style.ts +77 -0
  67. package/src/pages/chat/SessionList/index.tsx +18 -0
  68. package/src/pages/chat/[id]/Config/ConfigCell.tsx +68 -0
  69. package/src/pages/chat/[id]/Config/ReadMode.tsx +63 -0
  70. package/src/pages/chat/[id]/Config/index.tsx +79 -0
  71. package/src/pages/chat/[id]/Conversation/ChatList.tsx +36 -0
  72. package/src/pages/chat/[id]/Conversation/Input.tsx +61 -0
  73. package/src/pages/chat/[id]/Conversation/index.tsx +32 -0
  74. package/src/pages/chat/[id]/Header.tsx +86 -0
  75. package/src/pages/chat/[id]/edit/AgentConfig.tsx +95 -0
  76. package/src/pages/chat/[id]/edit/AgentMeta.tsx +117 -0
  77. package/src/pages/chat/[id]/edit/FormItem.tsx +26 -0
  78. package/src/pages/chat/[id]/edit/Prompt.tsx +68 -0
  79. package/src/pages/chat/[id]/edit/index.page.tsx +62 -0
  80. package/src/pages/chat/[id]/edit/style.ts +42 -0
  81. package/src/pages/chat/[id]/index.page.tsx +40 -0
  82. package/src/pages/chat/index.page.tsx +1 -0
  83. package/src/pages/chat/layout.tsx +51 -0
  84. package/src/pages/index.page.tsx +1 -0
  85. package/src/pages/setting/Header.tsx +27 -0
  86. package/src/pages/setting/SettingForm.tsx +42 -0
  87. package/src/pages/setting/index.page.tsx +41 -0
  88. package/src/prompts/agent.ts +65 -0
  89. package/src/services/chatModel.ts +34 -0
  90. package/src/services/langChain.ts +18 -0
  91. package/src/services/url.ts +8 -0
  92. package/src/store/middleware/createHashStorage.ts +49 -0
  93. package/src/store/session/index.ts +33 -0
  94. package/src/store/session/initialState.ts +11 -0
  95. package/src/store/session/selectors.ts +3 -0
  96. package/src/store/session/slices/agentConfig/action.ts +226 -0
  97. package/src/store/session/slices/agentConfig/index.ts +3 -0
  98. package/src/store/session/slices/agentConfig/initialState.ts +34 -0
  99. package/src/store/session/slices/agentConfig/selectors.ts +54 -0
  100. package/src/store/session/slices/chat/action.ts +210 -0
  101. package/src/store/session/slices/chat/index.ts +3 -0
  102. package/src/store/session/slices/chat/initialState.ts +12 -0
  103. package/src/store/session/slices/chat/messageReducer.test.ts +70 -0
  104. package/src/store/session/slices/chat/messageReducer.ts +84 -0
  105. package/src/store/session/slices/chat/selectors.ts +83 -0
  106. package/src/store/session/slices/session/action.ts +118 -0
  107. package/src/store/session/slices/session/index.ts +3 -0
  108. package/src/store/session/slices/session/initialState.ts +31 -0
  109. package/src/store/session/slices/session/reducers/session.test.ts +456 -0
  110. package/src/store/session/slices/session/reducers/session.ts +113 -0
  111. package/src/store/session/slices/session/selectors/chat.ts +4 -0
  112. package/src/store/session/slices/session/selectors/index.ts +20 -0
  113. package/src/store/session/slices/session/selectors/list.ts +65 -0
  114. package/src/store/session/store.ts +17 -0
  115. package/src/store/settings/action.ts +31 -0
  116. package/src/store/settings/index.ts +23 -0
  117. package/src/store/settings/initialState.ts +25 -0
  118. package/src/store/settings/selectors.ts +9 -0
  119. package/src/store/settings/store.ts +13 -0
  120. package/src/styles/antdOverride.ts +29 -0
  121. package/src/styles/global.ts +23 -0
  122. package/src/styles/index.ts +6 -0
  123. package/src/types/chatMessage.ts +46 -0
  124. package/src/types/exportConfig.ts +23 -0
  125. package/src/types/global.d.ts +14 -0
  126. package/src/types/i18next.d.ts +8 -0
  127. package/src/types/langchain.ts +34 -0
  128. package/src/types/llm.ts +49 -0
  129. package/src/types/locale.ts +7 -0
  130. package/src/types/meta.ts +26 -0
  131. package/src/types/openai.ts +62 -0
  132. package/src/types/session.ts +59 -0
  133. package/src/utils/VersionController.test.ts +90 -0
  134. package/src/utils/VersionController.ts +64 -0
  135. package/src/utils/compass.ts +94 -0
  136. package/src/utils/fetch.ts +132 -0
  137. package/src/utils/filter.test.ts +120 -0
  138. package/src/utils/filter.ts +29 -0
  139. package/src/utils/uploadFIle.ts +8 -0
  140. package/src/utils/uuid.ts +9 -0
  141. package/tsconfig.json +26 -0
  142. package/vitest.config.ts +11 -0
@@ -0,0 +1,94 @@
1
+ import brotliPromise from 'brotli-wasm';
2
+
3
+ /**
4
+ * @title 字符串压缩器
5
+ */
6
+ export class StrCompressor {
7
+ /**
8
+ * @ignore
9
+ */
10
+ private instance!: {
11
+ compress(buf: Uint8Array, options?: any): Uint8Array;
12
+ decompress(buf: Uint8Array): Uint8Array;
13
+ };
14
+
15
+ async init(): Promise<void> {
16
+ this.instance = await brotliPromise; // Import is async in browsers due to wasm requirements!
17
+ }
18
+
19
+ /**
20
+ * @title 压缩字符串
21
+ * @param str - 要压缩的字符串
22
+ * @returns 压缩后的字符串
23
+ */
24
+ compress(str: string): string {
25
+ const input = new TextEncoder().encode(str);
26
+
27
+ const compressedData = this.instance.compress(input);
28
+
29
+ return this.urlSafeBase64Encode(compressedData);
30
+ }
31
+
32
+ /**
33
+ * @title 解压缩字符串
34
+ * @param str - 要解压缩的字符串
35
+ * @returns 解压缩后的字符串
36
+ */
37
+ decompress(str: string): string {
38
+ const compressedData = this.urlSafeBase64Decode(str);
39
+
40
+ const decompressedData = this.instance.decompress(compressedData);
41
+
42
+ return new TextDecoder().decode(decompressedData);
43
+ }
44
+
45
+ /**
46
+ * @title 异步压缩字符串
47
+ * @param str - 要压缩的字符串
48
+ * @returns Promise
49
+ */
50
+ async compressAsync(str: string) {
51
+ const brotli = await brotliPromise;
52
+
53
+ const input = new TextEncoder().encode(str);
54
+
55
+ const compressedData = brotli.compress(input);
56
+
57
+ return this.urlSafeBase64Encode(compressedData);
58
+ }
59
+
60
+ /**
61
+ * @title 异步解压缩字符串
62
+ * @param str - 要解压缩的字符串
63
+ * @returns Promise
64
+ */
65
+ async decompressAsync(str: string) {
66
+ const brotli = await brotliPromise;
67
+
68
+ const compressedData = this.urlSafeBase64Decode(str);
69
+
70
+ const decompressedData = brotli.decompress(compressedData);
71
+
72
+ return new TextDecoder().decode(decompressedData);
73
+ }
74
+
75
+ private urlSafeBase64Encode = (data: Uint8Array): string => {
76
+ const base64Str = btoa(String.fromCharCode(...data));
77
+ return base64Str.replaceAll('+', '_0_').replaceAll('/', '_').replace(/=+$/, '');
78
+ };
79
+
80
+ private urlSafeBase64Decode = (data: string): Uint8Array => {
81
+ let after = data.replaceAll('_0_', '+').replaceAll('_', '/');
82
+ while (after.length % 4) {
83
+ after += '=';
84
+ }
85
+
86
+ return new Uint8Array(
87
+ atob(after)
88
+ .split('')
89
+ .map((c) => c.charCodeAt(0)),
90
+ );
91
+ };
92
+ }
93
+
94
+ export const Compressor = new StrCompressor();
@@ -0,0 +1,132 @@
1
+ // import { notification } from '@/layout';
2
+ import { fetchChatModel } from '@/services/chatModel';
3
+ import { ChatMessageError } from '@/types/chatMessage';
4
+
5
+ const codeMessage: Record<number, string> = {
6
+ 200: '成功获取数据,服务已响应',
7
+ 201: '操作成功,数据已保存',
8
+ 202: '您的请求已进入后台排队,请耐心等待异步任务完成',
9
+ 204: '数据已成功删除',
10
+ 400: '很抱歉,您的请求出错,服务器未执行任何数据的创建或修改操作',
11
+ 401: '很抱歉,您的权限不足。请确认用户名或密码是否正确',
12
+ 403: '很抱歉,您无权访问此内容',
13
+ 404: '很抱歉,您请求的记录不存在,服务器未能执行任何操作',
14
+ 406: '很抱歉,服务器不支持该请求格式',
15
+ 410: '很抱歉,你所请求的资源已永久删除',
16
+ 422: '很抱歉,在创建对象时遇到验证错误,请稍后再试',
17
+ 500: '很抱歉,服务器出现了问题,请稍后再试',
18
+ 502: '很抱歉,您遇到了网关错误。这可能是由于网络故障或服务器问题导致的。请稍后再试,或联系管理员以获取更多帮助',
19
+ 503: '很抱歉,我们的服务器过载或处在维护中,服务暂时不可用',
20
+ 504: '很抱歉,网关请求超时,请稍后再试',
21
+ };
22
+
23
+ export interface FetchSSEOptions {
24
+ onErrorHandle?: (error: ChatMessageError) => void;
25
+ onMessageHandle?: (text: string) => void;
26
+ }
27
+
28
+ /**
29
+ * 使用流式方法获取数据
30
+ * @param fetchFn
31
+ * @param options
32
+ */
33
+ export const fetchSSE = async (fetchFn: () => Promise<Response>, options: FetchSSEOptions = {}) => {
34
+ const response = await fetchFn();
35
+
36
+ // 如果不 ok 说明有连接请求错误
37
+ if (!response.ok) {
38
+ const chatMessageError: ChatMessageError = {
39
+ message: codeMessage[response.status],
40
+ status: response.status,
41
+ type: 'general',
42
+ };
43
+
44
+ options.onErrorHandle?.(chatMessageError);
45
+ return;
46
+ }
47
+
48
+ const returnRes = response.clone();
49
+
50
+ const data = response.body;
51
+
52
+ if (!data) return;
53
+
54
+ const reader = data.getReader();
55
+ const decoder = new TextDecoder();
56
+
57
+ let done = false;
58
+
59
+ while (!done) {
60
+ const { value, done: doneReading } = await reader.read();
61
+ done = doneReading;
62
+ const chunkValue = decoder.decode(value);
63
+
64
+ options.onMessageHandle?.(chunkValue);
65
+ }
66
+
67
+ return returnRes;
68
+ };
69
+
70
+ interface FetchAITaskResultParams<T> {
71
+ abortController?: AbortController;
72
+ /**
73
+ * 错误处理函数
74
+ */
75
+ onError?: (e: Error) => void;
76
+ /**
77
+ * 加载状态变化处理函数
78
+ * @param loading - 是否处于加载状态
79
+ */
80
+ onLoadingChange?: (loading: boolean) => void;
81
+ /**
82
+ * 消息处理函数
83
+ * @param text - 消息内容
84
+ */
85
+ onMessageHandle?: (text: string) => void;
86
+
87
+ /**
88
+ * 请求对象
89
+ */
90
+ params: T;
91
+ }
92
+
93
+ export const fetchAIFactory =
94
+ <T>(fetcher: (params: T, signal?: AbortSignal) => Promise<Response>) =>
95
+ async ({
96
+ params,
97
+ onMessageHandle,
98
+ onError,
99
+ onLoadingChange,
100
+ abortController,
101
+ }: FetchAITaskResultParams<T>) => {
102
+ const errorHandle = (error: Error) => {
103
+ onLoadingChange?.(false);
104
+ if (abortController?.signal.aborted) {
105
+ // notification.primaryInfo({
106
+ // message: '已中断当前节点的执行任务',
107
+ // });
108
+ return;
109
+ }
110
+
111
+ // notification?.error({
112
+ // message: `请求失败(${error.message})`,
113
+ // placement: 'bottomRight',
114
+ // });
115
+ onError?.(error);
116
+ };
117
+
118
+ onLoadingChange?.(true);
119
+
120
+ const data = await fetchSSE(() => fetcher(params, abortController?.signal), {
121
+ onErrorHandle: (error) => {
122
+ errorHandle(new Error(error.message));
123
+ },
124
+ onMessageHandle,
125
+ }).catch(errorHandle);
126
+
127
+ onLoadingChange?.(false);
128
+
129
+ return await data?.text();
130
+ };
131
+
132
+ export const fetchPresetTaskResult = fetchAIFactory(fetchChatModel);
@@ -0,0 +1,120 @@
1
+ test('placeholder', () => {});
2
+ // describe('filterWithKeywords', () => {
3
+ // const data: Record<string, BaseDataModel> = {
4
+ // 1: {
5
+ // id: '1',
6
+ // meta: {
7
+ // title: 'hello world',
8
+ // description: 'test case',
9
+ // tag: ['a', 'b'],
10
+ // },
11
+ // },
12
+ // 2: {
13
+ // id: '2',
14
+ // meta: {
15
+ // title: 'goodbye',
16
+ // description: 'hello world',
17
+ // tag: ['c', 'd'],
18
+ // },
19
+ // },
20
+ // };
21
+ //
22
+ // it('should return an empty object if map is empty', () => {
23
+ // const result = filterWithKeywords({}, 'hello');
24
+ // expect(result).toEqual({});
25
+ // });
26
+ //
27
+ // it('should return the original map if keywords is empty', () => {
28
+ // const result = filterWithKeywords(data, '');
29
+ // expect(result).toEqual(data);
30
+ // });
31
+ //
32
+ // it('should return a filtered map if keywords is not empty', () => {
33
+ // const result = filterWithKeywords(data, 'world');
34
+ // expect(result).toEqual({
35
+ // 1: {
36
+ // id: '1',
37
+ // meta: {
38
+ // title: 'hello world',
39
+ // description: 'test case',
40
+ // tag: ['a', 'b'],
41
+ // },
42
+ // },
43
+ // 2: {
44
+ // id: '2',
45
+ // meta: {
46
+ // title: 'goodbye',
47
+ // description: 'hello world',
48
+ // tag: ['c', 'd'],
49
+ // },
50
+ // },
51
+ // });
52
+ // });
53
+ //
54
+ // it('should only consider title, description and tag properties if extraSearchStr is not provided', () => {
55
+ // const result = filterWithKeywords(data, 'test');
56
+ // expect(result).toEqual({
57
+ // 1: {
58
+ // id: '1',
59
+ // meta: {
60
+ // title: 'hello world',
61
+ // description: 'test case',
62
+ // tag: ['a', 'b'],
63
+ // },
64
+ // },
65
+ // });
66
+ // });
67
+ //
68
+ // it('should consider extraSearchStr in addition to title, description and tag properties if provided', () => {
69
+ // const extraSearchStr = (item: BaseDataModel) => {
70
+ // return item.meta.avatar || '';
71
+ // };
72
+ // const data: Record<string, BaseDataModel> = {
73
+ // a: {
74
+ // id: 'a',
75
+ // meta: {
76
+ // title: 'hello world',
77
+ // description: 'test case',
78
+ // tag: ['a', 'b'],
79
+ // avatar: 'xxx',
80
+ // },
81
+ // },
82
+ // b: {
83
+ // id: 'b',
84
+ // meta: {
85
+ // title: 'goodbye',
86
+ // description: 'hello world',
87
+ // tag: ['c', 'd'],
88
+ // avatar: 'yyy',
89
+ // },
90
+ // },
91
+ // };
92
+ //
93
+ // const result = filterWithKeywords(data, 'yyy', extraSearchStr);
94
+ // expect(result).toEqual({
95
+ // b: {
96
+ // id: 'b',
97
+ // meta: {
98
+ // title: 'goodbye',
99
+ // description: 'hello world',
100
+ // tag: ['c', 'd'],
101
+ // avatar: 'yyy',
102
+ // },
103
+ // },
104
+ // });
105
+ // });
106
+ //
107
+ // it('should ensure that each filtered object has at least one property that includes the keyword or extraSearchStr', () => {
108
+ // const result = filterWithKeywords(data, 't');
109
+ // expect(result).toEqual({
110
+ // 1: {
111
+ // id: '1',
112
+ // meta: {
113
+ // title: 'hello world',
114
+ // description: 'test case',
115
+ // tag: ['a', 'b'],
116
+ // },
117
+ // },
118
+ // });
119
+ // });
120
+ // });
@@ -0,0 +1,29 @@
1
+ import { BaseDataModel } from '@/types/meta';
2
+
3
+ export const filterWithKeywords = <T extends BaseDataModel>(
4
+ map: Record<string, T>,
5
+ keywords: string,
6
+ extraSearchStr?: (item: T) => string | string[],
7
+ ) => {
8
+ if (!keywords) return map;
9
+
10
+ return Object.fromEntries(
11
+ Object.entries(map).filter(([, item]) => {
12
+ const meta = item.meta;
13
+
14
+ const keyList = [meta.title, meta.description, meta.tag?.join('')].filter(
15
+ Boolean,
16
+ ) as string[];
17
+
18
+ const defaultSearchKey = keyList.join('');
19
+
20
+ let extraSearchKey: string = '';
21
+ if (extraSearchStr) {
22
+ const searchStr = extraSearchStr(item);
23
+ extraSearchKey = Array.isArray(searchStr) ? searchStr.join('') : searchStr;
24
+ }
25
+
26
+ return `${defaultSearchKey}${extraSearchKey}`.toLowerCase().includes(keywords.toLowerCase());
27
+ }),
28
+ );
29
+ };
@@ -0,0 +1,8 @@
1
+ export const createUploadImageHandler =
2
+ (onUploadImage: (base64: string) => void) => (file: any) => {
3
+ const reader = new FileReader();
4
+ reader.readAsDataURL(file);
5
+ reader.addEventListener('load', () => {
6
+ onUploadImage(String(reader.result));
7
+ });
8
+ };
@@ -0,0 +1,9 @@
1
+ // generate('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 16); //=> "4f90d13a42"
2
+ import { customAlphabet } from 'nanoid/non-secure';
3
+
4
+ export const nanoid = customAlphabet(
5
+ '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
6
+ 8,
7
+ );
8
+
9
+ export { v4 as uuid } from 'uuid';
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "lib": ["dom", "dom.iterable", "esnext"],
6
+ "allowJs": true,
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "module": "esnext",
13
+ "moduleResolution": "node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "jsx": "preserve",
17
+ "incremental": true,
18
+ "baseUrl": ".",
19
+ "types": ["vitest/globals"],
20
+ "paths": {
21
+ "@/*": ["src/*"]
22
+ }
23
+ },
24
+ "exclude": ["node_modules"],
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.d.ts", "**/*.tsx"]
26
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ globals: true,
7
+ alias: {
8
+ '@': './src',
9
+ },
10
+ },
11
+ });