@lobehub/lobehub 2.0.0-next.277 → 2.0.0-next.279

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 (91) hide show
  1. package/.cursor/rules/db-migrations.mdc +1 -1
  2. package/.cursor/rules/debug-usage.mdc +7 -5
  3. package/.cursor/rules/desktop-controller-tests.mdc +2 -1
  4. package/.cursor/rules/desktop-feature-implementation.mdc +9 -5
  5. package/.cursor/rules/desktop-local-tools-implement.mdc +67 -66
  6. package/.cursor/rules/desktop-menu-configuration.mdc +21 -9
  7. package/.cursor/rules/desktop-window-management.mdc +17 -2
  8. package/.cursor/rules/drizzle-schema-style-guide.mdc +6 -6
  9. package/.cursor/rules/hotkey.mdc +1 -0
  10. package/.cursor/rules/i18n.mdc +1 -0
  11. package/.cursor/rules/project-structure.mdc +16 -3
  12. package/.cursor/rules/react.mdc +17 -5
  13. package/.cursor/rules/recent-data-usage.mdc +2 -1
  14. package/.cursor/rules/testing-guide/testing-guide.mdc +262 -238
  15. package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +1 -1
  16. package/.cursor/rules/zustand-action-patterns.mdc +1 -1
  17. package/.cursor/rules/zustand-slice-organization.mdc +4 -4
  18. package/CHANGELOG.md +51 -0
  19. package/CLAUDE.md +1 -1
  20. package/GEMINI.md +1 -1
  21. package/changelog/v1.json +14 -0
  22. package/docs/development/database-schema.dbml +16 -0
  23. package/locales/en-US/chat.json +24 -0
  24. package/locales/zh-CN/chat.json +24 -0
  25. package/package.json +1 -1
  26. package/packages/business/const/src/index.ts +3 -0
  27. package/packages/database/migrations/0069_add_topic_shares_table.sql +22 -0
  28. package/packages/database/migrations/meta/0069_snapshot.json +9704 -0
  29. package/packages/database/migrations/meta/_journal.json +7 -0
  30. package/packages/database/src/models/__tests__/topicShare.test.ts +318 -0
  31. package/packages/database/src/models/topicShare.ts +177 -0
  32. package/packages/database/src/schemas/topic.ts +44 -2
  33. package/packages/types/src/conversation.ts +5 -0
  34. package/packages/types/src/topic/topic.ts +46 -0
  35. package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +24 -9
  36. package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +2 -1
  37. package/src/app/[variants]/(main)/agent/features/Portal/_layout/Mobile.tsx +3 -3
  38. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +3 -2
  39. package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +26 -9
  40. package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +2 -1
  41. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +3 -3
  42. package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -1
  43. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/AspectRatioSelect/index.tsx +1 -2
  44. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ImageNum.tsx +54 -173
  45. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ResolutionSelect.tsx +22 -67
  46. package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +18 -0
  47. package/src/app/[variants]/router/desktopRouter.config.tsx +18 -0
  48. package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +54 -0
  49. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +170 -0
  50. package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +66 -0
  51. package/src/app/[variants]/share/t/[id]/index.tsx +112 -0
  52. package/src/app/robots.tsx +1 -1
  53. package/src/business/client/BusinessMobileRoutes.tsx +1 -1
  54. package/src/features/Conversation/ChatList/index.tsx +17 -6
  55. package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +8 -4
  56. package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +15 -10
  57. package/src/features/Conversation/Messages/AssistantGroup/Tools.tsx +3 -1
  58. package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +3 -2
  59. package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +2 -2
  60. package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +25 -26
  61. package/src/features/Conversation/Messages/Supervisor/components/Group.tsx +4 -2
  62. package/src/features/Conversation/Messages/Tool/Tool/index.tsx +16 -12
  63. package/src/features/Conversation/Messages/Tool/index.tsx +20 -11
  64. package/src/features/Conversation/Messages/index.tsx +1 -1
  65. package/src/features/Conversation/store/slices/data/action.test.ts +42 -0
  66. package/src/features/Conversation/store/slices/data/action.ts +4 -2
  67. package/src/features/Portal/GroupThread/Header/index.tsx +2 -2
  68. package/src/features/Portal/MessageDetail/Body/index.tsx +3 -3
  69. package/src/features/Portal/components/Header.tsx +3 -3
  70. package/src/features/ProfileEditor/AgentTool.tsx +50 -19
  71. package/src/features/SharePopover/index.tsx +215 -0
  72. package/src/features/SharePopover/style.ts +10 -0
  73. package/src/hooks/useNavigateToAgent.ts +3 -3
  74. package/src/libs/next/proxy/define-config.ts +4 -1
  75. package/src/locales/default/chat.ts +26 -0
  76. package/src/proxy.ts +1 -0
  77. package/src/server/routers/lambda/__tests__/message.test.ts +152 -0
  78. package/src/server/routers/lambda/__tests__/share.test.ts +227 -0
  79. package/src/server/routers/lambda/__tests__/topic.test.ts +174 -0
  80. package/src/server/routers/lambda/index.ts +2 -0
  81. package/src/server/routers/lambda/message.ts +37 -4
  82. package/src/server/routers/lambda/share.ts +55 -0
  83. package/src/server/routers/lambda/topic.ts +45 -0
  84. package/src/services/message/index.ts +1 -0
  85. package/src/services/topic/index.ts +16 -0
  86. package/src/store/chat/slices/portal/action.test.ts +0 -41
  87. package/src/store/chat/slices/portal/action.ts +0 -25
  88. package/src/store/chat/slices/thread/action.test.ts +10 -6
  89. package/src/store/chat/slices/thread/action.ts +10 -3
  90. package/src/app/[variants]/(main)/group/features/Portal/features/Portal.tsx +0 -105
  91. package/src/app/[variants]/(main)/group/features/Portal/features/PortalPanel.tsx +0 -23
