@lobehub/chat 1.33.5 → 1.34.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 (203) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/chat.json +7 -0
  4. package/locales/ar/common.json +2 -0
  5. package/locales/ar/models.json +24 -0
  6. package/locales/ar/setting.json +5 -0
  7. package/locales/ar/thread.json +5 -0
  8. package/locales/bg-BG/chat.json +7 -0
  9. package/locales/bg-BG/common.json +2 -0
  10. package/locales/bg-BG/models.json +24 -0
  11. package/locales/bg-BG/setting.json +5 -0
  12. package/locales/bg-BG/thread.json +5 -0
  13. package/locales/de-DE/chat.json +7 -0
  14. package/locales/de-DE/common.json +2 -0
  15. package/locales/de-DE/models.json +24 -0
  16. package/locales/de-DE/setting.json +5 -0
  17. package/locales/de-DE/thread.json +5 -0
  18. package/locales/en-US/chat.json +7 -0
  19. package/locales/en-US/common.json +2 -0
  20. package/locales/en-US/models.json +24 -0
  21. package/locales/en-US/setting.json +5 -0
  22. package/locales/en-US/thread.json +5 -0
  23. package/locales/es-ES/chat.json +7 -0
  24. package/locales/es-ES/common.json +2 -0
  25. package/locales/es-ES/models.json +24 -0
  26. package/locales/es-ES/setting.json +5 -0
  27. package/locales/es-ES/thread.json +5 -0
  28. package/locales/fa-IR/chat.json +7 -0
  29. package/locales/fa-IR/common.json +2 -0
  30. package/locales/fa-IR/models.json +24 -0
  31. package/locales/fa-IR/setting.json +5 -0
  32. package/locales/fa-IR/thread.json +5 -0
  33. package/locales/fr-FR/chat.json +7 -0
  34. package/locales/fr-FR/common.json +2 -0
  35. package/locales/fr-FR/models.json +24 -0
  36. package/locales/fr-FR/setting.json +5 -0
  37. package/locales/fr-FR/thread.json +5 -0
  38. package/locales/it-IT/chat.json +7 -0
  39. package/locales/it-IT/common.json +2 -0
  40. package/locales/it-IT/models.json +24 -0
  41. package/locales/it-IT/setting.json +5 -0
  42. package/locales/it-IT/thread.json +5 -0
  43. package/locales/ja-JP/chat.json +7 -0
  44. package/locales/ja-JP/common.json +2 -0
  45. package/locales/ja-JP/models.json +24 -0
  46. package/locales/ja-JP/setting.json +5 -0
  47. package/locales/ja-JP/thread.json +5 -0
  48. package/locales/ko-KR/chat.json +7 -0
  49. package/locales/ko-KR/common.json +2 -0
  50. package/locales/ko-KR/models.json +24 -0
  51. package/locales/ko-KR/setting.json +5 -0
  52. package/locales/ko-KR/thread.json +5 -0
  53. package/locales/nl-NL/chat.json +7 -0
  54. package/locales/nl-NL/common.json +2 -0
  55. package/locales/nl-NL/models.json +24 -0
  56. package/locales/nl-NL/setting.json +5 -0
  57. package/locales/nl-NL/thread.json +5 -0
  58. package/locales/pl-PL/chat.json +7 -0
  59. package/locales/pl-PL/common.json +2 -0
  60. package/locales/pl-PL/models.json +24 -0
  61. package/locales/pl-PL/setting.json +5 -0
  62. package/locales/pl-PL/thread.json +5 -0
  63. package/locales/pt-BR/chat.json +7 -0
  64. package/locales/pt-BR/common.json +2 -0
  65. package/locales/pt-BR/models.json +24 -0
  66. package/locales/pt-BR/setting.json +5 -0
  67. package/locales/pt-BR/thread.json +5 -0
  68. package/locales/ru-RU/chat.json +7 -0
  69. package/locales/ru-RU/common.json +2 -0
  70. package/locales/ru-RU/models.json +24 -0
  71. package/locales/ru-RU/setting.json +5 -0
  72. package/locales/ru-RU/thread.json +5 -0
  73. package/locales/tr-TR/chat.json +7 -0
  74. package/locales/tr-TR/common.json +2 -0
  75. package/locales/tr-TR/models.json +24 -0
  76. package/locales/tr-TR/setting.json +5 -0
  77. package/locales/tr-TR/thread.json +5 -0
  78. package/locales/vi-VN/chat.json +7 -0
  79. package/locales/vi-VN/common.json +2 -0
  80. package/locales/vi-VN/models.json +24 -0
  81. package/locales/vi-VN/setting.json +5 -0
  82. package/locales/vi-VN/thread.json +5 -0
  83. package/locales/zh-CN/chat.json +7 -0
  84. package/locales/zh-CN/common.json +2 -0
  85. package/locales/zh-CN/models.json +24 -0
  86. package/locales/zh-CN/setting.json +5 -0
  87. package/locales/zh-CN/thread.json +5 -0
  88. package/locales/zh-TW/chat.json +7 -0
  89. package/locales/zh-TW/common.json +2 -0
  90. package/locales/zh-TW/models.json +24 -0
  91. package/locales/zh-TW/setting.json +5 -0
  92. package/locales/zh-TW/thread.json +5 -0
  93. package/package.json +1 -1
  94. package/src/app/(main)/chat/(workspace)/@conversation/default.tsx +2 -0
  95. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatHydration/index.tsx +11 -2
  96. package/src/{features → app/(main)/chat/(workspace)/@conversation/features}/ChatInput/Desktop/Footer/index.tsx +7 -9
  97. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +7 -2
  98. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/Thread.tsx +62 -0
  99. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/ThreadItem.tsx +68 -0
  100. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +62 -2
  101. package/src/app/(main)/chat/(workspace)/@conversation/features/ThreadHydration.tsx +47 -0
  102. package/src/app/(main)/chat/(workspace)/@portal/_layout/Desktop.tsx +3 -2
  103. package/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx +47 -6
  104. package/src/app/(main)/chat/(workspace)/@topic/features/SkeletonList.tsx +3 -2
  105. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ByTimeMode/index.tsx +10 -3
  106. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/FlatMode/index.tsx +1 -1
  107. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ThreadItem/Content.tsx +164 -0
  108. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ThreadItem/index.tsx +98 -0
  109. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/{TopicItem.tsx → TopicItem/index.tsx} +33 -22
  110. package/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx +12 -5
  111. package/src/app/(main)/chat/(workspace)/_layout/Mobile/index.tsx +1 -2
  112. package/src/const/message.ts +2 -0
  113. package/src/const/settings/systemAgent.ts +1 -0
  114. package/src/database/server/migrations/0012_add_thread.sql +39 -0
  115. package/src/database/server/migrations/meta/0012_snapshot.json +3671 -0
  116. package/src/database/server/migrations/meta/_journal.json +7 -0
  117. package/src/database/server/models/_template.ts +2 -2
  118. package/src/database/server/models/message.ts +1 -0
  119. package/src/database/server/models/thread.ts +79 -0
  120. package/src/database/server/schemas/lobechat/message.ts +2 -1
  121. package/src/database/server/schemas/lobechat/relations.ts +13 -1
  122. package/src/database/server/schemas/lobechat/topic.ts +30 -1
  123. package/src/database/server/utils/idGenerator.ts +1 -0
  124. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +6 -4
  125. package/src/features/ChatInput/ActionBar/Token/index.tsx +24 -5
  126. package/src/features/ChatInput/ActionBar/config.ts +3 -2
  127. package/src/features/ChatInput/Desktop/index.tsx +15 -7
  128. package/src/features/ChatInput/Mobile/index.tsx +4 -4
  129. package/src/features/Conversation/Actions/Assistant.tsx +24 -5
  130. package/src/features/Conversation/Actions/User.tsx +21 -4
  131. package/src/features/Conversation/Actions/index.ts +1 -66
  132. package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/index.tsx +3 -1
  133. package/src/features/Conversation/Messages/{Tool/index.tsx → Assistant/ToolCallItem/Tool.tsx} +10 -11
  134. package/src/features/Conversation/Messages/Assistant/ToolCallItem/index.tsx +5 -3
  135. package/src/features/Conversation/Messages/Assistant/index.tsx +22 -14
  136. package/src/features/Conversation/Messages/index.ts +0 -2
  137. package/src/features/Conversation/components/AutoScroll.tsx +1 -1
  138. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +79 -5
  139. package/src/features/Conversation/components/ChatItem/InPortalThreadContext.ts +3 -0
  140. package/src/features/Conversation/components/ChatItem/index.tsx +16 -5
  141. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +9 -1
  142. package/src/features/Conversation/components/ThreadDivider/index.tsx +19 -0
  143. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +19 -4
  144. package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +90 -0
  145. package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +30 -0
  146. package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +66 -0
  147. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +50 -0
  148. package/src/features/Portal/Thread/Chat/ChatItem.tsx +62 -0
  149. package/src/features/Portal/Thread/Chat/ChatList.tsx +49 -0
  150. package/src/features/Portal/Thread/Chat/ThreadDivider/index.tsx +19 -0
  151. package/src/features/Portal/Thread/Chat/index.tsx +28 -0
  152. package/src/features/Portal/Thread/Header/Active.tsx +35 -0
  153. package/src/features/Portal/Thread/Header/New.tsx +37 -0
  154. package/src/features/Portal/Thread/Header/Title.tsx +18 -0
  155. package/src/features/Portal/Thread/Header/index.tsx +20 -0
  156. package/src/features/Portal/Thread/hook.ts +8 -0
  157. package/src/features/Portal/Thread/index.ts +12 -0
  158. package/src/features/Portal/router.tsx +2 -1
  159. package/src/hooks/useFetchTopics.ts +7 -1
  160. package/src/locales/default/chat.ts +8 -1
  161. package/src/locales/default/common.ts +3 -0
  162. package/src/locales/default/index.ts +2 -0
  163. package/src/locales/default/setting.ts +5 -0
  164. package/src/locales/default/thread.ts +5 -0
  165. package/src/server/routers/lambda/index.ts +2 -0
  166. package/src/server/routers/lambda/thread.ts +83 -0
  167. package/src/services/thread.ts +54 -0
  168. package/src/store/chat/initialState.ts +3 -0
  169. package/src/store/chat/selectors.ts +2 -1
  170. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +1 -1
  171. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +1 -1
  172. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +31 -8
  173. package/src/store/chat/slices/aiChat/actions/rag.ts +1 -1
  174. package/src/store/chat/slices/message/selectors.test.ts +3 -3
  175. package/src/store/chat/slices/message/selectors.ts +50 -29
  176. package/src/store/chat/slices/plugin/action.ts +26 -8
  177. package/src/store/chat/slices/portal/action.ts +1 -0
  178. package/src/store/chat/slices/portal/initialState.ts +1 -0
  179. package/src/store/chat/slices/portal/selectors/thread.ts +17 -0
  180. package/src/store/chat/slices/portal/selectors.ts +2 -0
  181. package/src/store/chat/slices/thread/action.ts +326 -0
  182. package/src/store/chat/slices/thread/initialState.ts +34 -0
  183. package/src/store/chat/slices/thread/reducer.ts +48 -0
  184. package/src/store/chat/slices/thread/selectors/index.ts +202 -0
  185. package/src/store/chat/slices/thread/selectors/util.ts +22 -0
  186. package/src/store/chat/slices/topic/action.ts +5 -1
  187. package/src/store/chat/store.ts +5 -2
  188. package/src/store/global/initialState.ts +4 -0
  189. package/src/store/global/selectors.ts +4 -0
  190. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  191. package/src/types/message/index.ts +17 -1
  192. package/src/types/topic/index.ts +1 -0
  193. package/src/types/topic/thread.ts +42 -0
  194. package/src/types/user/settings/systemAgent.ts +1 -0
  195. package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +0 -11
  196. package/src/app/(main)/chat/(workspace)/_layout/Mobile/PortalModal.tsx +0 -35
  197. /package/src/{features → app/(main)/chat/(workspace)/@conversation/features}/ChatInput/Desktop/Footer/SendMore.tsx +0 -0
  198. /package/src/{features → app/(main)/chat/(workspace)/@conversation/features}/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -0
  199. /package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/{DefaultContent.tsx → TopicItem/DefaultContent.tsx} +0 -0
  200. /package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/{TopicContent.tsx → TopicItem/TopicContent.tsx} +0 -0
  201. /package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/PluginResultJSON.tsx +0 -0
  202. /package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/Settings.tsx +0 -0
  203. /package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/style.ts +0 -0
