@lobehub/chat 0.19.0 → 0.20.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 (29) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/src/locales/default/common.ts +2 -2
  4. package/src/pages/chat/[id]/Config/SideBar.tsx +9 -5
  5. package/src/pages/chat/[id]/Config/Topic.tsx +27 -0
  6. package/src/pages/chat/[id]/Config/TopicItem.tsx +80 -0
  7. package/src/pages/chat/[id]/Conversation/Input/index.tsx +5 -2
  8. package/src/pages/chat/[id]/Header.tsx +14 -10
  9. package/src/pages/chat/layout.tsx +10 -4
  10. package/src/prompts/chat.ts +19 -0
  11. package/src/store/session/selectors.ts +1 -1
  12. package/src/store/session/slices/agentConfig/selectors.ts +4 -0
  13. package/src/store/session/slices/chat/actions/index.ts +21 -0
  14. package/src/store/session/slices/chat/{action.ts → actions/message.ts} +33 -23
  15. package/src/store/session/slices/chat/actions/topic.ts +98 -0
  16. package/src/store/session/slices/chat/index.ts +1 -1
  17. package/src/store/session/slices/chat/initialState.ts +2 -5
  18. package/src/store/session/slices/chat/reducers/message.test.ts +92 -0
  19. package/src/store/session/slices/chat/reducers/message.ts +16 -1
  20. package/src/store/session/slices/chat/reducers/topic.test.ts +179 -0
  21. package/src/store/session/slices/chat/reducers/topic.ts +59 -0
  22. package/src/store/session/slices/chat/selectors/chat.ts +8 -4
  23. package/src/store/session/slices/chat/selectors/index.ts +5 -0
  24. package/src/store/session/slices/chat/selectors/topic.ts +16 -0
  25. package/src/store/session/slices/chat/selectors/utils.test.ts +81 -1
  26. package/src/store/session/slices/chat/selectors/utils.ts +8 -2
  27. package/src/store/session/slices/session/reducers/session.ts +25 -1
  28. package/src/types/topic.ts +0 -1
  29. package/src/pages/chat/[id]/Config/ConfigCell.tsx +0 -68
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.20.0](https://github.com/lobehub/lobe-chat/compare/v0.19.0...v0.20.0)
6
+
7
+ <sup>Released on **2023-07-25**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: 实现话题模块.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: 实现话题模块, closes [#16](https://github.com/lobehub/lobe-chat/issues/16) ([64fd6ee](https://github.com/lobehub/lobe-chat/commit/64fd6ee))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 0.19.0](https://github.com/lobehub/lobe-chat/compare/v0.18.2...v0.19.0)
6
31
 
7
32
  <sup>Released on **2023-07-24**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -27,8 +27,8 @@ export default {
27
27
  share: '分享',
28
28
  tokenDetail: '系统设定: {{systemRoleToken}} 历史消息: {{chatsToken}}',
29
29
  topic: {
30
- saveCurrentMessages: '保存当前对话为话题',
31
- searchPlaceholder: '搜索归档对话...',
30
+ saveCurrentMessages: '将当前会话保存为话题',
31
+ searchPlaceholder: '搜索话题...',
32
32
  },
33
33
  updateAgent: '更新助理信息',
34
34
  updatePrompt: '更新提示词',
@@ -1,11 +1,12 @@
1
1
  import { ActionIcon, DraggablePanelBody, EditableMessage, SearchBar } from '@lobehub/ui';
2
2
  import { createStyles } from 'antd-style';
3
- import { ChevronRight } from 'lucide-react';
3
+ import { Maximize2Icon } from 'lucide-react';
4
4
  import { memo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
  import { shallow } from 'zustand/shallow';
8
8
 
9
+ import { Topic } from '@/pages/chat/[id]/Config/Topic';
9
10
  import { agentSelectors, useSessionStore } from '@/store/session';
10
11
 
11
12
  import Header from './Header';
@@ -42,8 +43,10 @@ const useStyles = createStyles(({ css, token }) => ({
42
43
  const SideBar = memo(() => {
43
44
  const [openModal, setOpenModal] = useState(false);
44
45
  const { styles } = useStyles();
45
- const [updateAgentConfig] = useSessionStore((s) => [s.updateAgentConfig], shallow);
46
- const systemRole = useSessionStore(agentSelectors.currentAgentSystemRole, shallow);
46
+ const [systemRole, updateAgentConfig] = useSessionStore(
47
+ (s) => [agentSelectors.currentAgentSystemRole(s), s.updateAgentConfig],
48
+ shallow,
49
+ );
47
50
 
48
51
  const { t } = useTranslation('common');
49
52
  return (
@@ -51,7 +54,7 @@ const SideBar = memo(() => {
51
54
  <Header
52
55
  actions={
53
56
  <ActionIcon
54
- icon={ChevronRight}
57
+ icon={Maximize2Icon}
55
58
  onClick={() => setOpenModal(true)}
56
59
  size="small"
57
60
  title={t('edit')}
@@ -76,8 +79,9 @@ const SideBar = memo(() => {
76
79
  }}
77
80
  value={systemRole}
78
81
  />
79
- <Flexbox style={{ padding: 16 }}>
82
+ <Flexbox gap={12} style={{ padding: 16 }}>
80
83
  <SearchBar placeholder={t('topic.searchPlaceholder')} type={'block'} />
84
+ <Topic />
81
85
  </Flexbox>
82
86
  </DraggablePanelBody>
83
87
  );
@@ -0,0 +1,27 @@
1
+ import { Flexbox } from 'react-layout-kit';
2
+ import { shallow } from 'zustand/shallow';
3
+
4
+ import { topicSelectors, useSessionStore } from '@/store/session';
5
+
6
+ import TopicItem from './TopicItem';
7
+
8
+ export const Topic = () => {
9
+ const topics = useSessionStore(topicSelectors.currentTopics);
10
+ const [activeTopicId] = useSessionStore((s) => [s.activeTopicId], shallow);
11
+ return (
12
+ <Flexbox gap={8}>
13
+ <TopicItem active={!activeTopicId} fav={false} title={'默认话题'} />
14
+
15
+ {topics.map(({ id, favorite, title }) => (
16
+ <TopicItem
17
+ active={activeTopicId === id}
18
+ fav={favorite}
19
+ id={id}
20
+ key={id}
21
+ showFav
22
+ title={title}
23
+ />
24
+ ))}
25
+ </Flexbox>
26
+ );
27
+ };
@@ -0,0 +1,80 @@
1
+ import { StarFilled, StarOutlined } from '@ant-design/icons';
2
+ import { ActionIcon } from '@lobehub/ui';
3
+ import { createStyles } from 'antd-style';
4
+ import { LucideIcon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { Flexbox } from 'react-layout-kit';
7
+ import { shallow } from 'zustand/shallow';
8
+
9
+ import { useSessionStore } from '@/store/session';
10
+
11
+ const useStyles = createStyles(({ css, token }) => ({
12
+ active: css`
13
+ background: ${token.colorFill};
14
+
15
+ &:hover {
16
+ background: ${token.colorFill};
17
+ }
18
+ `,
19
+ container: css`
20
+ cursor: pointer;
21
+ background: ${token.colorFillTertiary};
22
+ border-radius: 6px;
23
+
24
+ &:hover {
25
+ background: ${token.colorFillSecondary};
26
+ }
27
+ `,
28
+ split: css`
29
+ border-bottom: 1px solid ${token.colorSplit};
30
+ `,
31
+ }));
32
+
33
+ export interface ConfigCellProps {
34
+ active?: boolean;
35
+ fav?: boolean;
36
+ id?: string;
37
+ showFav?: boolean;
38
+ title: string;
39
+ }
40
+
41
+ const TopicItem = memo<ConfigCellProps>(({ title, active, id, showFav, fav }) => {
42
+ const { styles, theme, cx } = useStyles();
43
+
44
+ const [dispatchTopic, toggleTopic] = useSessionStore(
45
+ (s) => [s.dispatchTopic, s.toggleTopic],
46
+ shallow,
47
+ );
48
+ const starIcon = (fav ? StarFilled : StarOutlined) as LucideIcon;
49
+
50
+ return (
51
+ <Flexbox
52
+ align={'center'}
53
+ className={cx(styles.container, active && styles.active)}
54
+ distribution={'space-between'}
55
+ horizontal
56
+ onClick={() => {
57
+ toggleTopic(id);
58
+ }}
59
+ padding={'10px 12px'}
60
+ >
61
+ {title}
62
+ {!showFav ? undefined : (
63
+ <ActionIcon
64
+ icon={starIcon}
65
+ onClick={() => {
66
+ if (!id) return;
67
+
68
+ dispatchTopic({ id, key: 'favorite', type: 'updateChatTopic', value: !fav });
69
+ }}
70
+ size={'small'}
71
+ style={{
72
+ color: fav ? theme.yellow : undefined,
73
+ }}
74
+ />
75
+ )}
76
+ </Flexbox>
77
+ );
78
+ });
79
+
80
+ export default TopicItem;
@@ -18,12 +18,15 @@ const ChatInput = () => {
18
18
  const [text, setText] = useState('');
19
19
 
20
20
  const [inputHeight] = useSettings((s) => [s.inputHeight], shallow);
21
- const [sendMessage] = useSessionStore((s) => [s.createOrSendMsg], shallow);
21
+ const [sendMessage, saveToTopic] = useSessionStore(
22
+ (s) => [s.createOrSendMsg, s.saveToTopic],
23
+ shallow,
24
+ );
22
25
 
23
26
  const footer = useMemo(
24
27
  () => (
25
28
  <Tooltip title={t('topic.saveCurrentMessages')}>
26
- <Button icon={<Icon icon={LucideGalleryVerticalEnd} />} />
29
+ <Button icon={<Icon icon={LucideGalleryVerticalEnd} />} onClick={saveToTopic} />
27
30
  </Tooltip>
28
31
  ),
29
32
  [],
@@ -12,10 +12,18 @@ import { agentSelectors, useSessionStore } from '@/store/session';
12
12
  const Header = memo(() => {
13
13
  const { t } = useTranslation('common');
14
14
 
15
- const [meta, id, modle] = useSessionStore(
16
- (s) => [agentSelectors.currentAgentMeta(s), s.activeId, agentSelectors.currentAgentModel(s)],
15
+ const [title, description, avatar, backgroundColor, id, model] = useSessionStore(
16
+ (s) => [
17
+ agentSelectors.currentAgentTitle(s),
18
+ agentSelectors.currentAgentDescription(s),
19
+ agentSelectors.currentAgentAvatar(s),
20
+ agentSelectors.currentAgentBackgroundColor(s),
21
+ s.activeId,
22
+ agentSelectors.currentAgentModel(s),
23
+ ],
17
24
  shallow,
18
25
  );
26
+
19
27
  const [showAgentSettings, toggleConfig] = useSessionStore(
20
28
  (s) => [s.showAgentSettings, s.toggleConfig],
21
29
  shallow,
@@ -26,20 +34,16 @@ const Header = memo(() => {
26
34
  left={
27
35
  <>
28
36
  <Avatar
29
- avatar={meta?.avatar}
30
- background={meta?.backgroundColor}
37
+ avatar={avatar}
38
+ background={backgroundColor}
31
39
  onClick={() => {
32
40
  Router.push(`/chat/${id}/edit`);
33
41
  }}
34
42
  size={40}
35
43
  style={{ cursor: 'pointer' }}
36
- title={meta?.title}
37
- />
38
- <HeaderTitle
39
- desc={meta?.description || t('noDescription')}
40
- tag={<Tag>{modle}</Tag>}
41
- title={meta?.title || t('defaultAgent')}
44
+ title={title}
42
45
  />
46
+ <HeaderTitle desc={description} tag={<Tag>{model}</Tag>} title={title} />
43
47
  </>
44
48
  }
45
49
  right={
@@ -17,8 +17,8 @@ const ChatLayout = memo<PropsWithChildren>(({ children }) => {
17
17
  initI18n.finally();
18
18
  }, []);
19
19
 
20
- const [activeSession] = useSessionStore((s) => {
21
- return [s.activeSession];
20
+ const [activeSession, toggleTopic] = useSessionStore((s) => {
21
+ return [s.activeSession, s.toggleTopic];
22
22
  }, shallow);
23
23
 
24
24
  const router = useRouter();
@@ -26,10 +26,16 @@ const ChatLayout = memo<PropsWithChildren>(({ children }) => {
26
26
 
27
27
  useEffect(() => {
28
28
  const hasRehydrated = useSessionStore.persist.hasHydrated();
29
- // 只有当水合完毕后,才能正常去激活会话
30
- if (typeof id === 'string' && hasRehydrated) {
29
+ // 只有当水合完毕后再开始做操作
30
+ if (!hasRehydrated) return;
31
+
32
+ // 1. 正常激活会话
33
+ if (typeof id === 'string') {
31
34
  activeSession(id);
32
35
  }
36
+
37
+ // 将话题重置为默认值
38
+ toggleTopic();
33
39
  }, [id]);
34
40
 
35
41
  useEffect(() => {
@@ -0,0 +1,19 @@
1
+ import { OpenAIChatMessage, OpenAIStreamPayload } from '@/types/openai';
2
+
3
+ export const promptSummaryTitle = (
4
+ messages: OpenAIChatMessage[],
5
+ ): Partial<OpenAIStreamPayload> => ({
6
+ messages: [
7
+ {
8
+ content:
9
+ '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不需要包含标点符号',
10
+ role: 'system',
11
+ },
12
+ {
13
+ content: `${messages.map((message) => `${message.role}: ${message.content}`).join('\n')}
14
+
15
+ 请总结上述对话为10个字以内的标题,不需要包含标点符号`,
16
+ role: 'user',
17
+ },
18
+ ],
19
+ });
@@ -1,3 +1,3 @@
1
1
  export { agentSelectors } from './slices/agentConfig';
2
- export { chatSelectors } from './slices/chat';
2
+ export { chatSelectors, topicSelectors } from './slices/chat';
3
3
  export { sessionSelectors } from './slices/session';
@@ -17,6 +17,9 @@ const currentAgentMeta = (s: SessionStore): MetaData => {
17
17
 
18
18
  const currentAgentTitle = (s: SessionStore) => currentAgentMeta(s)?.title || t('defaultSession');
19
19
 
20
+ const currentAgentDescription = (s: SessionStore) =>
21
+ currentAgentMeta(s)?.description || t('noDescription');
22
+
20
23
  const currentAgentBackgroundColor = (s: SessionStore) => {
21
24
  const session = sessionSelectors.currentSession(s);
22
25
  if (!session) return DEFAULT_BACKGROUND_COLOR;
@@ -62,6 +65,7 @@ export const agentSelectors = {
62
65
  currentAgentBackgroundColor,
63
66
  currentAgentConfig,
64
67
  currentAgentConfigSafe,
68
+ currentAgentDescription,
65
69
  currentAgentMeta,
66
70
  currentAgentModel,
67
71
  currentAgentSystemRole,
@@ -0,0 +1,21 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { SessionStore } from '@/store/session';
4
+
5
+ import { ChatMessageAction, chatMessage } from './message';
6
+ import { ChatTopicAction, chatTopic } from './topic';
7
+
8
+ /**
9
+ * 聊天操作
10
+ */
11
+ export interface ChatAction extends ChatTopicAction, ChatMessageAction {}
12
+
13
+ export const createChatSlice: StateCreator<
14
+ SessionStore,
15
+ [['zustand/devtools', never]],
16
+ [],
17
+ ChatAction
18
+ > = (...params) => ({
19
+ ...chatTopic(...params),
20
+ ...chatMessage(...params),
21
+ });
@@ -7,14 +7,14 @@ import { fetchSSE } from '@/utils/fetch';
7
7
  import { isFunctionMessage } from '@/utils/message';
8
8
  import { nanoid } from '@/utils/uuid';
9
9
 
10
- import { MessageDispatch, messagesReducer } from './reducers/message';
10
+ import { MessageDispatch, messagesReducer } from '../reducers/message';
11
11
 
12
12
  const LOADING_FLAT = '...';
13
13
 
14
14
  /**
15
15
  * 聊天操作
16
16
  */
17
- export interface ChatAction {
17
+ export interface ChatMessageAction {
18
18
  /**
19
19
  * 清除消息
20
20
  */
@@ -57,6 +57,7 @@ export interface ChatAction {
57
57
  * @param id - 消息 ID
58
58
  */
59
59
  resendMessage: (id: string) => Promise<void>;
60
+
60
61
  /**
61
62
  * 发送消息
62
63
  * @param text - 消息文本
@@ -64,14 +65,20 @@ export interface ChatAction {
64
65
  sendMessage: (text: string) => Promise<void>;
65
66
  }
66
67
 
67
- export const createChatSlice: StateCreator<
68
+ export const chatMessage: StateCreator<
68
69
  SessionStore,
69
70
  [['zustand/devtools', never]],
70
71
  [],
71
- ChatAction
72
+ ChatMessageAction
72
73
  > = (set, get) => ({
73
74
  clearMessage: () => {
74
- get().dispatchMessage({ type: 'resetMessages' });
75
+ const { dispatchMessage, activeTopicId, dispatchTopic } = get();
76
+
77
+ dispatchMessage({ topicId: activeTopicId, type: 'resetMessages' });
78
+
79
+ if (activeTopicId) {
80
+ dispatchTopic({ id: activeTopicId, type: 'deleteChatTopic' });
81
+ }
75
82
  },
76
83
 
77
84
  createOrSendMsg: async (message) => {
@@ -155,7 +162,7 @@ export const createChatSlice: StateCreator<
155
162
  },
156
163
 
157
164
  realFetchAIResponse: async (messages, userMessageId) => {
158
- const { dispatchMessage, generateMessage } = get();
165
+ const { dispatchMessage, generateMessage, activeTopicId } = get();
159
166
 
160
167
  // 添加 systemRole
161
168
  const { systemRole, model } = agentSelectors.currentAgentConfigSafe(get());
@@ -165,49 +172,46 @@ export const createChatSlice: StateCreator<
165
172
 
166
173
  // 再添加一个空的信息用于放置 ai 响应,注意顺序不能反
167
174
  // 因为如果顺序反了,messages 中将包含新增的 ai message
168
- const assistantId = nanoid();
175
+ const mid = nanoid();
169
176
 
170
177
  dispatchMessage({
171
- id: assistantId,
178
+ id: mid,
172
179
  message: LOADING_FLAT,
173
180
  parentId: userMessageId,
174
181
  role: 'assistant',
175
182
  type: 'addMessage',
176
183
  });
177
184
 
185
+ // 如果有 activeTopicId,则添加 topicId
186
+ if (activeTopicId) {
187
+ dispatchMessage({ id: mid, key: 'topicId', type: 'updateMessage', value: activeTopicId });
188
+ }
189
+
178
190
  // 为模型添加 fromModel 的额外信息
179
- dispatchMessage({
180
- id: assistantId,
181
- key: 'fromModel',
182
- type: 'updateMessageExtra',
183
- value: model,
184
- });
191
+ dispatchMessage({ id: mid, key: 'fromModel', type: 'updateMessageExtra', value: model });
185
192
 
186
193
  // 生成 ai message
187
- const { output, isFunctionCall } = await generateMessage(messages, assistantId);
194
+ const { output, isFunctionCall } = await generateMessage(messages, mid);
188
195
 
189
196
  // 如果是 function,则发送函数调用方法
190
197
  if (isFunctionCall) {
191
198
  const { function_call } = JSON.parse(output);
192
199
 
193
200
  dispatchMessage({
194
- id: assistantId,
201
+ id: mid,
195
202
  key: 'function_call',
196
203
  type: 'updateMessage',
197
204
  value: function_call,
198
205
  });
199
206
 
200
207
  await generateMessage(
201
- [
202
- ...messages,
203
- { content: '', function_call, id: assistantId, role: 'assistant' } as ChatMessage,
204
- ],
205
- assistantId,
208
+ [...messages, { content: '', function_call, id: mid, role: 'assistant' } as ChatMessage],
209
+ mid,
206
210
  true,
207
211
  );
208
212
 
209
213
  dispatchMessage({
210
- id: assistantId,
214
+ id: mid,
211
215
  key: 'role',
212
216
  type: 'updateMessage',
213
217
  value: 'assistant',
@@ -243,13 +247,19 @@ export const createChatSlice: StateCreator<
243
247
  },
244
248
 
245
249
  sendMessage: async (message) => {
246
- const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta } = get();
250
+ const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta, activeTopicId } =
251
+ get();
247
252
  const session = sessionSelectors.currentSession(get());
248
253
  if (!session || !message) return;
249
254
 
250
255
  const userId = nanoid();
251
256
  dispatchMessage({ id: userId, message, role: 'user', type: 'addMessage' });
252
257
 
258
+ // 如果有 activeTopicId,则添加 topicId
259
+ if (activeTopicId) {
260
+ dispatchMessage({ id: userId, key: 'topicId', type: 'updateMessage', value: activeTopicId });
261
+ }
262
+
253
263
  // 先拿到当前的 messages
254
264
  const messages = chatSelectors.currentChats(get());
255
265
 
@@ -0,0 +1,98 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { promptSummaryTitle } from '@/prompts/chat';
4
+ import { SessionStore, chatSelectors, sessionSelectors } from '@/store/session';
5
+ import { fetchPresetTaskResult } from '@/utils/fetch';
6
+ import { nanoid } from '@/utils/uuid';
7
+
8
+ import { ChatTopicDispatch, topicReducer } from '../reducers/topic';
9
+
10
+ export interface ChatTopicAction {
11
+ /**
12
+ * 分发主题
13
+ * @param payload - 要分发的主题
14
+ */
15
+ dispatchTopic: (payload: ChatTopicDispatch) => void;
16
+ /**
17
+ * 将当前消息保存为主题
18
+ */
19
+ saveToTopic: () => void;
20
+ /**
21
+ * 切换主题
22
+ * @param id - 要切换的主题的 ID
23
+ */
24
+ toggleTopic: (id?: string) => void;
25
+ /**
26
+ * 更新主题加载状态
27
+ * @param id - 要更新的主题的 ID
28
+ */
29
+ updateTopicLoading: (id?: string) => void;
30
+ }
31
+ export const chatTopic: StateCreator<
32
+ SessionStore,
33
+ [['zustand/devtools', never]],
34
+ [],
35
+ ChatTopicAction
36
+ > = (set, get) => ({
37
+ dispatchTopic: (payload) => {
38
+ const { activeId } = get();
39
+ const session = sessionSelectors.currentSession(get());
40
+ if (!activeId || !session) return;
41
+
42
+ const topics = topicReducer(session.topics || {}, payload);
43
+
44
+ get().dispatchSession({ id: activeId, topics, type: 'updateSessionTopic' });
45
+ },
46
+ saveToTopic: () => {
47
+ const session = sessionSelectors.currentSession(get());
48
+ if (!session) return;
49
+
50
+ const { dispatchTopic, dispatchMessage, updateTopicLoading } = get();
51
+ // 获取当前的 messages
52
+ const messages = chatSelectors.currentChats(get());
53
+
54
+ const topicId = nanoid();
55
+
56
+ const defaultTitle = '默认话题';
57
+ const newTopic = {
58
+ createAt: Date.now(),
59
+ id: topicId,
60
+ title: defaultTitle,
61
+ updateAt: Date.now(),
62
+ };
63
+
64
+ dispatchTopic({
65
+ topic: newTopic,
66
+ type: 'addChatTopic',
67
+ });
68
+
69
+ // 为所有 message 添加 topicId
70
+ for (const m of messages) {
71
+ dispatchMessage({ id: m.id, key: 'topicId', type: 'updateMessage', value: topicId });
72
+ }
73
+
74
+ let output = '';
75
+
76
+ // 自动总结话题标题
77
+ fetchPresetTaskResult({
78
+ onError: () => {
79
+ dispatchTopic({ id: topicId, key: 'title', type: 'updateChatTopic', value: defaultTitle });
80
+ },
81
+ onLoadingChange: (loading) => {
82
+ updateTopicLoading(loading ? topicId : undefined);
83
+ },
84
+ onMessageHandle: (x) => {
85
+ output += x;
86
+ dispatchTopic({ id: topicId, key: 'title', type: 'updateChatTopic', value: output });
87
+ },
88
+ params: promptSummaryTitle(messages),
89
+ });
90
+ },
91
+ toggleTopic: (id) => {
92
+ set({ activeTopicId: id });
93
+ },
94
+
95
+ updateTopicLoading: (id) => {
96
+ set({ topicLoadingId: id });
97
+ },
98
+ });
@@ -1,3 +1,3 @@
1
- export * from './action';
1
+ export * from './actions';
2
2
  export * from './initialState';
3
3
  export * from './selectors';
@@ -1,12 +1,9 @@
1
1
  export interface ChatState {
2
+ activeTopicId?: string;
2
3
  chatLoading: boolean;
3
- editingMessageId?: string;
4
+ topicLoadingId?: string;
4
5
  }
5
6
 
6
7
  export const initialChatState: ChatState = {
7
8
  chatLoading: false,
8
-
9
- // activeId: null,
10
- // searchKeywords: '',
11
- //
12
9
  };
@@ -116,6 +116,30 @@ describe('messagesReducer', () => {
116
116
  quotaId: 'message2',
117
117
  });
118
118
  });
119
+
120
+ it('should use the provided parentId and quotaId when adding a new message', () => {
121
+ const payload: MessageDispatch = {
122
+ type: 'addMessage',
123
+ message: 'New Message',
124
+ id: 'message3',
125
+ role: 'user',
126
+ parentId: 'message1',
127
+ quotaId: 'message2',
128
+ };
129
+
130
+ const newState = messagesReducer(initialState, payload);
131
+
132
+ expect(newState.message3).toEqual({
133
+ id: 'message3',
134
+ content: 'New Message',
135
+ meta: {},
136
+ createAt: expect.any(Number),
137
+ updateAt: expect.any(Number),
138
+ role: 'user',
139
+ parentId: 'message1',
140
+ quotaId: 'message2',
141
+ });
142
+ });
119
143
  });
120
144
 
121
145
  describe('deleteMessage', () => {
@@ -142,6 +166,17 @@ describe('messagesReducer', () => {
142
166
 
143
167
  expect(newState).toEqual(initialState);
144
168
  });
169
+
170
+ it('should not modify the state if the specified message does not exist', () => {
171
+ const payload: MessageDispatch = {
172
+ type: 'deleteMessage',
173
+ id: 'nonexistentMessage',
174
+ };
175
+
176
+ const newState = messagesReducer(initialState, payload);
177
+
178
+ expect(newState).toEqual(initialState);
179
+ });
145
180
  });
146
181
 
147
182
  describe('updateMessage', () => {
@@ -171,6 +206,19 @@ describe('messagesReducer', () => {
171
206
 
172
207
  expect(newState).toEqual(initialState);
173
208
  });
209
+
210
+ it('should not modify the state if the specified message does not exist', () => {
211
+ const payload: MessageDispatch = {
212
+ type: 'updateMessage',
213
+ id: 'nonexistentMessage',
214
+ key: 'content',
215
+ value: 'Updated Message',
216
+ };
217
+
218
+ const newState = messagesReducer(initialState, payload);
219
+
220
+ expect(newState).toEqual(initialState);
221
+ });
174
222
  });
175
223
 
176
224
  describe('updateMessageExtra', () => {
@@ -200,6 +248,19 @@ describe('messagesReducer', () => {
200
248
 
201
249
  expect(newState).toEqual(initialState);
202
250
  });
251
+
252
+ it('should not modify the state if the specified message does not exist', () => {
253
+ const payload: MessageDispatch = {
254
+ type: 'updateMessageExtra',
255
+ id: 'nonexistentMessage',
256
+ key: 'translate',
257
+ value: { target: 'en', to: 'zh' },
258
+ };
259
+
260
+ const newState = messagesReducer(initialState, payload);
261
+
262
+ expect(newState).toEqual(initialState);
263
+ });
203
264
  });
204
265
 
205
266
  describe('resetMessages', () => {
@@ -212,6 +273,37 @@ describe('messagesReducer', () => {
212
273
 
213
274
  expect(newState).toEqual({});
214
275
  });
276
+
277
+ it('should delete messages with the specified topicId', () => {
278
+ const initialState = {
279
+ message1: {
280
+ id: 'message1',
281
+ content: 'Hello World',
282
+ createAt: 1629264000000,
283
+ updateAt: 1629264000000,
284
+ role: 'user',
285
+ topicId: 'topic1',
286
+ },
287
+ message2: {
288
+ id: 'message2',
289
+ content: 'How are you?',
290
+ createAt: 1629264000000,
291
+ updateAt: 1629264000000,
292
+ role: 'system',
293
+ },
294
+ } as unknown as ChatMessageMap;
295
+
296
+ const payload: MessageDispatch = {
297
+ type: 'resetMessages',
298
+ topicId: 'topic1',
299
+ };
300
+
301
+ const newState = messagesReducer(initialState, payload);
302
+
303
+ expect(Object.keys(newState)).toHaveLength(1);
304
+ expect(newState).not.toHaveProperty('message1');
305
+ expect(newState).toHaveProperty('message2');
306
+ });
215
307
  });
216
308
 
217
309
  describe('unimplemented type', () => {
@@ -21,6 +21,7 @@ interface DeleteMessage {
21
21
  }
22
22
 
23
23
  interface ResetMessages {
24
+ topicId?: string;
24
25
  type: 'resetMessages';
25
26
  }
26
27
 
@@ -101,7 +102,21 @@ export const messagesReducer = (
101
102
  }
102
103
 
103
104
  case 'resetMessages': {
104
- return {};
105
+ return produce(state, (draftState) => {
106
+ const { topicId } = payload;
107
+
108
+ const messages = Object.values(draftState).filter((message) => {
109
+ // 如果没有 topicId,说明是清空默认对话里的消息
110
+ if (!topicId) return !message.topicId;
111
+
112
+ return message.topicId === topicId;
113
+ });
114
+
115
+ // 删除上述找到的消息
116
+ for (const message of messages) {
117
+ delete draftState[message.id];
118
+ }
119
+ });
105
120
  }
106
121
 
107
122
  default: {
@@ -0,0 +1,179 @@
1
+ import { produce } from 'immer';
2
+
3
+ import { ChatTopic, ChatTopicMap } from '@/types/topic';
4
+
5
+ import { ChatTopicDispatch, topicReducer } from './topic';
6
+
7
+ describe('topicReducer', () => {
8
+ let state: ChatTopicMap;
9
+
10
+ beforeEach(() => {
11
+ state = {};
12
+ });
13
+
14
+ describe('addChatTopic', () => {
15
+ it('should add a new ChatTopic object to state', () => {
16
+ const payload: ChatTopicDispatch = {
17
+ type: 'addChatTopic',
18
+ topic: {
19
+ id: '1',
20
+ title: 'Test Topic',
21
+ createAt: Date.now(),
22
+ updateAt: Date.now(),
23
+ },
24
+ };
25
+
26
+ const newState = topicReducer(state, payload);
27
+
28
+ expect(newState).toMatchObject({
29
+ '1': payload.topic,
30
+ });
31
+ });
32
+
33
+ it('should add a ChatTopic object with correct id', () => {
34
+ const payload: ChatTopicDispatch = {
35
+ type: 'addChatTopic',
36
+ topic: {
37
+ id: '1',
38
+ title: 'Test Topic',
39
+ createAt: Date.now(),
40
+ updateAt: Date.now(),
41
+ },
42
+ };
43
+
44
+ const newState = topicReducer(state, payload);
45
+
46
+ expect(newState['1']).toBeDefined();
47
+ });
48
+
49
+ it('should add a ChatTopic object with correct properties', () => {
50
+ const payload: ChatTopicDispatch = {
51
+ type: 'addChatTopic',
52
+ topic: {
53
+ id: '1',
54
+ title: 'Test Topic',
55
+ createAt: Date.now(),
56
+ updateAt: Date.now(),
57
+ },
58
+ };
59
+
60
+ const newState = topicReducer(state, payload);
61
+
62
+ expect(newState['1']).toMatchObject(payload.topic);
63
+ });
64
+ });
65
+
66
+ describe('updateChatTopic', () => {
67
+ it('should update the ChatTopic object in state', () => {
68
+ const topic: ChatTopic = {
69
+ id: '1',
70
+ title: 'Test Topic',
71
+ createAt: Date.now(),
72
+ updateAt: Date.now(),
73
+ };
74
+
75
+ state['1'] = topic;
76
+
77
+ const payload: ChatTopicDispatch = {
78
+ type: 'updateChatTopic',
79
+ id: '1',
80
+ key: 'title',
81
+ value: 'Updated Topic',
82
+ };
83
+
84
+ const newState = topicReducer(state, payload);
85
+
86
+ expect(newState['1'].title).toBe('Updated Topic');
87
+ });
88
+
89
+ it('should update the ChatTopic object with correct properties', () => {
90
+ const topic: ChatTopic = {
91
+ id: '1',
92
+ title: 'Test Topic',
93
+ createAt: Date.now() - 1,
94
+ updateAt: Date.now() - 1, // 设定比当前时间前面一点
95
+ };
96
+
97
+ state['1'] = topic;
98
+
99
+ const payload: ChatTopicDispatch = {
100
+ type: 'updateChatTopic',
101
+ id: '1',
102
+ key: 'title',
103
+ value: 'Updated Topic',
104
+ };
105
+
106
+ const newState = topicReducer(state, payload);
107
+
108
+ expect(newState['1'].updateAt).toBeGreaterThan(topic.updateAt);
109
+ });
110
+ });
111
+
112
+ describe('deleteChatTopic', () => {
113
+ it('should delete the specified ChatTopic object from state', () => {
114
+ const topic: ChatTopic = {
115
+ id: '1',
116
+ title: 'Test Topic',
117
+ createAt: Date.now(),
118
+ updateAt: Date.now(),
119
+ };
120
+
121
+ state['1'] = topic;
122
+
123
+ const payload: ChatTopicDispatch = {
124
+ type: 'deleteChatTopic',
125
+ id: '1',
126
+ };
127
+
128
+ const newState = topicReducer(state, payload);
129
+
130
+ expect(newState['1']).toBeUndefined();
131
+ });
132
+ });
133
+
134
+ describe('default', () => {
135
+ it('should return the original state object', () => {
136
+ const payload = {
137
+ type: 'unknown',
138
+ } as unknown as ChatTopicDispatch;
139
+
140
+ const newState = topicReducer(state, payload);
141
+
142
+ expect(newState).toBe(state);
143
+ });
144
+ });
145
+
146
+ describe('produce', () => {
147
+ it('should generate immutable state object', () => {
148
+ const payload: ChatTopicDispatch = {
149
+ type: 'addChatTopic',
150
+ topic: {
151
+ id: '1',
152
+ title: 'Test Topic',
153
+ createAt: Date.now(),
154
+ updateAt: Date.now(),
155
+ },
156
+ };
157
+
158
+ const newState = topicReducer(state, payload);
159
+
160
+ expect(newState).not.toBe(state);
161
+ });
162
+
163
+ it('should not modify the original state object', () => {
164
+ const payload: ChatTopicDispatch = {
165
+ type: 'addChatTopic',
166
+ topic: {
167
+ id: '1',
168
+ title: 'Test Topic',
169
+ createAt: Date.now(),
170
+ updateAt: Date.now(),
171
+ },
172
+ };
173
+
174
+ const newState = topicReducer(state, payload);
175
+
176
+ expect(state).toMatchObject({});
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,59 @@
1
+ import { produce } from 'immer';
2
+
3
+ import { ChatTopic, ChatTopicMap } from '@/types/topic';
4
+
5
+ interface AddChatTopicAction {
6
+ topic: ChatTopic;
7
+ type: 'addChatTopic';
8
+ }
9
+
10
+ interface UpdateChatTopicAction {
11
+ id: string;
12
+ key: keyof ChatTopic;
13
+ type: 'updateChatTopic';
14
+ value: any;
15
+ }
16
+
17
+ interface DeleteChatTopicAction {
18
+ id: string;
19
+ type: 'deleteChatTopic';
20
+ }
21
+
22
+ export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction | DeleteChatTopicAction;
23
+
24
+ export const topicReducer = (state: ChatTopicMap, payload: ChatTopicDispatch): ChatTopicMap => {
25
+ switch (payload.type) {
26
+ case 'addChatTopic': {
27
+ return produce(state, (draftState) => {
28
+ draftState[payload.topic.id] = payload.topic;
29
+ });
30
+ }
31
+
32
+ case 'updateChatTopic': {
33
+ return produce(state, (draftState) => {
34
+ const { key, value, id } = payload;
35
+
36
+ if (!draftState[id]) return;
37
+
38
+ const topic = draftState[id];
39
+
40
+ if (value !== undefined) {
41
+ // @ts-ignore
42
+ topic[key] = value;
43
+
44
+ topic.updateAt = Date.now();
45
+ }
46
+ });
47
+ }
48
+
49
+ case 'deleteChatTopic': {
50
+ return produce(state, (draftState) => {
51
+ delete draftState[payload.id];
52
+ });
53
+ }
54
+
55
+ default: {
56
+ return state;
57
+ }
58
+ }
59
+ };
@@ -12,10 +12,14 @@ export const currentChats = (s: SessionStore): ChatMessage[] => {
12
12
  const session = sessionSelectors.currentSession(s);
13
13
  if (!session) return [];
14
14
 
15
- return organizeChats(session, {
16
- assistant: agentSelectors.currentAgentAvatar(s),
17
- user: useSettings.getState().settings.avatar || DEFAULT_USER_AVATAR,
18
- });
15
+ return organizeChats(
16
+ session,
17
+ {
18
+ assistant: agentSelectors.currentAgentAvatar(s),
19
+ user: useSettings.getState().settings.avatar || DEFAULT_USER_AVATAR,
20
+ },
21
+ s.activeTopicId,
22
+ );
19
23
  };
20
24
 
21
25
  export const systemRoleSel = (s: SessionStore): string => {
@@ -1,5 +1,6 @@
1
1
  import { currentChats } from './chat';
2
2
  import { chatsTokenCount, systemRoleTokenCount, totalTokenCount } from './token';
3
+ import { currentTopics } from './topic';
3
4
 
4
5
  export const chatSelectors = {
5
6
  chatsTokenCount,
@@ -7,3 +8,7 @@ export const chatSelectors = {
7
8
  systemRoleTokenCount,
8
9
  totalTokenCount,
9
10
  };
11
+
12
+ export const topicSelectors = {
13
+ currentTopics,
14
+ };
@@ -0,0 +1,16 @@
1
+ // 展示在聊天框中的消息
2
+ import { SessionStore, sessionSelectors } from '@/store/session';
3
+ import { ChatTopic } from '@/types/topic';
4
+
5
+ export const currentTopics = (s: SessionStore): ChatTopic[] => {
6
+ const session = sessionSelectors.currentSession(s);
7
+ if (!session) return [];
8
+
9
+ const topics = Object.values(session.topics || {});
10
+
11
+ // 按时间倒序
12
+ const favTopics = topics.filter((t) => t.favorite).sort((a, b) => b.updateAt - a.updateAt);
13
+ const defaultTopics = topics.filter((t) => !t.favorite).sort((a, b) => b.updateAt - a.updateAt);
14
+
15
+ return [...favTopics, ...defaultTopics];
16
+ };
@@ -1,5 +1,6 @@
1
- import { beforeEach } from 'vitest';
1
+ import { beforeEach, describe } from 'vitest';
2
2
 
3
+ import { ChatMessage } from '@/types/chatMessage';
3
4
  import { LobeAgentSession } from '@/types/session';
4
5
 
5
6
  import { organizeChats } from './utils';
@@ -261,4 +262,83 @@ describe('organizeChats', () => {
261
262
  },
262
263
  ]);
263
264
  });
265
+
266
+ const avatar = {
267
+ assistant: 'assistant-avatar',
268
+ user: 'user-avatar',
269
+ };
270
+
271
+ it('should organize chats in chronological order when topicId is not provided', () => {
272
+ const result = organizeChats(session, avatar);
273
+
274
+ expect(result.length).toBe(3);
275
+ expect(result[0].id).toBe('1');
276
+ expect(result[1].id).toBe('2');
277
+ expect(result[2].id).toBe('3');
278
+ });
279
+
280
+ it('should only return chats with specified topicId when topicId is provided', () => {
281
+ const result = organizeChats(session, avatar, 'topic-id');
282
+
283
+ expect(result.length).toBe(0);
284
+ });
285
+
286
+ describe('user Meta', () => {
287
+ it('should return correct meta for user role', () => {
288
+ const result = organizeChats(session, avatar);
289
+ const meta = result[1].meta;
290
+
291
+ expect(meta.avatar).toBe(avatar.user);
292
+ });
293
+
294
+ it('should return correct meta for assistant role', () => {
295
+ const result = organizeChats(session, avatar);
296
+ const meta = result[0].meta;
297
+
298
+ expect(meta.avatar).toBe(avatar.assistant);
299
+ expect(meta.title).toBeUndefined();
300
+ });
301
+
302
+ describe('should return correct meta for function role', () => {
303
+ it('找不到插件', () => {
304
+ const message = {
305
+ id: '4',
306
+ createAt: 1927785600004,
307
+ updateAt: 1927785600004,
308
+ role: 'function',
309
+ function_call: {
310
+ name: 'plugin-name',
311
+ },
312
+ } as ChatMessage;
313
+
314
+ session.chats[message.id] = message;
315
+
316
+ const result = organizeChats(session, avatar);
317
+ const meta = result[3].meta;
318
+
319
+ expect(meta.avatar).toBe('🧩');
320
+ expect(meta.title).toBe('plugin-unknown');
321
+ });
322
+
323
+ it('找到的插件', () => {
324
+ const message = {
325
+ id: '4',
326
+ createAt: 1927785600004,
327
+ updateAt: 1927785600004,
328
+ role: 'function',
329
+ function_call: {
330
+ name: 'realtimeWeather',
331
+ },
332
+ } as ChatMessage;
333
+
334
+ session.chats[message.id] = message;
335
+
336
+ const result = organizeChats(session, avatar);
337
+ const meta = result[3].meta;
338
+
339
+ expect(meta.avatar).toBe('☂️');
340
+ expect(meta.title).toBe('realtimeWeather');
341
+ });
342
+ });
343
+ });
264
344
  });
@@ -5,6 +5,7 @@ import { LobeAgentSession } from '@/types/session';
5
5
  export const organizeChats = (
6
6
  session: LobeAgentSession,
7
7
  avatar: { assistant: string; user: string },
8
+ topicId?: string,
8
9
  ) => {
9
10
  const getMeta = (message: ChatMessage) => {
10
11
  switch (message.role) {
@@ -38,8 +39,13 @@ export const organizeChats = (
38
39
  const basic = Object.values<ChatMessage>(session.chats)
39
40
  // 首先按照时间顺序排序,越早的在越前面
40
41
  .sort((pre, next) => pre.createAt - next.createAt)
41
- // 过滤掉包含 topicId 的消息,有主题的消息不应该出现在聊天框中
42
- .filter((m) => !m.topicId)
42
+ .filter((m) => {
43
+ // 过滤掉包含 topicId 的消息,有主题的消息不应该出现在聊天框中
44
+ if (!topicId) return !m.topicId;
45
+
46
+ // 或者当话题 id 一致时,再展示话题
47
+ return m.topicId === topicId;
48
+ })
43
49
  // 映射头像关系
44
50
  .map((m) => {
45
51
  return {
@@ -3,6 +3,7 @@ import { produce } from 'immer';
3
3
  import { ChatMessageMap } from '@/types/chatMessage';
4
4
  import { MetaData } from '@/types/meta';
5
5
  import { LobeAgentConfig, LobeAgentSession, LobeSessions } from '@/types/session';
6
+ import { ChatTopicMap } from '@/types/topic';
6
7
 
7
8
  /**
8
9
  * @title 添加会话
@@ -37,6 +38,19 @@ interface UpdateSessionChat {
37
38
  type: 'updateSessionChat';
38
39
  }
39
40
 
41
+ /**
42
+ * @title 更新会话聊天上下文
43
+ */
44
+ interface UpdateSessionTopic {
45
+ /**
46
+ * 会话 ID
47
+ */
48
+ id: string;
49
+ topics: ChatTopicMap;
50
+
51
+ type: 'updateSessionTopic';
52
+ }
53
+
40
54
  interface UpdateSessionMeta {
41
55
  id: string;
42
56
  key: keyof MetaData;
@@ -55,7 +69,8 @@ export type SessionDispatch =
55
69
  | UpdateSessionChat
56
70
  | RemoveSession
57
71
  | UpdateSessionMeta
58
- | UpdateSessionAgentConfig;
72
+ | UpdateSessionAgentConfig
73
+ | UpdateSessionTopic;
59
74
 
60
75
  export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => {
61
76
  switch (payload.type) {
@@ -96,6 +111,15 @@ export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch):
96
111
  });
97
112
  }
98
113
 
114
+ case 'updateSessionTopic': {
115
+ return produce(state, (draft) => {
116
+ const chat = draft[payload.id];
117
+ if (!chat) return;
118
+
119
+ chat.topics = payload.topics;
120
+ });
121
+ }
122
+
99
123
  case 'updateSessionConfig': {
100
124
  return produce(state, (draft) => {
101
125
  const { id, config } = payload;
@@ -1,7 +1,6 @@
1
1
  import { BaseDataModel } from '@/types/meta';
2
2
 
3
3
  export interface ChatTopic extends Omit<BaseDataModel, 'meta'> {
4
- chats: string[];
5
4
  favorite?: boolean;
6
5
  title: string;
7
6
  }
@@ -1,68 +0,0 @@
1
- import { Icon } from '@lobehub/ui';
2
- import { createStyles } from 'antd-style';
3
- import { LucideChevronRight, LucideIcon } from 'lucide-react';
4
- import { memo } from 'react';
5
- import { Flexbox } from 'react-layout-kit';
6
-
7
- const useStyles = createStyles(({ css, token }) => ({
8
- container: css`
9
- background: ${token.colorFillQuaternary};
10
- border-radius: 6px;
11
- `,
12
- split: css`
13
- border-bottom: 1px solid ${token.colorSplit};
14
- `,
15
- }));
16
- export interface ConfigItem {
17
- icon: LucideIcon;
18
- label: string;
19
- value?: string | number;
20
- }
21
-
22
- export type ConfigCellProps = ConfigItem;
23
-
24
- export const ConfigCell = memo<ConfigCellProps>(({ icon, label, value }) => {
25
- const { styles } = useStyles();
26
- return (
27
- <Flexbox
28
- className={styles.container}
29
- distribution={'space-between'}
30
- horizontal
31
- padding={'10px 12px'}
32
- >
33
- <Flexbox gap={8} horizontal>
34
- <Icon icon={icon} />
35
- <Flexbox>{label}</Flexbox>
36
- </Flexbox>
37
- {value ?? <Icon icon={LucideChevronRight} />}
38
- </Flexbox>
39
- );
40
- });
41
-
42
- export interface CellGroupProps {
43
- items: ConfigItem[];
44
- }
45
-
46
- export const ConfigCellGroup = memo<CellGroupProps>(({ items }) => {
47
- const { styles } = useStyles();
48
-
49
- return (
50
- <Flexbox className={styles.container}>
51
- {items.map(({ label, icon, value }, index) => (
52
- <Flexbox
53
- className={items.length === index + 1 ? undefined : styles.split}
54
- distribution={'space-between'}
55
- horizontal
56
- key={label}
57
- padding={'10px 12px'}
58
- >
59
- <Flexbox gap={8} horizontal>
60
- <Icon icon={icon} />
61
- <Flexbox>{label}</Flexbox>
62
- </Flexbox>
63
- {value ?? <Icon icon={LucideChevronRight} />}
64
- </Flexbox>
65
- ))}
66
- </Flexbox>
67
- );
68
- });