@@ -6,6 +6,7 @@ import { createStoreUpdater } from 'zustand-utils';
6
6
  import { useFetchThreads } from '@/hooks/useFetchThreads';
7
7
  import { useQueryState } from '@/hooks/useQueryParam';
8
8
  import { useChatStore } from '@/store/chat';
9
+ import { PortalViewType } from '@/store/chat/slices/portal/initialState';
9
10
 
10
11
  // sync outside state to useChatStore
11
12
  const ThreadHydration = memo(() => {
@@ -31,7 +32,7 @@ const ThreadHydration = memo(() => {
31
32
  // should open portal automatically when portalThread is set
32
33
  useEffect(() => {
33
34
  if (!!portalThread && !useChatStore.getState().showPortal) {
34
- useChatStore.getState().togglePortal(true);
35
+ useChatStore.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
35
36
  }
36
37
  }, [portalThread]);
37
38
 
@@ -16,10 +16,10 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
16
16
  }));
17
17
 
18
18
  const Layout = () => {
19
- const [showMobilePortal, isPortalThread, togglePortal] = useChatStore((s) => [
19
+ const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
20
20
  s.showPortal,
21
21
  portalThreadSelectors.showThread(s),
22
- s.togglePortal,
22
+ s.clearPortalStack,
23
23
  ]);
24
24
  const { t } = useTranslation('portal');
25
25
 
@@ -42,7 +42,7 @@ const Layout = () => {
42
42
  destroyOnHidden
43
43
  footer={null}
44
44
  height={'95%'}
45
- onCancel={() => togglePortal(false)}
45
+ onCancel={() => clearPortalStack()}
46
46
  open={showMobilePortal}
47
47
  styles={{
48
48
  body: { padding: 0 },
@@ -11,6 +11,7 @@ import UserAvatar from '@/features/User/UserAvatar';
11
11
  import { useAgentGroupStore } from '@/store/agentGroup';
12
12
  import { agentGroupSelectors } from '@/store/agentGroup/selectors';
13
13
  import { useChatStore } from '@/store/chat';
14
+ import { PortalViewType } from '@/store/chat/slices/portal/initialState';
14
15
  import { useUserStore } from '@/store/user';
15
16
  import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
16
17
 
@@ -36,7 +37,7 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
36
37
  const addAgentsToGroup = useAgentGroupStore((s) => s.addAgentsToGroup);
37
38
  const removeAgentFromGroup = useAgentGroupStore((s) => s.removeAgentFromGroup);
38
39
  const toggleThread = useAgentGroupStore((s) => s.toggleThread);
39
- const togglePortal = useChatStore((s) => s.togglePortal);
40
+ const pushPortalView = useChatStore((s) => s.pushPortalView);
40
41
 
41
42
  // Get members from store (excluding supervisor)
42
43
  const groupMembers = useAgentGroupStore(agentGroupSelectors.getGroupMembers(groupId || ''));
@@ -76,7 +77,7 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
76
77
 
77
78
  const handleMemberClick = (agentId: string) => {
78
79
  toggleThread(agentId);
79
- togglePortal(true);
80
+ pushPortalView({ agentId, type: PortalViewType.GroupThread });
80
81
  };
81
82
 
82
83
  return (
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { ENABLE_TOPIC_LINK_SHARE } from '@lobechat/business-const';
3
4
  import { ActionIcon } from '@lobehub/ui';
4
5
  import { Share2 } from 'lucide-react';
5
6
  import dynamic from 'next/dynamic';
@@ -8,8 +9,12 @@ import { useTranslation } from 'react-i18next';
8
9
 
9
10
  import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
10
11
  import { useWorkspaceModal } from '@/hooks/useWorkspaceModal';
12
+ import { useChatStore } from '@/store/chat';
13
+
14
+ console.log('ENABLE_TOPIC_LINK_SHARE', ENABLE_TOPIC_LINK_SHARE);
11
15
 
12
16
  const ShareModal = dynamic(() => import('@/features/ShareModal'));
17
+ const SharePopover = dynamic(() => import('@/features/SharePopover'));
13
18
 
14
19
  interface ShareButtonProps {
15
20
  mobile?: boolean;
@@ -20,18 +25,30 @@ interface ShareButtonProps {
20
25
  const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
21
26
  const [isModalOpen, setIsModalOpen] = useWorkspaceModal(open, setOpen);
22
27
  const { t } = useTranslation('common');
28
+ const activeTopicId = useChatStore((s) => s.activeTopicId);
29
+
30
+ // Hide share button when no topic exists (no messages sent yet)
31
+ if (!activeTopicId) return null;
32
+
33
+ const iconButton = (
34
+ <ActionIcon
35
+ icon={Share2}
36
+ onClick={ENABLE_TOPIC_LINK_SHARE ? undefined : () => setIsModalOpen(true)}
37
+ size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
38
+ title={t('share')}
39
+ tooltipProps={{
40
+ placement: 'bottom',
41
+ }}
42
+ />
43
+ );
23
44
 
24
45
  return (
25
46
  <>
26
- <ActionIcon
27
- icon={Share2}
28
- onClick={() => setIsModalOpen(true)}
29
- size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
30
- title={t('share')}
31
- tooltipProps={{
32
- placement: 'bottom',
33
- }}
34
- />
47
+ {ENABLE_TOPIC_LINK_SHARE ? (
48
+ <SharePopover onOpenModal={() => setIsModalOpen(true)}>{iconButton}</SharePopover>
49
+ ) : (
50
+ iconButton
51
+ )}
35
52
  <ShareModal onCancel={() => setIsModalOpen(false)} open={isModalOpen} />
36
53
  </>
37
54
  );
@@ -6,6 +6,7 @@ import { createStoreUpdater } from 'zustand-utils';
6
6
  import { useFetchThreads } from '@/hooks/useFetchThreads';
7
7
  import { useQueryState } from '@/hooks/useQueryParam';
8
8
  import { useChatStore } from '@/store/chat';
9
+ import { PortalViewType } from '@/store/chat/slices/portal/initialState';
9
10
 
10
11
  // sync outside state to useChatStore
11
12
  const ThreadHydration = memo(() => {
@@ -31,7 +32,7 @@ const ThreadHydration = memo(() => {
31
32
  // should open portal automatically when portalThread is set
32
33
  useEffect(() => {
33
34
  if (!!portalThread && !useChatStore.getState().showPortal) {
34
- useChatStore.getState().togglePortal(true);
35
+ useChatStore.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
35
36
  }
36
37
  }, [portalThread]);
37
38
 
@@ -16,10 +16,10 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
16
16
  }));
17
17
 
18
18
  const Layout = () => {
19
- const [showMobilePortal, isPortalThread, togglePortal] = useChatStore((s) => [
19
+ const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
20
20
  s.showPortal,
21
21
  portalThreadSelectors.showThread(s),
22
- s.togglePortal,
22
+ s.clearPortalStack,
23
23
  ]);
24
24
  const { t } = useTranslation('portal');
25
25
 
@@ -42,7 +42,7 @@ const Layout = () => {
42
42
  destroyOnHidden
43
43
  footer={null}
44
44
  height={'95%'}
45
- onCancel={() => togglePortal(false)}
45
+ onCancel={() => clearPortalStack()}
46
46
  open={showMobilePortal}
47
47
  styles={{
48
48
  body: { padding: 0 },
@@ -1,13 +1,16 @@
1
1
  'use client';
2
2
 
3
3
  import { AgentTool as SharedAgentTool } from '@/features/ProfileEditor';
4
+ import { useGroupProfileStore } from '@/store/groupProfile';
4
5
 
5
6
  /**
6
7
  * AgentTool for group profile editor
7
8
  * - Uses default settings (no web browsing, no filterAvailableInWeb, uses metaList)
9
+ * - Passes agentId from group profile store to display the correct member's plugins
8
10
  */
9
11
  const AgentTool = () => {
10
- return <SharedAgentTool />;
12
+ const agentId = useGroupProfileStore((s) => s.activeTabId);
13
+ return <SharedAgentTool agentId={agentId} />;
11
14
  };
12
15
 
13
16
  export default AgentTool;
@@ -29,8 +29,7 @@ const AspectRatioSelect = memo<AspectRatioSelectProps>(
29
29
  {options?.map((item) => {
30
30
  const [width, height] = item.value.split(':').map(Number);
31
31
  const isWidthGreater = width > height;
32
- const isEqual = width === height;
33
- const isActive = isEqual ? item.value === '1:1' : active === item.value;
32
+ const isActive = active === item.value;
34
33
  return (
35
34
  <Block
36
35
  align={'center'}
@@ -1,116 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
- import { ActionIcon, Flexbox, InputNumber } from '@lobehub/ui';
5
- import { createStaticStyles, cx } from 'antd-style';
4
+ import { ActionIcon, Flexbox, InputNumber, Segmented } from '@lobehub/ui';
6
5
  import { Check, Plus, X } from 'lucide-react';
7
- import { memo, useCallback, useEffect, useRef, useState } from 'react';
6
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
7
 
9
8
  import { useImageStore } from '@/store/image';
10
9
  import { imageGenerationConfigSelectors } from '@/store/image/selectors';
11
10
 
12
11
  const DEFAULT_IMAGE_NUM_MAX = ENABLE_BUSINESS_FEATURES ? 8 : 50;
13
-
14
- const styles = createStaticStyles(({ css, cssVar }) => ({
15
- actionButton: css`
16
- flex-shrink: 0;
17
- `,
18
-
19
- button: css`
20
- cursor: pointer;
21
-
22
- display: flex;
23
- align-items: center;
24
- justify-content: center;
25
-
26
- min-width: 40px;
27
- height: 32px;
28
- padding-block: 0;
29
- padding-inline: 12px;
30
- border: 1px solid ${cssVar.colorBorder};
31
- border-radius: ${cssVar.borderRadius}px;
32
-
33
- font-size: 14px;
34
- font-weight: 500;
35
- color: ${cssVar.colorText};
36
-
37
- background: ${cssVar.colorBgContainer};
38
-
39
- transition: all 0.2s ease;
40
-
41
- &:hover {
42
- border-color: ${cssVar.colorPrimary};
43
- background: ${cssVar.colorBgTextHover};
44
- }
45
-
46
- &:disabled {
47
- cursor: not-allowed;
48
- opacity: 0.5;
49
-
50
- &:hover {
51
- border-color: ${cssVar.colorBorder};
52
- background: ${cssVar.colorBgContainer};
53
- }
54
- }
55
- `,
56
-
57
- cancelButton: css`
58
- border-color: ${cssVar.colorBorder};
59
- color: ${cssVar.colorTextTertiary};
60
-
61
- &:hover {
62
- border-color: ${cssVar.colorBorderSecondary};
63
- color: ${cssVar.colorText};
64
- background: ${cssVar.colorBgTextHover};
65
- }
66
- `,
67
-
68
- confirmButton: css`
69
- border-color: ${cssVar.colorSuccess};
70
- color: ${cssVar.colorSuccess};
71
-
72
- &:hover {
73
- border-color: ${cssVar.colorSuccessHover};
74
- color: ${cssVar.colorSuccessHover};
75
- background: ${cssVar.colorSuccessBg};
76
- }
77
- `,
78
-
79
- container: css`
80
- display: flex;
81
- gap: 8px;
82
- align-items: center;
83
- `,
84
-
85
- editContainer: css`
86
- display: flex;
87
- gap: 8px;
88
- align-items: center;
89
- width: 100%;
90
- `,
91
-
92
- input: css`
93
- flex: 1;
94
- min-width: 80px;
95
-
96
- .ant-input {
97
- font-weight: 500;
98
- text-align: center;
99
- }
100
- `,
101
-
102
- selectedButton: css`
103
- border-color: ${cssVar.colorPrimary};
104
- color: ${cssVar.colorPrimary};
105
- background: ${cssVar.colorPrimaryBg};
106
-
107
- &:hover {
108
- border-color: ${cssVar.colorPrimary};
109
- color: ${cssVar.colorPrimary};
110
- background: ${cssVar.colorPrimaryBgHover};
111
- }
112
- `,
113
- }));
12
+ const CUSTOM_VALUE = '__custom__';
114
13
 
115
14
  interface ImageNumSelectorProps {
116
15
  disabled?: boolean;
@@ -130,34 +29,52 @@ const ImageNum = memo<ImageNumSelectorProps>(
130
29
 
131
30
  const isCustomValue = !presetCounts.includes(imageNum);
132
31
 
133
- // 处理预设按钮点击
134
- const handlePresetClick = useCallback(
135
- (count: number) => {
32
+ const options = useMemo(() => {
33
+ const items = presetCounts.map((count) => ({
34
+ label: String(count),
35
+ value: count,
36
+ }));
37
+
38
+ // Add custom option or show current custom value
39
+ if (isCustomValue) {
40
+ items.push({
41
+ label: String(imageNum),
42
+ value: imageNum,
43
+ });
44
+ } else {
45
+ items.push({
46
+ label: <Plus size={16} style={{ verticalAlign: 'middle' }} />,
47
+ value: CUSTOM_VALUE,
48
+ } as any);
49
+ }
50
+
51
+ return items;
52
+ }, [presetCounts, isCustomValue, imageNum]);
53
+
54
+ const handleChange = useCallback(
55
+ (value: number | string) => {
136
56
  if (disabled) return;
137
- setImageNum(count);
57
+
58
+ if (value === CUSTOM_VALUE || (isCustomValue && value === imageNum)) {
59
+ // Enter edit mode
60
+ setCustomCount(imageNum);
61
+ customCountRef.current = imageNum;
62
+ setIsEditing(true);
63
+ } else {
64
+ setImageNum(value as number);
65
+ }
138
66
  },
139
- [disabled, setImageNum],
67
+ [disabled, isCustomValue, imageNum, setImageNum],
140
68
  );
141
69
 
142
- // 进入编辑模式
143
- const handleEditStart = useCallback(() => {
144
- if (disabled) return;
145
- setCustomCount(imageNum);
146
- customCountRef.current = imageNum;
147
- setIsEditing(true);
148
- }, [disabled, imageNum]);
149
-
150
- // 确认自定义输入
151
70
  const handleCustomConfirm = useCallback(() => {
152
71
  let count = customCountRef.current;
153
72
 
154
- // 如果解析失败或输入为空,使用当前值
155
73
  if (count === null) {
156
74
  setIsEditing(false);
157
75
  return;
158
76
  }
159
77
 
160
- // 智能处理超出范围的值 (作为二次保险)
161
78
  if (count > max) {
162
79
  count = max;
163
80
  } else if (count < min) {
@@ -174,10 +91,7 @@ const ImageNum = memo<ImageNumSelectorProps>(
174
91
  setCustomCount(null);
175
92
  }, []);
176
93
 
177
- // 处理输入变化
178
94
  const handleInputChange = useCallback((value: number | string | null) => {
179
- console.log('handleInputChange', value);
180
-
181
95
  if (value === null) {
182
96
  setCustomCount(null);
183
97
  customCountRef.current = null;
@@ -192,10 +106,8 @@ const ImageNum = memo<ImageNumSelectorProps>(
192
106
  }
193
107
  }, []);
194
108
 
195
- // 自动聚焦和选择输入框内容
196
109
  useEffect(() => {
197
110
  if (isEditing) {
198
- // 延迟聚焦以确保 input 已渲染
199
111
  setTimeout(() => {
200
112
  if (inputRef.current) {
201
113
  inputRef.current.focus();
@@ -205,16 +117,12 @@ const ImageNum = memo<ImageNumSelectorProps>(
205
117
  }
206
118
  }, [isEditing]);
207
119
 
208
- // 验证输入是否有效
209
- const isValidInput = useCallback(() => {
210
- return customCount !== null;
211
- }, [customCount]);
120
+ const isValidInput = customCount !== null;
212
121
 
213
122
  if (isEditing) {
214
123
  return (
215
- <div className={styles.editContainer}>
124
+ <Flexbox gap={8} horizontal style={{ width: '100%' }}>
216
125
  <InputNumber
217
- className={styles.input}
218
126
  max={max}
219
127
  min={min}
220
128
  onChange={handleInputChange}
@@ -228,59 +136,32 @@ const ImageNum = memo<ImageNumSelectorProps>(
228
136
  placeholder={`${min}-${max}`}
229
137
  ref={inputRef}
230
138
  size="small"
139
+ style={{ flex: 1 }}
231
140
  value={customCount}
232
141
  />
233
142
  <ActionIcon
234
- className={cx(styles.actionButton, styles.confirmButton)}
235
- disabled={!isValidInput()}
143
+ color="success"
144
+ disabled={!isValidInput}
236
145
  icon={Check}
237
146
  onClick={handleCustomConfirm}
238
147
  size="small"
148
+ variant="filled"
239
149
  />
240
- <ActionIcon
241
- className={cx(styles.actionButton, styles.cancelButton)}
242
- icon={X}
243
- onClick={handleCustomCancel}
244
- size="small"
245
- />
246
- </div>
150
+ <ActionIcon icon={X} onClick={handleCustomCancel} size="small" variant="filled" />
151
+ </Flexbox>
247
152
  );
248
153
  }
249
154
 
250
155
  return (
251
- <Flexbox className={styles.container} horizontal>
252
- {presetCounts.map((count) => (
253
- <button
254
- className={cx(styles.button, imageNum === count && styles.selectedButton)}
255
- disabled={disabled}
256
- key={count}
257
- onClick={() => handlePresetClick(count)}
258
- type="button"
259
- >
260
- {count}
261
- </button>
262
- ))}
263
-
264
- {isCustomValue ? (
265
- <button
266
- className={cx(styles.button, styles.selectedButton)}
267
- disabled={disabled}
268
- onClick={handleEditStart}
269
- type="button"
270
- >
271
- {imageNum}
272
- </button>
273
- ) : (
274
- <button
275
- className={styles.button}
276
- disabled={disabled}
277
- onClick={handleEditStart}
278
- type="button"
279
- >
280
- <Plus size={16} />
281
- </button>
282
- )}
283
- </Flexbox>
156
+ <Segmented
157
+ block
158
+ disabled={disabled}
159
+ onChange={handleChange}
160
+ options={options}
161
+ style={{ width: '100%' }}
162
+ value={isCustomValue ? imageNum : imageNum}
163
+ variant="filled"
164
+ />
284
165
  );
285
166
  },
286
167
  );
@@ -1,86 +1,41 @@
1
- import { Flexbox } from '@lobehub/ui';
2
- import { createStaticStyles, cx } from 'antd-style';
3
- import { memo, useCallback } from 'react';
1
+ import { Segmented } from '@lobehub/ui';
2
+ import { memo, useCallback, useMemo } from 'react';
4
3
  import { useTranslation } from 'react-i18next';
5
4
 
6
5
  import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
7
6
 
8
- const styles = createStaticStyles(({ css, cssVar }) => ({
9
- button: css`
10
- cursor: pointer;
11
-
12
- display: flex;
13
- align-items: center;
14
- justify-content: center;
15
-
16
- min-width: 60px;
17
- height: 32px;
18
- padding-block: 0;
19
- padding-inline: 16px;
20
- border: 1px solid ${cssVar.colorBorder};
21
- border-radius: ${cssVar.borderRadius}px;
22
-
23
- font-size: 14px;
24
- font-weight: 500;
25
- color: ${cssVar.colorText};
26
-
27
- background: ${cssVar.colorBgContainer};
28
-
29
- transition: all 0.2s ease;
30
-
31
- &:hover {
32
- border-color: ${cssVar.colorPrimary};
33
- background: ${cssVar.colorBgTextHover};
34
- }
35
- `,
36
-
37
- container: css`
38
- display: flex;
39
- gap: 8px;
40
- align-items: center;
41
- `,
42
-
43
- selectedButton: css`
44
- border-color: ${cssVar.colorPrimary};
45
- color: ${cssVar.colorPrimary};
46
- background: ${cssVar.colorPrimaryBg};
47
-
48
- &:hover {
49
- border-color: ${cssVar.colorPrimary};
50
- color: ${cssVar.colorPrimary};
51
- background: ${cssVar.colorPrimaryBgHover};
52
- }
53
- `,
54
- }));
55
-
56
7
  const ResolutionSelect = memo(() => {
57
8
  const { t } = useTranslation('image');
58
9
  const { value, setValue, enumValues } = useGenerationConfigParam('resolution');
59
10
 
60
- const handleClick = useCallback(
61
- (resolution: string) => {
62
- setValue(resolution);
11
+ const handleChange = useCallback(
12
+ (resolution: string | number) => {
13
+ setValue(String(resolution));
63
14
  },
64
15
  [setValue],
65
16
  );
66
17
 
67
- if (!enumValues || enumValues.length === 0) {
18
+ const options = useMemo(() => {
19
+ if (!enumValues || enumValues.length === 0) return [];
20
+ return enumValues.map((resolution) => ({
21
+ label: t(`config.resolution.options.${resolution}`, { defaultValue: resolution }),
22
+ value: resolution,
23
+ }));
24
+ }, [enumValues, t]);
25
+
26
+ if (options.length === 0) {
68
27
  return null;
69
28
  }
70
29
 
71
30
  return (
72
- <Flexbox className={styles.container} horizontal>
73
- {enumValues.map((resolution) => (
74
- <button
75
- className={cx(styles.button, value === resolution && styles.selectedButton)}
76
- key={resolution}
77
- onClick={() => handleClick(resolution)}
78
- type="button"
79
- >
80
- {t(`config.resolution.options.${resolution}`, { defaultValue: resolution })}
81
- </button>
82
- ))}
83
- </Flexbox>
31
+ <Segmented
32
+ block
33
+ onChange={handleChange}
34
+ options={options}
35
+ style={{ width: '100%' }}
36
+ value={value}
37
+ variant="filled"
38
+ />
84
39
  );
85
40
  });
86
41
 
@@ -254,4 +254,22 @@ export const mobileRoutes: RouteConfig[] = [
254
254
  path: '/onboarding',
255
255
  },
256
256
  ...BusinessMobileRoutesWithoutMainLayout,
257
+
258
+ // Share topic route (outside main layout)
259
+ {
260
+ children: [
261
+ {
262
+ element: dynamicElement(
263
+ () => import('../../share/t/[id]'),
264
+ 'Mobile > Share > Topic',
265
+ ),
266
+ path: ':id',
267
+ },
268
+ ],
269
+ element: dynamicElement(
270
+ () => import('../../share/t/[id]/_layout'),
271
+ 'Mobile > Share > Topic > Layout',
272
+ ),
273
+ path: '/share/t',
274
+ },
257
275
  ];
@@ -398,6 +398,24 @@ export const desktopRoutes: RouteConfig[] = [
398
398
  // Onboarding route (outside main layout)
399
399
 
400
400
  ...BusinessDesktopRoutesWithoutMainLayout,
401
+
402
+ // Share topic route (outside main layout)
403
+ {
404
+ children: [
405
+ {
406
+ element: dynamicElement(
407
+ () => import('../share/t/[id]'),
408
+ 'Desktop > Share > Topic',
409
+ ),
410
+ path: ':id',
411
+ },
412
+ ],
413
+ element: dynamicElement(
414
+ () => import('../share/t/[id]/_layout'),
415
+ 'Desktop > Share > Topic > Layout',
416
+ ),
417
+ path: '/share/t',
418
+ },
401
419
  ];
402
420
 
403
421
  // Desktop onboarding route (SPA-only)