@lobehub/chat 0.1.6

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 (149) 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 +20 -0
  29. package/README.md +147 -0
  30. package/locales/en_US/common.json +41 -0
  31. package/locales/en_US/setting.json +117 -0
  32. package/locales/zh_CN/common.json +41 -0
  33. package/locales/zh_CN/setting.json +117 -0
  34. package/next.config.mjs +32 -0
  35. package/package.json +140 -0
  36. package/public/next.svg +1 -0
  37. package/public/vercel.svg +1 -0
  38. package/scripts/genDefaultLocale.ts +18 -0
  39. package/scripts/genResources.ts +51 -0
  40. package/scripts/toc.ts +7 -0
  41. package/src/const/fetch.ts +1 -0
  42. package/src/const/modelTokens.ts +8 -0
  43. package/src/features/AvatarWithUpload/index.tsx +27 -0
  44. package/src/features/FolderPanel/index.tsx +55 -0
  45. package/src/features/SideBar/index.tsx +35 -0
  46. package/src/helpers/prompt.test.ts +36 -0
  47. package/src/helpers/prompt.ts +36 -0
  48. package/src/helpers/url.ts +17 -0
  49. package/src/layout/index.tsx +57 -0
  50. package/src/layout/style.ts +18 -0
  51. package/src/locales/create.ts +49 -0
  52. package/src/locales/default/common.ts +41 -0
  53. package/src/locales/default/setting.ts +117 -0
  54. package/src/locales/index.ts +5 -0
  55. package/src/locales/options.ts +19 -0
  56. package/src/locales/resources/en_US.ts +9 -0
  57. package/src/locales/resources/index.ts +13 -0
  58. package/src/locales/resources/zh_CN.ts +9 -0
  59. package/src/migrations/FromV0ToV1.ts +12 -0
  60. package/src/migrations/index.ts +13 -0
  61. package/src/pages/_app.page.tsx +13 -0
  62. package/src/pages/_document.page.tsx +70 -0
  63. package/src/pages/api/LangChainStream.ts +95 -0
  64. package/src/pages/api/chain.api.ts +17 -0
  65. package/src/pages/api/openai.api.ts +31 -0
  66. package/src/pages/chat/SessionList/Header.tsx +56 -0
  67. package/src/pages/chat/SessionList/List/SessionItem.tsx +90 -0
  68. package/src/pages/chat/SessionList/List/index.tsx +31 -0
  69. package/src/pages/chat/SessionList/List/style.ts +77 -0
  70. package/src/pages/chat/SessionList/index.tsx +18 -0
  71. package/src/pages/chat/[id]/Config/ConfigCell.tsx +68 -0
  72. package/src/pages/chat/[id]/Config/ReadMode.tsx +63 -0
  73. package/src/pages/chat/[id]/Config/index.tsx +79 -0
  74. package/src/pages/chat/[id]/Conversation/ChatList.tsx +36 -0
  75. package/src/pages/chat/[id]/Conversation/Input.tsx +61 -0
  76. package/src/pages/chat/[id]/Conversation/index.tsx +32 -0
  77. package/src/pages/chat/[id]/Header.tsx +86 -0
  78. package/src/pages/chat/[id]/edit/AgentConfig.tsx +95 -0
  79. package/src/pages/chat/[id]/edit/AgentMeta.tsx +117 -0
  80. package/src/pages/chat/[id]/edit/FormItem.tsx +26 -0
  81. package/src/pages/chat/[id]/edit/Prompt.tsx +68 -0
  82. package/src/pages/chat/[id]/edit/index.page.tsx +62 -0
  83. package/src/pages/chat/[id]/edit/style.ts +42 -0
  84. package/src/pages/chat/[id]/index.page.tsx +40 -0
  85. package/src/pages/chat/index.page.tsx +1 -0
  86. package/src/pages/chat/layout.tsx +51 -0
  87. package/src/pages/index.page.tsx +1 -0
  88. package/src/pages/setting/Header.tsx +27 -0
  89. package/src/pages/setting/SettingForm.tsx +299 -0
  90. package/src/pages/setting/SliderWithInput/index.tsx +40 -0
  91. package/src/pages/setting/ThemeSwatches/ThemeSwatchesNeutral.tsx +33 -0
  92. package/src/pages/setting/ThemeSwatches/ThemeSwatchesPrimary.tsx +33 -0
  93. package/src/pages/setting/ThemeSwatches/index.ts +2 -0
  94. package/src/pages/setting/index.page.tsx +41 -0
  95. package/src/prompts/agent.ts +65 -0
  96. package/src/services/chatModel.ts +34 -0
  97. package/src/services/langChain.ts +18 -0
  98. package/src/services/url.ts +8 -0
  99. package/src/store/middleware/createHashStorage.ts +49 -0
  100. package/src/store/session/index.ts +33 -0
  101. package/src/store/session/initialState.ts +11 -0
  102. package/src/store/session/selectors.ts +3 -0
  103. package/src/store/session/slices/agentConfig/action.ts +226 -0
  104. package/src/store/session/slices/agentConfig/index.ts +3 -0
  105. package/src/store/session/slices/agentConfig/initialState.ts +34 -0
  106. package/src/store/session/slices/agentConfig/selectors.ts +54 -0
  107. package/src/store/session/slices/chat/action.ts +210 -0
  108. package/src/store/session/slices/chat/index.ts +3 -0
  109. package/src/store/session/slices/chat/initialState.ts +12 -0
  110. package/src/store/session/slices/chat/messageReducer.test.ts +70 -0
  111. package/src/store/session/slices/chat/messageReducer.ts +84 -0
  112. package/src/store/session/slices/chat/selectors.ts +83 -0
  113. package/src/store/session/slices/session/action.ts +118 -0
  114. package/src/store/session/slices/session/index.ts +3 -0
  115. package/src/store/session/slices/session/initialState.ts +31 -0
  116. package/src/store/session/slices/session/reducers/session.test.ts +456 -0
  117. package/src/store/session/slices/session/reducers/session.ts +113 -0
  118. package/src/store/session/slices/session/selectors/chat.ts +4 -0
  119. package/src/store/session/slices/session/selectors/index.ts +20 -0
  120. package/src/store/session/slices/session/selectors/list.ts +65 -0
  121. package/src/store/session/store.ts +17 -0
  122. package/src/store/settings/action.ts +40 -0
  123. package/src/store/settings/index.ts +23 -0
  124. package/src/store/settings/initialState.ts +41 -0
  125. package/src/store/settings/selectors.ts +17 -0
  126. package/src/store/settings/store.ts +13 -0
  127. package/src/styles/antdOverride.ts +33 -0
  128. package/src/styles/global.ts +23 -0
  129. package/src/styles/index.ts +6 -0
  130. package/src/types/chatMessage.ts +46 -0
  131. package/src/types/exportConfig.ts +44 -0
  132. package/src/types/global.d.ts +14 -0
  133. package/src/types/i18next.d.ts +8 -0
  134. package/src/types/langchain.ts +34 -0
  135. package/src/types/llm.ts +49 -0
  136. package/src/types/locale.ts +1 -0
  137. package/src/types/meta.ts +26 -0
  138. package/src/types/openai.ts +62 -0
  139. package/src/types/session.ts +59 -0
  140. package/src/utils/VersionController.test.ts +90 -0
  141. package/src/utils/VersionController.ts +64 -0
  142. package/src/utils/compass.ts +94 -0
  143. package/src/utils/fetch.ts +132 -0
  144. package/src/utils/filter.test.ts +120 -0
  145. package/src/utils/filter.ts +29 -0
  146. package/src/utils/uploadFIle.ts +8 -0
  147. package/src/utils/uuid.ts +9 -0
  148. package/tsconfig.json +31 -0
  149. package/vitest.config.ts +11 -0