@@ -0,0 +1,62 @@
1
+ import { Typography } from 'antd';
2
+ import { createStyles } from 'antd-style';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { CSSProperties, memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import { useChatStore } from '@/store/chat';
9
+ import { threadSelectors } from '@/store/chat/selectors';
10
+
11
+ import ThreadItem from './ThreadItem';
12
+
13
+ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
14
+ container: css`
15
+ cursor: pointer;
16
+
17
+ padding-block: 8px 4px;
18
+ padding-inline: 4px;
19
+
20
+ background: ${isDarkMode ? token.colorFillTertiary : token.colorFillQuaternary};
21
+ border-radius: 6px;
22
+ `,
23
+ }));
24
+
25
+ interface ThreadProps {
26
+ id: string;
27
+ placement: 'start' | 'end';
28
+ style?: CSSProperties;
29
+ }
30
+
31
+ const Thread = memo<ThreadProps>(({ id, placement, style }) => {
32
+ const { t } = useTranslation('chat');
33
+ const { styles } = useStyles();
34
+
35
+ const threads = useChatStore(threadSelectors.getThreadsBySourceMsgId(id), isEqual);
36
+
37
+ return (
38
+ <Flexbox
39
+ direction={placement === 'end' ? 'horizontal-reverse' : 'horizontal'}
40
+ gap={12}
41
+ paddingInline={16}
42
+ style={{ paddingBottom: 16, ...style }}
43
+ >
44
+ <div style={{ width: 40 }} />
45
+ <Flexbox className={styles.container} gap={4} padding={4} style={{ width: 'fit-content' }}>
46
+ <Flexbox gap={8} horizontal paddingInline={6}>
47
+ <Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
48
+ {t('thread.title')}
49
+ {threads.length}
50
+ </Typography.Text>
51
+ </Flexbox>
52
+ <Flexbox>
53
+ {threads.map((thread) => (
54
+ <ThreadItem key={thread.id} {...thread} />
55
+ ))}
56
+ </Flexbox>
57
+ </Flexbox>
58
+ </Flexbox>
59
+ );
60
+ });
61
+
62
+ export default Thread;
@@ -0,0 +1,68 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import dayjs from 'dayjs';
4
+ import { ChevronRight } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useIsMobile } from '@/hooks/useIsMobile';
10
+ import { useChatStore } from '@/store/chat';
11
+ import { chatSelectors } from '@/store/chat/selectors';
12
+ import { ThreadItem } from '@/types/topic';
13
+
14
+ const useStyles = createStyles(({ css, token }) => ({
15
+ active: css`
16
+ background: ${token.colorFillTertiary};
17
+ `,
18
+ container: css`
19
+ cursor: pointer;
20
+
21
+ padding-block: 4px;
22
+ padding-inline: 6px;
23
+
24
+ font-size: 12px;
25
+
26
+ border-radius: 6px;
27
+
28
+ &:hover {
29
+ background: ${token.colorFillTertiary};
30
+ }
31
+ `,
32
+ extra: css`
33
+ color: ${token.colorTextSecondary};
34
+ `,
35
+ }));
36
+
37
+ const Item = memo<ThreadItem>(({ id, title, lastActiveAt, sourceMessageId }) => {
38
+ const { t } = useTranslation('chat');
39
+ const openThreadInPortal = useChatStore((s) => s.openThreadInPortal);
40
+ const { styles, cx } = useStyles();
41
+ const [isActive, messageCount] = useChatStore((s) => [
42
+ s.activeThreadId === id,
43
+ chatSelectors.countMessagesByThreadId(id)(s),
44
+ ]);
45
+ const mobile = useIsMobile();
46
+ return (
47
+ <Flexbox
48
+ align={'baseline'}
49
+ className={cx(styles.container, isActive && styles.active)}
50
+ gap={8}
51
+ horizontal
52
+ onClick={() => {
53
+ if (isActive) return;
54
+
55
+ openThreadInPortal(id, sourceMessageId);
56
+ }}
57
+ >
58
+ {title}
59
+ <Flexbox className={styles.extra} horizontal>
60
+ {!!messageCount && t('thread.threadMessageCount', { messageCount })}
61
+ {!mobile && ` · ${dayjs(lastActiveAt).format('YYYY-MM-DD')}`}
62
+ <Icon icon={ChevronRight} />
63
+ </Flexbox>
64
+ </Flexbox>
65
+ );
66
+ });
67
+
68
+ export default Item;
@@ -1,3 +1,4 @@
1
+ import { createStyles } from 'antd-style';
1
2
  import React, { memo, useMemo } from 'react';
2
3
 
3
4
  import { ChatItem } from '@/features/Conversation';
@@ -5,7 +6,42 @@ import ActionsBar from '@/features/Conversation/components/ChatItem/ActionsBar';
5
6
  import { useAgentStore } from '@/store/agent';
6
7
  import { agentSelectors } from '@/store/agent/selectors';
7
8
  import { useChatStore } from '@/store/chat';
8
- import { chatSelectors } from '@/store/chat/selectors';
9
+ import { chatSelectors, threadSelectors } from '@/store/chat/selectors';
10
+
11
+ import Thread from './Thread';
12
+
13
+ const useStyles = createStyles(({ css, token, isDarkMode }) => {
14
+ const borderColor = isDarkMode ? token.colorFillSecondary : token.colorFillTertiary;
15
+
16
+ return {
17
+ end: css`
18
+ &::after {
19
+ inset-inline-end: 36px;
20
+ border-inline-end: 2px solid ${borderColor};
21
+ border-end-end-radius: 8px;
22
+ }
23
+ `,
24
+ line: css`
25
+ &::after {
26
+ content: '';
27
+
28
+ position: absolute;
29
+ inset-block: 56px 50px;
30
+
31
+ width: 32px;
32
+
33
+ border-block-end: 2px solid ${borderColor};
34
+ }
35
+ `,
36
+ start: css`
37
+ &::after {
38
+ inset-inline-start: 36px;
39
+ border-inline-start: 2px solid ${borderColor};
40
+ border-end-start-radius: 8px;
41
+ }
42
+ `,
43
+ };
44
+ });
9
45
 