@@ -0,0 +1,34 @@
1
+ import { merge } from 'lodash-es';
2
+
3
+ import type { OpenAIStreamPayload } from '@/types/openai';
4
+
5
+ import { URLS } from './url';
6
+
7
+ /**
8
+ * 专门用于对话的 fetch
9
+ */
10
+ export const fetchChatModel = (
11
+ params: Partial<OpenAIStreamPayload>,
12
+ signal?: AbortSignal | undefined,
13
+ ) => {
14
+ const payload = merge(
15
+ {
16
+ frequency_penalty: 0,
17
+ model: 'gpt-3.5-turbo',
18
+ presence_penalty: 0,
19
+ stream: true,
20
+ temperature: 0.6,
21
+ top_p: 1,
22
+ },
23
+ params,
24
+ );
25
+
26
+ return fetch(URLS.openai, {
27
+ body: JSON.stringify(payload),
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ },
31
+ method: 'POST',
32
+ signal,
33
+ });
34
+ };
@@ -0,0 +1,18 @@
1
+ import { URLS } from '@/services/url';
2
+ import { LangChainParams } from '@/types/langchain';
3
+ import { fetchAIFactory } from '@/utils/fetch';
4
+
5
+ /**
6
+ * 专门用于 FlowChain 的 fetch
7
+ */
8
+ export const fetchLangChain = fetchAIFactory(
9
+ (params: LangChainParams, signal?: AbortSignal | undefined) =>
10
+ fetch(URLS.chain, {
11
+ body: JSON.stringify(params),
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ method: 'POST',
16
+ signal,
17
+ }),
18
+ );
@@ -0,0 +1,8 @@
1
+ const isDev = process.env.NODE_ENV === 'development';
2
+
3
+ const prefix = isDev ? '-dev' : '';
4
+
5
+ export const URLS = {
6
+ chain: '/api/chain' + prefix,
7
+ openai: '/api/openai' + prefix,
8
+ };
@@ -0,0 +1,49 @@
1
+ import { isEmpty } from 'lodash-es';
2
+ import { PersistStorage } from 'zustand/middleware';
3
+
4
+ import { Compressor } from '@/utils/compass';
5
+
6
+ export const createHashStorage = <T extends object>(): PersistStorage<T> => {
7
+ return {
8
+ getItem: async () => {
9
+ const searchParameters = new URLSearchParams(location.hash.slice(1));
10
+ const state: any = {};
11
+ const pool = [...searchParameters.entries()].map(async ([k, v]) => {
12
+ const string_ = await Compressor.decompressAsync(v);
13
+
14
+ try {
15
+ state[k] = JSON.parse(string_);
16
+ } catch {
17
+ state[k] = string_;
18
+ }
19
+ });
20
+
21
+ await Promise.all(pool);
22
+
23
+ return { state } as any;
24
+ },
25
+ removeItem: (key): void => {
26
+ const searchParameters = new URLSearchParams(location.hash.slice(1));
27
+ searchParameters.delete(key);
28
+ location.hash = searchParameters.toString();
29
+ },
30
+ setItem: async (_, newValue) => {
31
+ const searchParameters = new URLSearchParams(location.hash.slice(1));
32
+
33
+ const pool = Object.entries(newValue.state).map(async ([k, v]) => {
34
+ if (isEmpty(v)) {
35
+ searchParameters.delete(k);
36
+ return;
37
+ }
38
+ if (typeof v === 'string') {
39
+ searchParameters.set(k, await Compressor.compressAsync(v));
40
+ } else {
41
+ searchParameters.set(k, await Compressor.compressAsync(JSON.stringify(v)));
42
+ }
43
+ });
44
+ await Promise.all(pool);
45
+
46
+ location.hash = searchParameters.toString();
47
+ },
48
+ };
49
+ };
@@ -0,0 +1,33 @@
1
+ import { create } from 'zustand';
2
+ import { PersistOptions, devtools, persist } from 'zustand/middleware';
3
+
4
+ import { SessionStore, createStore } from './store';
5
+
6
+ type SessionPersist = Pick<SessionStore, 'sessions'>;
7
+
8
+ export const LOBE_CHAT = 'LOBE_CHAT';
9
+
10
+ const persistOptions: PersistOptions<SessionStore, SessionPersist> = {
11
+ name: LOBE_CHAT,
12
+
13
+ partialize: (s) => ({
14
+ sessions: s.sessions,
15
+ }),
16
+
17
+ // 手动控制 Hydration ,避免 ssr 报错
18
+ skipHydration: true,
19
+ version: 0,
20
+ // version: Migration.targetVersion,
21
+ };
22
+
23
+ export const useSessionStore = create<SessionStore>()(
24
+ persist(
25
+ devtools(createStore, {
26
+ name: LOBE_CHAT,
27
+ }),
28
+ persistOptions,
29
+ ),
30
+ );
31
+
32
+ export * from './selectors';
33
+ export type { SessionStore } from './store';
@@ -0,0 +1,11 @@
1
+ import { AgentConfigState, initialAgentConfigState } from './slices/agentConfig';
2
+ import { ChatState, initialChatState } from './slices/chat';
3
+ import { SessionState, initialSessionState } from './slices/session';
4
+
5
+ export type SessionStoreState = SessionState & ChatState & AgentConfigState;
6
+
7
+ export const initialState: SessionStoreState = {
8
+ ...initialSessionState,
9
+ ...initialChatState,
10
+ ...initialAgentConfigState,
11
+ };
@@ -0,0 +1,3 @@
1
+ export { agentSelectors } from './slices/agentConfig';
2
+ export { chatSelectors } from './slices/chat';
3
+ export { sessionSelectors } from './slices/session';
@@ -0,0 +1,226 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { promptPickEmoji, promptSummaryAgentName, promptSummaryDescription } from '@/prompts/agent';
4
+ import { SessionStore, sessionSelectors } from '@/store/session';
5
+ import { MetaData } from '@/types/meta';
6
+ import { LobeAgentConfig } from '@/types/session';
7
+ import { fetchPresetTaskResult } from '@/utils/fetch';
8
+
9
+ import { SessionLoadingState } from './initialState';
10
+
11
+ /**
12
+ * 代理行为接口
13
+ */
14
+ export interface AgentAction {
15
+ /**
16
+ * 自动选择表情
17
+ * @param id - 表情的 ID
18
+ */
19
+ autoPickEmoji: (id: string) => void;
20
+ /**
21
+ * 自动完成代理描述
22
+ * @param id - 代理的 ID
23
+ * @returns 一个 Promise,用于异步操作完成后的处理
24
+ */
25
+ autocompleteAgentDescription: (id: string) => Promise<void>;
26
+ /**
27
+ * 自动完成代理标题
28
+ * @param id - 代理的 ID
29
+ * @returns 一个 Promise,用于异步操作完成后的处理
30
+ */
31
+ autocompleteAgentTitle: (id: string) => Promise<void>;
32
+
33
+ autocompleteMeta: (key: keyof MetaData) => void;
34
+ /**
35
+ * 自动完成会话代理元数据
36
+ * @param id - 代理的 ID
37
+ */
38
+ autocompleteSessionAgentMeta: (id: string, replace?: boolean) => void;
39
+
40
+ /**
41
+ * 内部更新代理元数据
42
+ * @param id - 代理的 ID
43
+ * @returns 任意类型的返回值
44
+ */
45
+ internalUpdateAgentMeta: (id: string) => any;
46
+ /**
47
+ * 切换配置
48
+ * @param showPanel - 是否显示面板,默认为 true
49
+ */
50
+ toggleConfig: (showPanel?: boolean) => void;
51
+
52
+ /**
53
+ * 更新代理配置
54
+ * @param config - 部分 LobeAgentConfig 的配置
55
+ */
56
+ updateAgentConfig: (config: Partial<LobeAgentConfig>) => void;
57
+ updateAgentMeta: (meta: Partial<MetaData>) => void;
58
+ /**
59
+ * 更新加载状态
60
+ * @param key - SessionLoadingState 的键
61
+ * @param value - 加载状态的值
62
+ */
63
+ updateLoadingState: (key: keyof SessionLoadingState, value: boolean) => void;
64
+ }
65
+
66
+ export const createAgentSlice: StateCreator<
67
+ SessionStore,
68
+ [['zustand/devtools', never]],
69
+ [],
70
+ AgentAction
71
+ > = (set, get) => ({
72
+ autoPickEmoji: async (id) => {
73
+ const { dispatchSession } = get();
74
+ const session = sessionSelectors.getSessionById(id)(get());
75
+ if (!session) return;
76
+
77
+ const systemRole = session.config.systemRole;
78
+
79
+ const emoji = await fetchPresetTaskResult({
80
+ onLoadingChange: (loading) => {
81
+ get().updateLoadingState('avatar', loading);
82
+ },
83
+ params: promptPickEmoji(systemRole),
84
+ });
85
+
86
+ if (emoji) {
87
+ dispatchSession({ id, key: 'avatar', type: 'updateSessionMeta', value: emoji });
88
+ }
89
+ },
90
+
91
+ autocompleteAgentDescription: async (id) => {
92
+ const { dispatchSession, updateLoadingState, internalUpdateAgentMeta } = get();
93
+ const session = sessionSelectors.getSessionById(id)(get());
94
+ if (!session) return;
95
+
96
+ const systemRole = session.config.systemRole;
97
+
98
+ if (!systemRole) return;
99
+
100
+ const preValue = session.meta.description;
101
+
102
+ // 替换为 ...
103
+ dispatchSession({ id, key: 'description', type: 'updateSessionMeta', value: '...' });
104
+
105
+ fetchPresetTaskResult({
106
+ onError: () => {
107
+ dispatchSession({
108
+ id,
109
+ key: 'description',
110
+ type: 'updateSessionMeta',
111
+ value: preValue,
112
+ });
113
+ },
114
+ onLoadingChange: (loading) => {
115
+ updateLoadingState('description', loading);
116
+ },
117
+ onMessageHandle: internalUpdateAgentMeta(id)('description'),
118
+ params: promptSummaryDescription(systemRole),
119
+ });
120
+ },
121
+
122
+ autocompleteAgentTitle: async (id) => {
123
+ const { dispatchSession, updateLoadingState, internalUpdateAgentMeta } = get();
124
+ const session = sessionSelectors.getSessionById(id)(get());
125
+ if (!session) return;
126
+
127
+ const systemRole = session.config.systemRole;
128
+
129
+ if (!systemRole) return;
130
+
131
+ const previousTitle = session.meta.title;
132
+
133
+ // 替换为 ...
134
+ dispatchSession({ id, key: 'title', type: 'updateSessionMeta', value: '...' });
135
+
136
+ fetchPresetTaskResult({
137
+ onError: () => {
138
+ dispatchSession({ id, key: 'title', type: 'updateSessionMeta', value: previousTitle });
139
+ },
140
+ onLoadingChange: (loading) => {
141
+ updateLoadingState('title', loading);
142
+ },
143
+ onMessageHandle: internalUpdateAgentMeta(id)('title'),
144
+ params: promptSummaryAgentName(systemRole),
145
+ });
146
+ },
147
+
148
+ autocompleteMeta: (key) => {
149
+ const { activeId, autoPickEmoji, autocompleteAgentTitle, autocompleteAgentDescription } = get();
150
+ if (!activeId) return;
151
+
152
+ switch (key) {
153
+ case 'avatar': {
154
+ autoPickEmoji(activeId);
155
+ return;
156
+ }
157
+
158
+ case 'description': {
159
+ autocompleteAgentDescription(activeId);
160
+ return;
161
+ }
162
+
163
+ case 'title': {
164
+ autocompleteAgentTitle(activeId);
165
+ }
166
+ }
167
+ },
168
+
169
+ autocompleteSessionAgentMeta: (id, replace) => {
170
+ const session = sessionSelectors.getSessionById(id)(get());
171
+
172
+ if (!session) return;
173
+
174
+ if (!session.meta.title || replace) {
175
+ get().autocompleteAgentTitle(id);
176
+ }
177
+
178
+ if (!session.meta.description || replace) {
179
+ get().autocompleteAgentDescription(id);
180
+ }
181
+
182
+ if (!session.meta.avatar || replace) {
183
+ get().autoPickEmoji(id);
184
+ }
185
+ },
186
+ internalUpdateAgentMeta: (id: string) => (key: keyof MetaData) => {
187
+ let value = '';
188
+ return (text: string) => {
189
+ value += text;
190
+ get().dispatchSession({ id, key, type: 'updateSessionMeta', value });
191
+ };
192
+ },
193
+
194
+ toggleConfig: (newValue) => {
195
+ const showAgentSettings = typeof newValue === 'boolean' ? newValue : !get().showAgentSettings;
196
+
197
+ set({ showAgentSettings });
198
+ },
199
+
200
+ updateAgentConfig: (config) => {
201
+ const { activeId } = get();
202
+ const session = sessionSelectors.currentSession(get());
203
+ if (!activeId || !session) return;
204
+
205
+ get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' });
206
+ },
207
+ updateAgentMeta: (meta) => {
208
+ const { activeId } = get();
209
+ const session = sessionSelectors.currentSession(get());
210
+ if (!activeId || !session) return;
211
+
212
+ for (const [key, value] of Object.entries(meta)) {
213
+ if (value !== undefined) {
214
+ get().dispatchSession({
215
+ id: activeId,
216
+ key: key as keyof MetaData,
217
+ type: 'updateSessionMeta',
218
+ value,
219
+ });
220
+ }
221
+ }
222
+ },
223
+ updateLoadingState: (key, value) => {
224
+ set({ autocompleteLoading: { ...get().autocompleteLoading, [key]: value } });
225
+ },
226
+ });
@@ -0,0 +1,3 @@
1
+ export * from './action';
2
+ export * from './initialState';
3
+ export * from './selectors';
@@ -0,0 +1,34 @@
1
+ import { LanguageModel } from '@/types/llm';
2
+ import { MetaData } from '@/types/meta';
3
+ import { LobeAgentConfig } from '@/types/session';
4
+
5
+ export type SessionLoadingState = Record<Partial<keyof MetaData>, boolean>;
6
+
7
+ export interface AgentConfigState {
8
+ autocompleteLoading: SessionLoadingState;
9
+
10
+ showAgentSettings: boolean;
11
+ }
12
+
13
+ export const initialLobeAgentConfig: LobeAgentConfig = {
14
+ model: LanguageModel.GPT3_5,
15
+ params: { temperature: 0.6 },
16
+ systemRole: '',
17
+ };
18
+
19
+ export const DEFAULT_AVATAR = 'https://npm.elemecdn.com/@lobehub/assets-logo/assets/logo-3d.webp';
20
+
21
+ export const DEFAULT_TITLE = '默认对话';
22
+
23
+ export const initialAgentConfigState: AgentConfigState = {
24
+ // // loading 中间态
25
+ autocompleteLoading: {
26
+ avatar: false,
27
+ backgroundColor: false,
28
+ description: false,
29
+ tag: false,
30
+ title: false,
31
+ },
32
+
33
+ showAgentSettings: false,
34
+ };
@@ -0,0 +1,54 @@
1
+ import { SessionStore } from '@/store/session';
2
+ import { LanguageModel } from '@/types/llm';
3
+ import { MetaData } from '@/types/meta';
4
+ import { LobeAgentConfig } from '@/types/session';
5
+
6
+ import { sessionSelectors } from '../session';
7
+ import { DEFAULT_AVATAR, initialLobeAgentConfig } from './initialState';
8
+
9
+ const currentAgentMeta = (s: SessionStore): MetaData => {
10
+ const session = sessionSelectors.currentSession(s);
11
+
12
+ return session?.meta || {};
13
+ };
14
+
15
+ const currentAgentTitle = (s: SessionStore) => currentAgentMeta(s)?.title;
16
+
17
+ const currentAgentAvatar = (s: SessionStore) => {
18
+ const session = sessionSelectors.currentSession(s);
19
+
20
+ if (!session) return DEFAULT_AVATAR;
21
+
22
+ return session.meta.avatar || DEFAULT_AVATAR;
23
+ };
24
+
25
+ const currentAgentConfig = (s: SessionStore) => {
26
+ const session = sessionSelectors.currentSession(s);
27
+
28
+ return session?.config;
29
+ };
30
+
31
+ const currentAgentConfigSafe = (s: SessionStore): LobeAgentConfig => {
32
+ return currentAgentConfig(s) || initialLobeAgentConfig;
33
+ };
34
+
35
+ const currentAgentModel = (s: SessionStore): LanguageModel => {
36
+ const config = currentAgentConfig(s);
37
+
38
+ return config?.model || LanguageModel.GPT3_5;
39
+ };
40
+
41
+ const hasSystemRole = (s: SessionStore) => {
42
+ const config = currentAgentConfigSafe(s);
43
+
44
+ return !!config.systemRole;
45
+ };
46
+ export const agentSelectors = {
47
+ currentAgentAvatar,
48
+ currentAgentConfig,
49
+ currentAgentConfigSafe,
50
+ currentAgentMeta,
51
+ currentAgentModel,
52
+ currentAgentTitle,
53
+ hasSystemRole,
54
+ };
@@ -0,0 +1,210 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { fetchChatModel } from '@/services/chatModel';
4
+ import { SessionStore, agentSelectors, chatSelectors, sessionSelectors } from '@/store/session';
5
+ import { ChatMessage } from '@/types/chatMessage';
6
+ import { FetchSSEOptions, fetchSSE } from '@/utils/fetch';
7
+ import { nanoid } from '@/utils/uuid';
8
+
9
+ import { MessageDispatch, messagesReducer } from './messageReducer';
10
+
11
+ const LOADING_FLAT = '...';
12
+
13
+ /**
14
+ * 聊天操作
15
+ */
16
+ export interface ChatAction {
17
+ /**
18
+ * 清除消息
19
+ */
20
+ clearMessage: () => void;
21
+ /**
22
+ * 创建或发送消息
23
+ * @param text - 消息文本
24
+ */
25
+ createOrSendMsg: (text: string) => Promise<void>;
26
+ /**
27
+ * 删除消息
28
+ * @param id - 消息 ID
29
+ */
30
+ deleteMessage: (id: string) => void;
31
+ /**
32
+ * 分发消息
33
+ * @param payload - 消息分发参数
34
+ */
35
+ dispatchMessage: (payload: MessageDispatch) => void;
36
+ /**
37
+ * 生成消息
38
+ * @param messages - 聊天消息数组
39
+ * @param options - 获取 SSE 选项
40
+ */
41
+ generateMessage: (messages: ChatMessage[], options: FetchSSEOptions) => Promise<void>;
42
+ /**
43
+ * 处理消息编辑
44
+ * @param messageId - 消息 ID,可选
45
+ */
46
+ handleMessageEditing: (messageId: string | undefined) => void;
47
+ /**
48
+ * 实际获取 AI 响应
49
+ *
50
+ * @param messages - 聊天消息数组
51
+ * @param parentId - 父消息 ID,可选
52
+ */
53
+ realFetchAIResponse: (messages: ChatMessage[], parentId?: string) => Promise<void>;
54
+ /**
55
+ * 重新发送消息
56
+ * @param id - 消息 ID
57
+ */
58
+ resendMessage: (id: string) => Promise<void>;
59
+ /**
60
+ * 发送消息
61
+ * @param text - 消息文本
62
+ */
63
+ sendMessage: (text: string) => Promise<void>;
64
+ }
65
+
66
+ export const createChatSlice: StateCreator<
67
+ SessionStore,
68
+ [['zustand/devtools', never]],
69
+ [],
70
+ ChatAction
71
+ > = (set, get) => ({
72
+ clearMessage: () => {
73
+ get().dispatchMessage({ type: 'resetMessages' });
74
+ },
75
+
76
+ createOrSendMsg: async (message) => {
77
+ if (!message) return;
78
+
79
+ const { sendMessage, createSession } = get();
80
+ const session = sessionSelectors.currentSession(get());
81
+
82
+ if (!session) {
83
+ await createSession();
84
+ }
85
+
86
+ sendMessage(message);
87
+ },
88
+
89
+ deleteMessage: (id) => {
90
+ get().dispatchMessage({ id, type: 'deleteMessage' });
91
+ },
92
+
93
+ dispatchMessage: (payload) => {
94
+ const { activeId } = get();
95
+ const session = sessionSelectors.currentSession(get());
96
+ if (!activeId || !session) return;
97
+
98
+ const chats = messagesReducer(session.chats, payload);
99
+
100
+ get().dispatchSession({ chats, id: activeId, type: 'updateSessionChat' });
101
+ },
102
+
103
+ generateMessage: async (messages, options) => {
104
+ set({ chatLoading: true });
105
+ const config = agentSelectors.currentAgentConfigSafe(get());
106
+
107
+ const fetcher = () => fetchChatModel({ messages, model: config.model, ...config.params });
108
+
109
+ await fetchSSE(fetcher, options);
110
+
111
+ set({ chatLoading: false });
112
+ },
113
+
114
+ handleMessageEditing: (messageId) => {
115
+ set({ editingMessageId: messageId });
116
+ },
117
+
118
+ realFetchAIResponse: async (messages: ChatMessage[], parentId?: string) => {
119
+ const { dispatchMessage, generateMessage } = get();
120
+
121
+ // 添加 systemRole
122
+ const { systemRole } = agentSelectors.currentAgentConfigSafe(get());
123
+ if (systemRole) {
124
+ messages.unshift({ content: systemRole, role: 'system' } as ChatMessage);
125
+ }
126
+
127
+ // 再添加一个空的信息用于放置 ai 响应,注意顺序不能反
128
+ // 因为如果顺序反了,messages 中将包含新增的 ai message
129
+ const assistantId = nanoid();
130
+ const userId = parentId ?? nanoid();
131
+
132
+ dispatchMessage({
133
+ id: assistantId,
134
+ message: LOADING_FLAT,
135
+ parentId: userId,
136
+ role: 'assistant',
137
+ type: 'addMessage',
138
+ });
139
+
140
+ let output = '';
141
+ // 生成 ai message
142
+ await generateMessage(messages, {
143
+ onErrorHandle: (error) => {
144
+ dispatchMessage({ id: assistantId, key: 'error', type: 'updateMessage', value: error });
145
+ },
146
+ onMessageHandle: (text) => {
147
+ output += text;
148
+
149
+ dispatchMessage({
150
+ id: assistantId,
151
+ key: 'content',
152
+ type: 'updateMessage',
153
+ value: output,
154
+ });
155
+
156
+ // 滚动到最后一条消息
157
+ const item = document.querySelector('#for-loading');
158
+ if (!item) return;
159
+
160
+ item.scrollIntoView({ behavior: 'smooth' });
161
+ },
162
+ });
163
+ },
164
+
165
+ resendMessage: async (messageId) => {
166
+ const session = sessionSelectors.currentSession(get());
167
+
168
+ if (!session) return;
169
+
170
+ // 1. 构造所有相关的历史记录
171
+ const chats = chatSelectors.currentChats(get());
172
+
173
+ const currentIndex = chats.findIndex((c) => c.id === messageId);
174
+
175
+ const histories = chats
176
+ .slice(0, currentIndex + 1)
177
+ // 如果点击重新发送的 message 其 role 是 assistant,那么需要移除
178
+ // 如果点击重新发送的 message 其 role 是 user,则不需要移除
179
+ .filter((c) => !(c.role === 'assistant' && c.id === messageId));
180
+
181
+ if (histories.length <= 0) return;
182
+
183
+ const { realFetchAIResponse } = get();
184
+
185
+ const latestMsg = histories.filter((s) => s.role === 'user').at(-1);
186
+
187
+ if (!latestMsg) return;
188
+
189
+ await realFetchAIResponse(histories, latestMsg.id);
190
+ },
191
+
192
+ sendMessage: async (message) => {
193
+ const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta } = get();
194
+ const session = sessionSelectors.currentSession(get());
195
+ if (!session || !message) return;
196
+
197
+ const userId = nanoid();
198
+ dispatchMessage({ id: userId, message, role: 'user', type: 'addMessage' });
199
+
200
+ // 先拿到当前的 messages
201
+ const messages = chatSelectors.currentChats(get());
202
+
203
+ await realFetchAIResponse(messages);
204
+
205
+ const chats = chatSelectors.currentChats(get());
206
+ if (chats.length >= 4) {
207
+ autocompleteSessionAgentMeta(session.id);
208
+ }
209
+ },
210
+ });
@@ -0,0 +1,3 @@
1
+ export * from './action';
2
+ export * from './initialState';
3
+ export * from './selectors';