10
46
  export interface ThreadChatItemProps {
11
47
  id: string;
@@ -13,7 +49,21 @@ export interface ThreadChatItemProps {
13
49
  }
14
50
 
15
51
  const MainChatItem = memo<ThreadChatItemProps>(({ id, index }) => {
16
- const [historyLength] = useChatStore((s) => [chatSelectors.mainDisplayChatIDs(s).length]);
52
+ const { styles, cx } = useStyles();
53
+
54
+ const [displayMode] = useAgentStore((s) => {
55
+ const config = agentSelectors.currentAgentChatConfig(s);
56
+ return [config.displayMode || 'chat'];
57
+ });
58
+
59
+ const userRole = useChatStore((s) => chatSelectors.getMessageById(id)(s)?.role);
60
+
61
+ const placement = displayMode === 'chat' && userRole === 'user' ? 'end' : 'start';
62
+
63
+ const [showThread, historyLength] = useChatStore((s) => [
64
+ threadSelectors.hasThreadBySourceMsgId(id)(s),
65
+ chatSelectors.mainDisplayChatIDs(s).length,
66
+ ]);
17
67
 
18
68
  const enableHistoryDivider = useAgentStore((s) => {
19
69
  const config = agentSelectors.currentAgentChatConfig(s);
@@ -29,7 +79,17 @@ const MainChatItem = memo<ThreadChatItemProps>(({ id, index }) => {
29
79
  return (
30
80
  <ChatItem
31
81
  actionBar={actionBar}
82
+ className={showThread ? cx(styles.line, styles[placement]) : ''}
32
83
  enableHistoryDivider={enableHistoryDivider}
84
+ endRender={
85
+ showThread && (
86
+ <Thread
87
+ id={id}
88
+ placement={placement}
89
+ style={{ marginTop: displayMode === 'docs' ? 12 : undefined }}
90
+ />
91
+ )
92
+ }
33
93
  id={id}
34
94
  index={index}
35
95
  />
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import { useQueryState } from 'nuqs';
4
+ import { memo, useEffect, useLayoutEffect } from 'react';
5
+ import { createStoreUpdater } from 'zustand-utils';
6
+
7
+ import { useChatStore } from '@/store/chat';
8
+
9
+ // sync outside state to useChatStore
10
+ const ThreadHydration = memo(() => {
11
+ const useStoreUpdater = createStoreUpdater(useChatStore);
12
+
13
+ // two-way bindings the topic params to chat store
14
+ const [portalThread, setThread] = useQueryState('portalThread');
15
+ useStoreUpdater('portalThreadId', portalThread);
16
+
17
+ useLayoutEffect(() => {
18
+ const unsubscribe = useChatStore.subscribe(
19
+ (s) => s.portalThreadId,
20
+ (state) => {
21
+ setThread(!state ? null : state);
22
+ },
23
+ );
24
+
25
+ return () => {
26
+ unsubscribe();
27
+ };
28
+ }, []);
29
+
30
+ // should open portal automatically when portalThread is set
31
+ useEffect(() => {
32
+ if (!!portalThread && !useChatStore.getState().showPortal) {
33
+ useChatStore.getState().togglePortal(true);
34
+ }
35
+ }, [portalThread]);
36
+
37
+ const [activeTopicId, useFetchThreads] = useChatStore((s) => [
38
+ s.activeTopicId,
39
+ s.useFetchThreads,
40
+ ]);
41
+
42
+ useFetchThreads(activeTopicId);
43
+
44
+ return null;
45
+ });
46
+
47
+ export default ThreadHydration;
@@ -1,12 +1,13 @@
1
1
  import { PropsWithChildren } from 'react';
2
2
 
3
+ import { PortalHeader } from '@/features/Portal/router';
4
+
3
5
  import Body from '../features/Body';
4
- import Header from '../features/Header';
5
6
 
6
7
  const Layout = ({ children }: PropsWithChildren) => {
7
8
  return (
8
9
  <>
9
- <Header />
10
+ <PortalHeader />
10
11
  <Body>{children}</Body>
11
12
  </>
12
13
  );
@@ -1,17 +1,58 @@
1
+ 'use client';
2
+
3
+ import { Modal } from '@lobehub/ui';
4
+ import { createStyles } from 'antd-style';
1
5
  import { PropsWithChildren } from 'react';
6
+ import { useTranslation } from 'react-i18next';
2
7
  import { Flexbox } from 'react-layout-kit';
3
8
 
9
+ import { PortalHeader } from '@/features/Portal/router';
10
+ import { useChatStore } from '@/store/chat';
11
+
12
+ const useStyles = createStyles(({ css, token }) => ({
13
+ container: css`
14
+ background: linear-gradient(${token.colorBgElevated}, ${token.colorBgLayout}) !important;
15
+ `,
16
+ }));
17
+
4
18
  const Layout = ({ children }: PropsWithChildren) => {
19
+ const { styles, cx } = useStyles();
20
+ const [showMobilePortal, isPortalThread, togglePortal] = useChatStore((s) => [
21
+ s.showPortal,
22
+ !!s.portalThreadId,
23
+ s.togglePortal,
24
+ ]);
25
+ const { t } = useTranslation('portal');
26
+
5
27
  return (
6
- <Flexbox gap={8} height={'100%'} padding={'8px 8px 0'} style={{ overflow: 'hidden' }}>
28
+ <Modal
29
+ allowFullscreen
30
+ className={cx(isPortalThread && styles.container)}
31
+ height={'95%'}
32
+ onCancel={() => togglePortal(false)}
33
+ open={showMobilePortal}
34
+ styles={{
35
+ body: { padding: 0 },
36
+ header: { display: 'none' },
37
+ }}
38
+ title={t('title')}
39
+ >
40
+ <PortalHeader />
7
41
  <Flexbox
8
- height={'100%'}
9
- style={{ marginInline: -8, overflow: 'hidden', position: 'relative' }}
10
- width={'calc(100% + 16px)'}
42
+ gap={8}
43
+ height={'calc(100% - 52px)'}
44
+ padding={'0 8px'}
45
+ style={{ overflow: 'hidden' }}
11
46
  >
12
- {children}
47
+ <Flexbox
48
+ height={'100%'}
49
+ style={{ marginInline: -8, overflow: 'hidden', position: 'relative' }}
50
+ width={'calc(100% + 16px)'}
51
+ >
52
+ {children}
53
+ </Flexbox>
13
54
  </Flexbox>
14
- </Flexbox>
55
+ </Modal>
15
56
  );
16
57
  };
17
58
 
@@ -12,7 +12,8 @@ const useStyles = createStyles(({ css, prefixCls }) => ({
12
12
  justify-content: center;
13
13
 
14
14
  height: 44px;
15
- padding: 8px;
15
+ padding-block: 8px;
16
+ padding-inline: 12px;
16
17
 
17
18
  .${prefixCls}-skeleton-content {
18
19
  display: flex;
@@ -48,7 +49,7 @@ export const Placeholder = memo(() => {
48
49
 
49
50
  export const SkeletonList = memo(() => (
50
51
  <Flexbox style={{ paddingTop: 6 }}>
51
- {Array.from({ length: 8 }).map((_, i) => (
52
+ {Array.from({ length: 6 }).map((_, i) => (
52
53
  <Placeholder key={i} />
53
54
  ))}
54
55
  </Flexbox>
@@ -15,7 +15,7 @@ import TopicGroupItem from './GroupItem';
15
15
  const ByTimeMode = memo(() => {
16
16
  const { t } = useTranslation('topic');
17
17
  const virtuosoRef = useRef<VirtuosoHandle>(null);
18
- const [activeTopicId] = useChatStore((s) => [s.activeTopicId]);
18
+ const [activeTopicId, activeThreadId] = useChatStore((s) => [s.activeTopicId, s.activeThreadId]);
19
19
  const groupTopics = useChatStore(topicSelectors.groupedTopicsSelector, isEqual);
20
20
 
21
21
  const { groups, groupCounts, topics } = useMemo(() => {
@@ -39,10 +39,17 @@ const ByTimeMode = memo(() => {
39
39
  return index === 0 ? (
40
40
  <TopicItem active={!activeTopicId} fav={favorite} title={title} />
41
41
  ) : (
42
- <TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
42
+ <TopicItem
43
+ active={activeTopicId === id}
44
+ fav={favorite}
45
+ id={id}
46
+ key={id}
47
+ threadId={activeThreadId}
48
+ title={title}
49
+ />
43
50
  );
44
51
  },
45
- [activeTopicId, topics],
52
+ [activeTopicId, topics, activeThreadId],
46
53
  );
47
54
 
48
55
  const groupContent = useCallback(
@@ -42,7 +42,7 @@ const FlatMode = memo(() => {
42
42
  // components={{ ScrollSeekPlaceholder: Placeholder }}
43
43
  computeItemKey={(_, item) => item.id}
44
44
  data={topics}
45
- fixedItemHeight={44}
45
+ defaultItemHeight={44}
46
46
  initialTopMostItemIndex={Math.max(activeIndex, 0)}
47
47
  itemContent={itemContent}
48
48
  overscan={44 * 10}
@@ -0,0 +1,164 @@
1
+ import { ActionIcon, EditableText, Icon } from '@lobehub/ui';
2
+ import { App, Dropdown, type MenuProps, Typography } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { MoreVertical, PencilLine, Trash } from 'lucide-react';
5
+ import { memo, useMemo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import BubblesLoading from '@/components/BubblesLoading';
10
+ import { LOADING_FLAT } from '@/const/message';
11
+ import { useIsMobile } from '@/hooks/useIsMobile';
12
+ import { useChatStore } from '@/store/chat';
13
+
14
+ const useStyles = createStyles(({ css, token }) => ({
15
+ active: css`
16
+ color: ${token.colorText};
17
+ `,
18
+ content: css`
19
+ position: relative;
20
+ overflow: hidden;
21
+ flex: 1;
22
+ `,
23
+ title: css`
24
+ flex: 1;
25
+
26
+ height: 28px;
27
+
28
+ line-height: 28px;
29
+ color: ${token.colorTextSecondary};
30
+ text-align: start;
31
+ `,
32
+ }));
33
+ const { Paragraph } = Typography;
34
+
35
+ interface TopicContentProps {
36
+ active?: boolean;
37
+ id: string;
38
+ showMore?: boolean;
39
+ title: string;
40
+ }
41
+
42
+ const Content = memo<TopicContentProps>(({ id, title, active, showMore }) => {
43
+ const { t } = useTranslation(['thread', 'common']);
44
+
45
+ const mobile = useIsMobile();
46
+
47
+ const [editing, updateThreadTitle, removeThread] = useChatStore((s) => [
48
+ s.threadRenamingId === id,
49
+ s.updateThreadTitle,
50
+ s.removeThread,
51
+ ]);
52
+ const { styles, cx } = useStyles();
53
+
54
+ const toggleEditing = (visible?: boolean) => {
55
+ useChatStore.setState({ threadRenamingId: visible ? id : '' });
56
+ };
57
+
58
+ const { modal } = App.useApp();
59
+
60
+ const items = useMemo<MenuProps['items']>(
61
+ () => [
62
+ {
63
+ icon: <Icon icon={PencilLine} />,
64
+ key: 'rename',
65
+ label: t('rename', { ns: 'common' }),
66
+ onClick: () => {
67
+ toggleEditing(true);
68
+ },
69
+ },
70
+ {
71
+ type: 'divider',
72
+ },
73
+ {
74
+ danger: true,
75
+ icon: <Icon icon={Trash} />,
76
+ key: 'delete',
77
+ label: t('delete', { ns: 'common' }),
78
+ onClick: () => {
79
+ if (!id) return;
80
+
81
+ modal.confirm({
82
+ centered: true,
83
+ okButtonProps: { danger: true },
84
+ onOk: async () => {
85
+ await removeThread(id);
86
+ },
87
+ title: t('actions.confirmRemoveThread'),
88
+ });
89
+ },
90
+ },
91
+ ],
92
+ [],
93
+ );
94
+
95
+ return (
96
+ <Flexbox
97
+ align={'center'}
98
+ gap={8}
99
+ horizontal
100
+ justify={'space-between'}
101
+ onDoubleClick={(e) => {
102
+ if (!id) return;
103
+ if (e.altKey) toggleEditing(true);
104
+ }}
105
+ >
106
+ {!editing ? (
107
+ title === LOADING_FLAT ? (
108
+ <Flexbox flex={1} height={28} justify={'center'}>
109
+ <BubblesLoading />
110
+ </Flexbox>
111
+ ) : (
112
+ <Paragraph
113
+ className={cx(styles.title, active && styles.active)}
114
+ ellipsis={{ rows: 1, tooltip: { placement: 'left', title } }}
115
+ style={{ margin: 0 }}
116
+ >
117
+ {title}
118
+ </Paragraph>
119
+ )
120
+ ) : (
121
+ <EditableText
122
+ editing={editing}
123
+ onChangeEnd={(v) => {
124
+ if (title !== v) {
125
+ updateThreadTitle(id, v);
126
+ }
127
+ toggleEditing(false);
128
+ }}
129
+ onEditingChange={toggleEditing}
130
+ showEditIcon={false}
131
+ size={'small'}
132
+ style={{
133
+ height: 28,
134
+ }}
135
+ type={'pure'}
136
+ value={title}
137
+ />
138
+ )}
139
+ {(showMore || mobile) && !editing && (
140
+ <Dropdown
141
+ arrow={false}
142
+ menu={{
143
+ items: items,
144
+ onClick: ({ domEvent }) => {
145
+ domEvent.stopPropagation();
146
+ },
147
+ }}
148
+ trigger={['click']}
149
+ >
150
+ <ActionIcon
151
+ className="topic-more"
152
+ icon={MoreVertical}
153
+ onClick={(e) => {
154
+ e.stopPropagation();
155
+ }}
156
+ size={'small'}
157
+ />
158
+ </Dropdown>
159
+ )}
160
+ </Flexbox>
161
+ );
162
+ });
163
+
164
+ export default Content;
@@ -0,0 +1,98 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { memo, useState } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import { useChatStore } from '@/store/chat';
6
+ import { useGlobalStore } from '@/store/global';
7
+
8
+ import Content from './Content';
9
+
10
+ const useStyles = createStyles(({ css, token, isDarkMode }, index: number) => ({
11
+ active: css`
12
+ background: ${isDarkMode ? token.colorFillSecondary : token.colorFillTertiary};
13
+ transition: background 200ms ${token.motionEaseOut};
14
+
15
+ &:hover {
16
+ background: ${token.colorFill};
17
+ }
18
+ `,
19
+ container: css`
20
+ margin-inline: 8px;
21
+
22
+ &::after {
23
+ content: '';
24
+
25
+ position: absolute;
26
+ inset-block: 50px ${index * 40 + 20}px;
27
+ inset-inline-start: 26px;
28
+
29
+ width: 18px;
30
+
31
+ border-block-end: 2px solid ${token.colorBorderSecondary};
32
+ border-inline-start: 2px solid ${token.colorBorderSecondary};
33
+ border-end-start-radius: 8px;
34
+ }
35
+
36
+ &.thread-item {
37
+ width: calc(100% - 16px);
38
+ }
39
+ `,
40
+ split: css`
41
+ border-block-end: 1px solid ${token.colorSplit};
42
+ `,
43
+ wrapper: css`
44
+ cursor: pointer;
45
+
46
+ width: calc(100% - 36px);
47
+ margin-block: 2px;
48
+ padding-block: 4px;
49
+ padding-inline: 8px;
50
+
51
+ border-radius: ${token.borderRadius}px;
52
+
53
+ &:hover {
54
+ background: ${token.colorFillSecondary};
55
+ }
56
+ `,
57
+ }));
58
+
59
+ export interface ThreadItemProps {
60
+ id: string;
61
+ index: number;
62
+ title: string;
63
+ }
64
+
65
+ const ThreadItem = memo<ThreadItemProps>(({ title, id, index }) => {
66
+ const { styles, cx } = useStyles(index);
67
+ const toggleConfig = useGlobalStore((s) => s.toggleMobileTopic);
68
+ const [toggleThread, activeThreadId] = useChatStore((s) => [s.switchThread, s.activeThreadId]);
69
+ const [isHover, setHovering] = useState(false);
70
+
71
+ const active = id === activeThreadId;
72
+ return (
73
+ <Flexbox className={cx(styles.container, 'thread-item')} horizontal>
74
+ <Flexbox height={36} width={36} />
75
+ <Flexbox
76
+ align={'center'}
77
+ className={cx(styles.wrapper, active && styles.active)}
78
+ distribution={'space-between'}
79
+ flex={1}
80
+ horizontal
81
+ onClick={() => {
82
+ toggleThread(id);
83
+ toggleConfig(false);
84
+ }}
85
+ onMouseEnter={() => {
86
+ setHovering(true);
87
+ }}
88
+ onMouseLeave={() => {
89
+ setHovering(false);
90
+ }}
91
+ >
92
+ {<Content active={active} id={id} showMore={isHover} title={title} />}
93
+ </Flexbox>
94
+ </Flexbox>
95
+ );
96
+ });
97
+
98
+ export default ThreadItem;