@lobehub/lobehub 2.1.14 → 2.1.16

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 (27) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v2.json +18 -0
  3. package/locales/en-US/discover.json +9 -0
  4. package/locales/en-US/setting.json +3 -0
  5. package/locales/zh-CN/discover.json +9 -0
  6. package/locales/zh-CN/setting.json +3 -0
  7. package/package.json +1 -1
  8. package/packages/types/src/discover/assistants.ts +1 -0
  9. package/packages/types/src/discover/index.ts +8 -0
  10. package/packages/utils/src/chunkers/trimBatchProbe/trimBatchProbe.ts +1 -1
  11. package/packages/utils/src/pricing.ts +6 -6
  12. package/src/app/[variants]/(main)/agent/profile/features/Header/AgentPublishButton/PublishButton.tsx +52 -8
  13. package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Header.tsx +3 -3
  14. package/src/app/[variants]/(main)/community/(detail)/user/features/DetailProvider.tsx +2 -0
  15. package/src/app/[variants]/(main)/community/(detail)/user/features/StatusFilter.tsx +36 -0
  16. package/src/app/[variants]/(main)/community/(detail)/user/features/UserAgentList.tsx +69 -13
  17. package/src/app/[variants]/(main)/community/(detail)/user/features/UserContent.tsx +0 -11
  18. package/src/app/[variants]/(main)/community/(detail)/user/features/UserGroupList.tsx +68 -14
  19. package/src/app/[variants]/(main)/community/(detail)/user/index.tsx +3 -1
  20. package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +2 -1
  21. package/src/app/[variants]/(main)/group/profile/features/Header/GroupPublishButton/PublishButton.tsx +52 -8
  22. package/src/locales/default/discover.ts +12 -0
  23. package/src/locales/default/setting.ts +3 -0
  24. package/src/server/routers/lambda/market/social.ts +1 -1
  25. package/src/server/services/discover/index.ts +57 -2
  26. package/src/services/models.ts +0 -1
  27. package/src/services/social.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 2.1.16](https://github.com/lobehub/lobe-chat/compare/v2.1.15...v2.1.16)
6
+
7
+ <sup>Released on **2026-02-04**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Add the preview publish to market button preview check.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Add the preview publish to market button preview check, closes [#12105](https://github.com/lobehub/lobe-chat/issues/12105) ([28887c7](https://github.com/lobehub/lobe-chat/commit/28887c7))
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
+
30
+ ### [Version 2.1.15](https://github.com/lobehub/lobe-chat/compare/v2.1.14...v2.1.15)
31
+
32
+ <sup>Released on **2026-02-04**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fixed the agents list the show updateAt time error.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fixed the agents list the show updateAt time error, closes [#12103](https://github.com/lobehub/lobe-chat/issues/12103) ([3063cee](https://github.com/lobehub/lobe-chat/commit/3063cee))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 2.1.14](https://github.com/lobehub/lobe-chat/compare/v2.1.13...v2.1.14)
6
56
 
7
57
  <sup>Released on **2026-02-04**</sup>
package/changelog/v2.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Add the preview publish to market button preview check."
6
+ ]
7
+ },
8
+ "date": "2026-02-04",
9
+ "version": "2.1.16"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Fixed the agents list the show updateAt time error."
15
+ ]
16
+ },
17
+ "date": "2026-02-04",
18
+ "version": "2.1.15"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
@@ -490,6 +490,15 @@
490
490
  "user.noForkedAgentGroups": "No forked Agent Groups yet",
491
491
  "user.noForkedAgents": "No forked Agents yet",
492
492
  "user.publishedAgents": "Created Agents",
493
+ "user.publishedGroups": "Created Groups",
494
+ "user.searchPlaceholder": "Search by name or description...",
495
+ "user.statusFilter.all": "All",
496
+ "user.statusFilter.archived": "Archived",
497
+ "user.statusFilter.deprecated": "Deprecated",
498
+ "user.statusFilter.favorite": "Favorite",
499
+ "user.statusFilter.forked": "Forked",
500
+ "user.statusFilter.published": "Published",
501
+ "user.statusFilter.unpublished": "Under Review",
493
502
  "user.tabs.favorites": "Favorites",
494
503
  "user.tabs.forkedAgents": "Forked",
495
504
  "user.tabs.publishedAgents": "Created",
@@ -268,6 +268,9 @@
268
268
  "marketPublish.upload.button": "Publish New Version",
269
269
  "marketPublish.upload.tooltip": "Publish a new version to Agent Community",
270
270
  "marketPublish.uploadGroup.tooltip": "Publish a new version to Group Community",
271
+ "marketPublish.validation.confirmPublish": "Are you sure you want to publish to the market?",
272
+ "marketPublish.validation.emptyName": "Cannot publish: Name is required",
273
+ "marketPublish.validation.emptySystemRole": "Cannot publish: System Role is required",
271
274
  "memory.enabled.desc": "Allow LobeHub to extract preferences and info from conversations and use them later. You can view, edit, or clear memory anytime.",
272
275
  "memory.enabled.title": "Enable Memory",
273
276
  "memory.title": "Memory Settings",
@@ -490,6 +490,15 @@
490
490
  "user.noForkedAgentGroups": "尚无已派生的代理组",
491
491
  "user.noForkedAgents": "尚无已派生的代理",
492
492
  "user.publishedAgents": "创作的助理",
493
+ "user.publishedGroups": "创作的群组",
494
+ "user.searchPlaceholder": "搜索名称或描述...",
495
+ "user.statusFilter.all": "全部",
496
+ "user.statusFilter.archived": "已归档",
497
+ "user.statusFilter.deprecated": "已废弃",
498
+ "user.statusFilter.favorite": "已收藏",
499
+ "user.statusFilter.forked": "已派生",
500
+ "user.statusFilter.published": "已发布",
501
+ "user.statusFilter.unpublished": "审核中",
493
502
  "user.tabs.favorites": "收藏",
494
503
  "user.tabs.forkedAgents": "已派生",
495
504
  "user.tabs.publishedAgents": "创作",
@@ -268,6 +268,9 @@
268
268
  "marketPublish.upload.button": "发布新版本",
269
269
  "marketPublish.upload.tooltip": "发布新版本到助理社区",
270
270
  "marketPublish.uploadGroup.tooltip": "向群组社区发布新版本",
271
+ "marketPublish.validation.confirmPublish": "确定要发布到市场吗?",
272
+ "marketPublish.validation.emptyName": "无法发布:名称不能为空",
273
+ "marketPublish.validation.emptySystemRole": "无法发布:系统角色不能为空",
271
274
  "memory.enabled.desc": "允许 LobeHub 从对话中提取偏好和信息,并在之后使用。您可以随时查看、编辑或清除记忆内容。",
272
275
  "memory.enabled.title": "启用记忆功能",
273
276
  "memory.title": "记忆设置",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -62,6 +62,7 @@ export interface DiscoverAssistantItem extends Omit<LobeAgentSettings, 'meta'>,
62
62
  status?: AgentStatus;
63
63
  tokenUsage: number;
64
64
  type?: AgentType;
65
+ updatedAt?: string;
65
66
  userName?: string;
66
67
  }
67
68
 
@@ -67,6 +67,14 @@ export interface DiscoverUserInfo {
67
67
  export interface DiscoverUserProfile {
68
68
  agentGroups?: DiscoverGroupAgentItem[];
69
69
  agents: DiscoverAssistantItem[];
70
+ /**
71
+ * Agent groups favorited by the user
72
+ */
73
+ favoriteAgentGroups?: DiscoverGroupAgentItem[];
74
+ /**
75
+ * Agents favorited by the user
76
+ */
77
+ favoriteAgents?: DiscoverAssistantItem[];
70
78
  /**
71
79
  * Agent groups forked by the user
72
80
  */
@@ -202,7 +202,7 @@ export const handleSingle = async (
202
202
  * to avoid breaking structured XML/JSON-like content mid-way.
203
203
  *
204
204
  * Bisection example (8 segments, keep newest):
205
- * try 4 (fits?) yes try 6 no try 5 yes best=5
205
+ * try 4 (fits?) -> yes -> try 6 -> no -> try 5 -> yes => best=5
206
206
  * if compact retry needed, repeat with build(true) and pick the better fit.
207
207
  *
208
208
  * This minimizes structural breakage by preferring whole built segments and only truncating the last one as a last resort.
@@ -2,9 +2,9 @@ import { Pricing, PricingUnit, PricingUnitName } from 'model-bank';
2
2
 
3
3
  /**
4
4
  * Internal helper to extract the displayed unit rate from a pricing unit by strategy
5
- * - fixed rate
6
- * - tiered tiers[0].rate
7
- * - lookup first price value
5
+ * - fixed: rate
6
+ * - tiered: tiers[0].rate
7
+ * - lookup: first price value
8
8
  */
9
9
  const getRateFromUnit = (unit: PricingUnit): number | undefined => {
10
10
  switch (unit.strategy) {
@@ -39,9 +39,9 @@ export const getUnitRateByName = (
39
39
 
40
40
  /**
41
41
  * Get text input unit rate from pricing
42
- * - fixed rate
43
- * - tiered tiers[0].rate
44
- * - lookup Object.values(lookup.prices)[0]
42
+ * - fixed: rate
43
+ * - tiered: tiers[0].rate
44
+ * - lookup: Object.values(lookup.prices)[0]
45
45
  */
46
46
  export function getTextInputUnitRate(pricing?: Pricing): number | undefined {
47
47
  return getUnitRateByName(pricing, 'textInput');
@@ -1,11 +1,15 @@
1
1
  import { Button } from '@lobehub/ui';
2
2
  import { ShapesUploadIcon } from '@lobehub/ui/icons';
3
+ import { Popconfirm } from 'antd';
4
+ import isEqual from 'fast-deep-equal';
3
5
  import { memo, useCallback, useMemo, useState } from 'react';
4
6
  import { useTranslation } from 'react-i18next';
5
7
 
6
8
  import { message } from '@/components/AntdStaticMethods';
7
9
  import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
8
10
  import { resolveMarketAuthError } from '@/layout/AuthProvider/MarketAuth/errors';
11
+ import { useAgentStore } from '@/store/agent';
12
+ import { agentSelectors } from '@/store/agent/selectors';
9
13
 
10
14
  import ForkConfirmModal from './ForkConfirmModal';
11
15
  import type { MarketPublishAction } from './types';
@@ -25,10 +29,17 @@ const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess
25
29
  onSuccess: onPublishSuccess,
26
30
  });
27
31
 
32
+ // Agent data for validation
33
+ const meta = useAgentStore(agentSelectors.currentAgentMeta, isEqual);
34
+ const systemRole = useAgentStore(agentSelectors.currentAgentSystemRole);
35
+
28
36
  // Fork confirmation modal state
29
37
  const [showForkModal, setShowForkModal] = useState(false);
30
38
  const [originalAgentInfo, setOriginalAgentInfo] = useState<OriginalAgentInfo | null>(null);
31
39
 
40
+ // Publish confirmation popconfirm state
41
+ const [confirmOpened, setConfirmOpened] = useState(false);
42
+
32
43
  const buttonConfig = useMemo(() => {
33
44
  if (action === 'upload') {
34
45
  return {
@@ -60,7 +71,25 @@ const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess
60
71
  await publish();
61
72
  }, [checkOwnership, publish]);
62
73
 
63
- const handleButtonClick = useCallback(async () => {
74
+ const handleButtonClick = useCallback(() => {
75
+ // Validate name and systemRole
76
+ if (!meta?.title || meta.title.trim() === '') {
77
+ message.error({ content: t('marketPublish.validation.emptyName') });
78
+ return;
79
+ }
80
+
81
+ if (!systemRole || systemRole.trim() === '') {
82
+ message.error({ content: t('marketPublish.validation.emptySystemRole') });
83
+ return;
84
+ }
85
+
86
+ // Open popconfirm for user confirmation
87
+ setConfirmOpened(true);
88
+ }, [meta?.title, systemRole, t]);
89
+
90
+ const handleConfirmPublish = useCallback(async () => {
91
+ setConfirmOpened(false);
92
+
64
93
  if (!isAuthenticated) {
65
94
  try {
66
95
  await signIn();
@@ -98,14 +127,29 @@ const PublishButton = memo<MarketPublishButtonProps>(({ action, onPublishSuccess
98
127
 
99
128
  return (
100
129
  <>
101
- <Button
102
- icon={ShapesUploadIcon}
103
- loading={loading}
104
- onClick={handleButtonClick}
105
- title={buttonTitle}
130
+ <Popconfirm
131
+ arrow={false}
132
+ okButtonProps={{ type: 'primary' }}
133
+ onCancel={() => setConfirmOpened(false)}
134
+ onConfirm={handleConfirmPublish}
135
+ onOpenChange={(open) => {
136
+ if (!open) {
137
+ setConfirmOpened(false);
138
+ }
139
+ }}
140
+ open={confirmOpened}
141
+ placement="bottomRight"
142
+ title={t('marketPublish.validation.confirmPublish')}
106
143
  >
107
- {t('publishToCommunity')}
108
- </Button>
144
+ <Button
145
+ icon={ShapesUploadIcon}
146
+ loading={loading}
147
+ onClick={handleButtonClick}
148
+ title={buttonTitle}
149
+ >
150
+ {t('publishToCommunity')}
151
+ </Button>
152
+ </Popconfirm>
109
153
  <ForkConfirmModal
110
154
  loading={isPublishing}
111
155
  onCancel={handleForkCancel}
@@ -75,7 +75,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
75
75
  // Fetch favorite status
76
76
  const { data: favoriteStatus, mutate: mutateFavorite } = useSWR(
77
77
  identifier && isAuthenticated ? ['favorite-status', 'agent', identifier] : null,
78
- () => socialService.checkFavoriteStatus('agent', identifier!),
78
+ () => socialService.checkFavoriteStatus('agent-group', identifier!),
79
79
  { revalidateOnFocus: false },
80
80
  );
81
81
 
@@ -100,10 +100,10 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
100
100
  setFavoriteLoading(true);
101
101
  try {
102
102
  if (isFavorited) {
103
- await socialService.removeFavorite('agent', identifier);
103
+ await socialService.removeFavorite('agent-group', identifier);
104
104
  message.success(t('assistant.unfavoriteSuccess'));
105
105
  } else {
106
- await socialService.addFavorite('agent', identifier);
106
+ await socialService.addFavorite('agent-group', identifier);
107
107
  message.success(t('assistant.favoriteSuccess'));
108
108
  }
109
109
  await mutateFavorite();
@@ -13,6 +13,8 @@ export interface UserDetailContextConfig {
13
13
  agentCount: number;
14
14
  agentGroups?: DiscoverGroupAgentItem[];
15
15
  agents: DiscoverAssistantItem[];
16
+ favoriteAgentGroups?: DiscoverGroupAgentItem[];
17
+ favoriteAgents?: DiscoverAssistantItem[];
16
18
  forkedAgentGroups?: DiscoverGroupAgentItem[];
17
19
  forkedAgents?: DiscoverAssistantItem[];
18
20
  groupCount: number;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { Select } from 'antd';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ export type StatusFilterValue = 'published' | 'unpublished' | 'deprecated' | 'archived' | 'forked' | 'favorite';
8
+
9
+ interface StatusFilterProps {
10
+ onChange: (value: StatusFilterValue) => void;
11
+ value: StatusFilterValue;
12
+ }
13
+
14
+ const StatusFilter = memo<StatusFilterProps>(({ value, onChange }) => {
15
+ const { t } = useTranslation('discover');
16
+
17
+ const options = [
18
+ { label: t('user.statusFilter.published'), value: 'published' as const },
19
+ { label: t('user.statusFilter.unpublished'), value: 'unpublished' as const },
20
+ { label: t('user.statusFilter.deprecated'), value: 'deprecated' as const },
21
+ { label: t('user.statusFilter.archived'), value: 'archived' as const },
22
+ { label: t('user.statusFilter.forked'), value: 'forked' as const },
23
+ { label: t('user.statusFilter.favorite'), value: 'favorite' as const },
24
+ ];
25
+
26
+ return (
27
+ <Select
28
+ onChange={onChange}
29
+ options={options}
30
+ style={{ minWidth: 120 }}
31
+ value={value}
32
+ />
33
+ );
34
+ });
35
+
36
+ export default StatusFilter;
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { Flexbox, Grid, Tag, Text } from '@lobehub/ui';
4
- import { Pagination } from 'antd';
4
+ import { Input, Pagination } from 'antd';
5
5
  import { memo, useMemo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import AssistantEmpty from '../../../features/AssistantEmpty';
9
9
  import { useUserDetailContext } from './DetailProvider';
10
+ import StatusFilter, { type StatusFilterValue } from './StatusFilter';
10
11
  import UserAgentCard from './UserAgentCard';
11
12
 
12
13
  interface UserAgentListProps {
@@ -14,27 +15,82 @@ interface UserAgentListProps {
14
15
  rows?: number;
15
16
  }
16
17
 
17
- const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 10 }) => {
18
+ const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 8 }) => {
18
19
  const { t } = useTranslation('discover');
19
- const { agents, agentCount } = useUserDetailContext();
20
+ const { agents, agentCount, forkedAgents = [], favoriteAgents = [], isOwner } = useUserDetailContext();
20
21
  const [currentPage, setCurrentPage] = useState(1);
22
+ const [statusFilter, setStatusFilter] = useState<StatusFilterValue>('published');
23
+ const [searchQuery, setSearchQuery] = useState('');
24
+
25
+ // Combine agents and forked agents, then filter based on status and search
26
+ const filteredAgents = useMemo(() => {
27
+ let allAgents = [...agents];
28
+
29
+ if (statusFilter === 'forked') {
30
+ // Show only forked agents (those with forkedFromAgentId)
31
+ allAgents = forkedAgents;
32
+ } else if (statusFilter === 'favorite') {
33
+ // Show only favorited agents
34
+ allAgents = favoriteAgents;
35
+ } else {
36
+ // Filter by status for non-forked agents
37
+ allAgents = allAgents.filter((agent) => {
38
+ return agent.status === statusFilter;
39
+ });
40
+ }
41
+
42
+ // Apply search filter
43
+ if (searchQuery.trim()) {
44
+ const query = searchQuery.toLowerCase();
45
+ allAgents = allAgents.filter((agent) => {
46
+ console.log('agent', agent);
47
+ const name = agent?.title?.toLowerCase() || '';
48
+ const description = agent?.description?.toLowerCase() || '';
49
+ return name.includes(query) || description.includes(query);
50
+ });
51
+ }
52
+
53
+ return allAgents;
54
+ }, [agents, forkedAgents, statusFilter, searchQuery]);
21
55
 
22
56
  const paginatedAgents = useMemo(() => {
23
57
  const startIndex = (currentPage - 1) * pageSize;
24
- return agents.slice(startIndex, startIndex + pageSize);
25
- }, [agents, currentPage, pageSize]);
58
+ return filteredAgents.slice(startIndex, startIndex + pageSize);
59
+ }, [filteredAgents, currentPage, pageSize]);
60
+
61
+ // Reset to page 1 when filter or search changes
62
+ useMemo(() => {
63
+ setCurrentPage(1);
64
+ }, [statusFilter, searchQuery]);
26
65
 
27
- if (agents.length === 0) return <AssistantEmpty />;
66
+ if (agents.length === 0 && forkedAgents.length === 0) return <AssistantEmpty />;
28
67
 
29
- const showPagination = agents.length > pageSize;
68
+ const showPagination = filteredAgents.length > pageSize;
30
69
 
31
70
  return (
32
71
  <Flexbox gap={16}>
33
- <Flexbox align={'center'} gap={8} horizontal>
34
- <Text fontSize={16} weight={500}>
35
- {t('user.publishedAgents')}
36
- </Text>
37
- {agentCount > 0 && <Tag>{agentCount}</Tag>}
72
+ <Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
73
+ <Flexbox align={'center'} gap={8} horizontal>
74
+ <Text fontSize={16} weight={500}>
75
+ {t('user.publishedAgents')}
76
+ </Text>
77
+ {agentCount > 0 && <Tag>{filteredAgents.length}</Tag>}
78
+ </Flexbox>
79
+ {isOwner && (
80
+ <Flexbox align={'center'} gap={8} horizontal>
81
+ <Input.Search
82
+ allowClear
83
+ onChange={(e) => setSearchQuery(e.target.value)}
84
+ placeholder={t('user.searchPlaceholder')}
85
+ style={{ width: 200 }}
86
+ value={searchQuery}
87
+ />
88
+ <StatusFilter
89
+ onChange={(value) => setStatusFilter(value)}
90
+ value={statusFilter}
91
+ />
92
+ </Flexbox>
93
+ )}
38
94
  </Flexbox>
39
95
  <Grid rows={rows} width={'100%'}>
40
96
  {paginatedAgents.map((item, index) => (
@@ -48,7 +104,7 @@ const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 10 }) =>
48
104
  onChange={(page) => setCurrentPage(page)}
49
105
  pageSize={pageSize}
50
106
  showSizeChanger={false}
51
- total={agents.length}
107
+ total={filteredAgents.length}
52
108
  />
53
109
  </Flexbox>
54
110
  )}
@@ -3,25 +3,14 @@
3
3
  import { Flexbox } from '@lobehub/ui';
4
4
  import { memo } from 'react';
5
5
 
6
- import { useUserDetailContext } from './DetailProvider';
7
6
  import UserAgentList from './UserAgentList';
8
- import UserFavoriteAgents from './UserFavoriteAgents';
9
- import UserFavoritePlugins from './UserFavoritePlugins';
10
- import UserForkedAgentGroups from './UserForkedAgentGroups';
11
- import UserForkedAgents from './UserForkedAgents';
12
7
  import UserGroupList from './UserGroupList';
13
8
 
14
9
  const UserContent = memo(() => {
15
- const { forkedAgents, forkedAgentGroups } = useUserDetailContext();
16
-
17
10
  return (
18
11
  <Flexbox gap={32}>
19
12
  <UserAgentList />
20
13
  <UserGroupList />
21
- <UserForkedAgents agents={forkedAgents} />
22
- <UserForkedAgentGroups agentGroups={forkedAgentGroups} />
23
- <UserFavoriteAgents />
24
- <UserFavoritePlugins />
25
14
  </Flexbox>
26
15
  );
27
16
  });
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { Flexbox, Grid, Tag, Text } from '@lobehub/ui';
4
- import { Pagination } from 'antd';
4
+ import { Input, Pagination } from 'antd';
5
5
  import { memo, useMemo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import { useUserDetailContext } from './DetailProvider';
9
+ import StatusFilter, { type StatusFilterValue } from './StatusFilter';
9
10
  import UserGroupCard from './UserGroupCard';
10
11
 
11
12
  interface UserGroupListProps {
@@ -13,28 +14,81 @@ interface UserGroupListProps {
13
14
  rows?: number;
14
15
  }
15
16
 
16
- const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 10 }) => {
17
+ const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 8 }) => {
17
18
  const { t } = useTranslation('discover');
18
- const { agentGroups, groupCount } = useUserDetailContext();
19
+ const { agentGroups = [], groupCount, forkedAgentGroups = [], favoriteAgentGroups = [], isOwner } = useUserDetailContext();
19
20
  const [currentPage, setCurrentPage] = useState(1);
21
+ const [statusFilter, setStatusFilter] = useState<StatusFilterValue>('published');
22
+ const [searchQuery, setSearchQuery] = useState('');
23
+
24
+ // Combine groups and forked groups, then filter based on status and search
25
+ const filteredGroups = useMemo(() => {
26
+ let allGroups = [...agentGroups];
27
+
28
+ if (statusFilter === 'forked') {
29
+ // Show only forked groups (those with forkedFromAgentId)
30
+ allGroups = forkedAgentGroups;
31
+ } else if (statusFilter === 'favorite') {
32
+ // Show only favorited groups
33
+ allGroups = favoriteAgentGroups;
34
+ } else {
35
+ // Filter by status for non-forked groups
36
+ allGroups = allGroups.filter((group) => {
37
+ return group.status === statusFilter;
38
+ });
39
+ }
40
+
41
+ // Apply search filter
42
+ if (searchQuery.trim()) {
43
+ const query = searchQuery.toLowerCase();
44
+ allGroups = allGroups.filter((group) => {
45
+ const name = group?.title?.toLowerCase() || '';
46
+ const description = group?.description?.toLowerCase() || '';
47
+ return name.includes(query) || description.includes(query);
48
+ });
49
+ }
50
+
51
+ return allGroups;
52
+ }, [agentGroups, forkedAgentGroups, statusFilter, searchQuery]);
20
53
 
21
54
  const paginatedGroups = useMemo(() => {
22
- if (!agentGroups) return [];
23
55
  const startIndex = (currentPage - 1) * pageSize;
24
- return agentGroups.slice(startIndex, startIndex + pageSize);
25
- }, [agentGroups, currentPage, pageSize]);
56
+ return filteredGroups.slice(startIndex, startIndex + pageSize);
57
+ }, [filteredGroups, currentPage, pageSize]);
58
+
59
+ // Reset to page 1 when filter or search changes
60
+ useMemo(() => {
61
+ setCurrentPage(1);
62
+ }, [statusFilter, searchQuery]);
26
63
 
27
- if (!agentGroups || agentGroups.length === 0) return null;
64
+ if (agentGroups.length === 0 && forkedAgentGroups.length === 0) return null;
28
65
 
29
- const showPagination = agentGroups.length > pageSize;
66
+ const showPagination = filteredGroups.length > pageSize;
30
67
 
31
68
  return (
32
69
  <Flexbox gap={16}>
33
- <Flexbox align={'center'} gap={8} horizontal>
34
- <Text fontSize={16} weight={500}>
35
- {t('user.publishedGroups', { defaultValue: '创作的群组' })}
36
- </Text>
37
- {groupCount > 0 && <Tag>{groupCount}</Tag>}
70
+ <Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
71
+ <Flexbox align={'center'} gap={8} horizontal>
72
+ <Text fontSize={16} weight={500}>
73
+ {t('user.publishedGroups', { defaultValue: '创作的群组' })}
74
+ </Text>
75
+ {groupCount > 0 && <Tag>{filteredGroups.length}</Tag>}
76
+ </Flexbox>
77
+ {isOwner && (
78
+ <Flexbox align={'center'} gap={8} horizontal>
79
+ <Input.Search
80
+ allowClear
81
+ onChange={(e) => setSearchQuery(e.target.value)}
82
+ placeholder={t('user.searchPlaceholder')}
83
+ style={{ width: 200 }}
84
+ value={searchQuery}
85
+ />
86
+ <StatusFilter
87
+ onChange={(value) => setStatusFilter(value)}
88
+ value={statusFilter}
89
+ />
90
+ </Flexbox>
91
+ )}
38
92
  </Flexbox>
39
93
  <Grid rows={rows} width={'100%'}>
40
94
  {paginatedGroups.map((item, index) => (
@@ -48,7 +102,7 @@ const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 10 }) =>
48
102
  onChange={(page) => setCurrentPage(page)}
49
103
  pageSize={pageSize}
50
104
  showSizeChanger={false}
51
- total={agentGroups.length}
105
+ total={filteredGroups.length}
52
106
  />
53
107
  </Flexbox>
54
108
  )}
@@ -61,12 +61,14 @@ const UserDetailPage = memo<UserDetailPageProps>(({ mobile }) => {
61
61
 
62
62
  const contextConfig = useMemo(() => {
63
63
  if (!data || !data.user) return null;
64
- const { user, agents, agentGroups, forkedAgents, forkedAgentGroups } = data;
64
+ const { user, agents, agentGroups, forkedAgents, forkedAgentGroups, favoriteAgents, favoriteAgentGroups } = data;
65
65
  const totalInstalls = agents.reduce((sum, agent) => sum + (agent.installCount || 0), 0);
66
66
  return {
67
67
  agentCount: agents.length,
68
68
  agentGroups: agentGroups || [],
69
69
  agents,
70
+ favoriteAgentGroups: favoriteAgentGroups || [],
71
+ favoriteAgents: favoriteAgents || [],
70
72
  forkedAgentGroups: forkedAgentGroups || [],
71
73
  forkedAgents: forkedAgents || [],
72
74
  groupCount: agentGroups?.length || 0,
@@ -56,6 +56,7 @@ const styles = createStaticStyles(({ css, cssVar }) => {
56
56
  const AssistantItem = memo<DiscoverAssistantItem>(
57
57
  ({
58
58
  createdAt,
59
+ updatedAt,
59
60
  author,
60
61
  avatar,
61
62
  title,
@@ -225,7 +226,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
225
226
  <Icon icon={ClockIcon} size={14} />
226
227
  <PublishedTime
227
228
  className={styles.secondaryDesc}
228
- date={createdAt}
229
+ date={updatedAt || createdAt}
229
230
  template={'MMM DD, YYYY'}
230
231
  />
231
232
  </Flexbox>
@@ -1,11 +1,15 @@
1
1
  import { Button } from '@lobehub/ui';
2
2
  import { ShapesUploadIcon } from '@lobehub/ui/icons';
3
+ import { Popconfirm } from 'antd';
4
+ import isEqual from 'fast-deep-equal';
3
5
  import { memo, useCallback, useMemo, useState } from 'react';
4
6
  import { useTranslation } from 'react-i18next';
5
7
 
6
8
  import { message } from '@/components/AntdStaticMethods';
7
9
  import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
8
10
  import { resolveMarketAuthError } from '@/layout/AuthProvider/MarketAuth/errors';
11
+ import { useAgentGroupStore } from '@/store/agentGroup';
12
+ import { agentGroupSelectors } from '@/store/agentGroup/selectors';
9
13
 
10
14
  import GroupForkConfirmModal from './GroupForkConfirmModal';
11
15
  import type { MarketPublishAction, OriginalGroupInfo } from './types';
@@ -25,10 +29,17 @@ const PublishButton = memo<GroupPublishButtonProps>(({ action, onPublishSuccess
25
29
  onSuccess: onPublishSuccess,
26
30
  });
27
31
 
32
+ // Group data for validation
33
+ const currentGroupMeta = useAgentGroupStore(agentGroupSelectors.currentGroupMeta, isEqual);
34
+ const currentGroup = useAgentGroupStore(agentGroupSelectors.currentGroup);
35
+
28
36
  // Fork confirmation modal state
29
37
  const [showForkModal, setShowForkModal] = useState(false);
30
38
  const [originalGroupInfo, setOriginalGroupInfo] = useState<OriginalGroupInfo | null>(null);
31
39
 
40
+ // Publish confirmation popconfirm state
41
+ const [confirmOpened, setConfirmOpened] = useState(false);
42
+
32
43
  const buttonConfig = useMemo(() => {
33
44
  if (action === 'upload') {
34
45
  return {
@@ -60,7 +71,25 @@ const PublishButton = memo<GroupPublishButtonProps>(({ action, onPublishSuccess
60
71
  await publish();
61
72
  }, [checkOwnership, publish]);
62
73
 
63
- const handleButtonClick = useCallback(async () => {
74
+ const handleButtonClick = useCallback(() => {
75
+ // Validate name and systemRole (stored in content)
76
+ if (!currentGroupMeta?.title || currentGroupMeta.title.trim() === '') {
77
+ message.error({ content: t('marketPublish.validation.emptyName') });
78
+ return;
79
+ }
80
+
81
+ if (!currentGroup?.content || currentGroup.content.trim() === '') {
82
+ message.error({ content: t('marketPublish.validation.emptySystemRole') });
83
+ return;
84
+ }
85
+
86
+ // Open popconfirm for user confirmation
87
+ setConfirmOpened(true);
88
+ }, [currentGroupMeta?.title, currentGroup?.content, t]);
89
+
90
+ const handleConfirmPublish = useCallback(async () => {
91
+ setConfirmOpened(false);
92
+
64
93
  if (!isAuthenticated) {
65
94
  try {
66
95
  await signIn();
@@ -98,14 +127,29 @@ const PublishButton = memo<GroupPublishButtonProps>(({ action, onPublishSuccess
98
127
 
99
128
  return (
100
129
  <>
101
- <Button
102
- icon={ShapesUploadIcon}
103
- loading={loading}
104
- onClick={handleButtonClick}
105
- title={buttonTitle}
130
+ <Popconfirm
131
+ arrow={false}
132
+ okButtonProps={{ type: 'primary' }}
133
+ onCancel={() => setConfirmOpened(false)}
134
+ onConfirm={handleConfirmPublish}
135
+ onOpenChange={(open) => {
136
+ if (!open) {
137
+ setConfirmOpened(false);
138
+ }
139
+ }}
140
+ open={confirmOpened}
141
+ placement="bottomRight"
142
+ title={t('marketPublish.validation.confirmPublish')}
106
143
  >
107
- {t('publishToCommunity')}
108
- </Button>
144
+ <Button
145
+ icon={ShapesUploadIcon}
146
+ loading={loading}
147
+ onClick={handleButtonClick}
148
+ title={buttonTitle}
149
+ >
150
+ {t('publishToCommunity')}
151
+ </Button>
152
+ </Popconfirm>
109
153
  <GroupForkConfirmModal
110
154
  loading={isPublishing}
111
155
  onCancel={handleForkCancel}
@@ -891,6 +891,18 @@ export default {
891
891
 
892
892
  'user.publishedAgents': 'Created Agents',
893
893
 
894
+ 'user.publishedGroups': 'Created Groups',
895
+
896
+ 'user.searchPlaceholder': 'Search by name or description...',
897
+
898
+ 'user.statusFilter.all': 'All',
899
+ 'user.statusFilter.archived': 'Archived',
900
+ 'user.statusFilter.deprecated': 'Deprecated',
901
+ 'user.statusFilter.favorite': 'Favorite',
902
+ 'user.statusFilter.forked': 'Forked',
903
+ 'user.statusFilter.published': 'Published',
904
+ 'user.statusFilter.unpublished': 'Under Review',
905
+
894
906
  'user.tabs.favorites': 'Favorites',
895
907
 
896
908
  'user.tabs.forkedAgents': 'Forked',
@@ -294,6 +294,9 @@ export default {
294
294
  'marketPublish.upload.button': 'Publish New Version',
295
295
  'marketPublish.upload.tooltip': 'Publish a new version to Agent Community',
296
296
  'marketPublish.uploadGroup.tooltip': 'Publish a new version to Group Community',
297
+ 'marketPublish.validation.confirmPublish': 'Are you sure you want to publish to the market?',
298
+ 'marketPublish.validation.emptyName': 'Cannot publish: Name is required',
299
+ 'marketPublish.validation.emptySystemRole': 'Cannot publish: System Role is required',
297
300
  'memory.enabled.desc':
298
301
  'Allow LobeHub to extract preferences and info from conversations and use them later. You can view, edit, or clear memory anytime.',
299
302
  'memory.enabled.title': 'Enable Memory',
@@ -17,7 +17,7 @@ const socialPublicProcedure = publicProcedure
17
17
  .use(marketSDK);
18
18
 
19
19
  // Schema definitions
20
- const targetTypeSchema = z.enum(['agent', 'plugin']);
20
+ const targetTypeSchema = z.enum(['agent', 'plugin', 'agent-group']);
21
21
 
22
22
  const paginationSchema = z.object({
23
23
  limit: z.number().optional(),
@@ -745,6 +745,7 @@ export class DiscoverService {
745
745
  title: item.name || item.identifier,
746
746
  tokenUsage: item.tokenUsage || 0,
747
747
  type: item.type,
748
+ updatedAt: item.updatedAt,
748
749
  userName: normalizedAuthor.userName,
749
750
  };
750
751
  });
@@ -1710,6 +1711,8 @@ export class DiscoverService {
1710
1711
  locale,
1711
1712
  })) as UserInfoResponse & {
1712
1713
  agentGroups?: any[];
1714
+ favoriteAgentGroups?: any[];
1715
+ favoriteAgents?: any[];
1713
1716
  forkedAgentGroups?: any[];
1714
1717
  forkedAgents?: any[];
1715
1718
  };
@@ -1719,7 +1722,7 @@ export class DiscoverService {
1719
1722
  return undefined;
1720
1723
  }
1721
1724
 
1722
- const { user, agents, agentGroups, forkedAgents, forkedAgentGroups } = response;
1725
+ const { user, agents, agentGroups, forkedAgents, forkedAgentGroups, favoriteAgents, favoriteAgentGroups } = response;
1723
1726
 
1724
1727
  // Transform agents to DiscoverAssistantItem format
1725
1728
  const transformedAgents: DiscoverAssistantItem[] = (agents || []).map((agent: any) => ({
@@ -1811,9 +1814,59 @@ export class DiscoverService {
1811
1814
  updatedAt: group.updatedAt,
1812
1815
  }));
1813
1816
 
1817
+ // Transform favoriteAgents to DiscoverAssistantItem format
1818
+ const transformedFavoriteAgents: DiscoverAssistantItem[] = (favoriteAgents || []).map(
1819
+ (agent: any) => ({
1820
+ author: agent.author || '',
1821
+ avatar: agent.avatar || '',
1822
+ category: agent.category as any,
1823
+ config: {} as any,
1824
+ createdAt: agent.createdAt,
1825
+ description: agent.description || '',
1826
+ forkCount: agent.forkCount || 0,
1827
+ forkedFromAgentId: agent.forkedFromAgentId || null,
1828
+ homepage: `https://lobehub.com/discover/assistant/${agent.identifier}`,
1829
+ identifier: agent.identifier,
1830
+ installCount: agent.installCount,
1831
+ isValidated: agent.isValidated,
1832
+ knowledgeCount: agent.knowledgeCount || 0,
1833
+ pluginCount: agent.pluginCount || 0,
1834
+ schemaVersion: 1,
1835
+ status: agent.status,
1836
+ tags: agent.tags || [],
1837
+ title: agent.name || agent.identifier,
1838
+ tokenUsage: agent.tokenUsage || 0,
1839
+ }),
1840
+ );
1841
+
1842
+ // Transform favoriteAgentGroups to DiscoverGroupAgentItem format
1843
+ const transformedFavoriteAgentGroups = (favoriteAgentGroups || []).map((group: any) => ({
1844
+ author: group.author || '',
1845
+ avatar: group.avatar || '👥',
1846
+ category: group.category as any,
1847
+ createdAt: group.createdAt,
1848
+ description: group.description || '',
1849
+ forkCount: group.forkCount || 0,
1850
+ forkedFromGroupId: group.forkedFromGroupId || null,
1851
+ homepage: `https://lobehub.com/discover/group_agent/${group.identifier}`,
1852
+ identifier: group.identifier,
1853
+ installCount: group.installCount || 0,
1854
+ isFeatured: group.isFeatured || false,
1855
+ isOfficial: group.isOfficial || false,
1856
+ isValidated: group.isValidated,
1857
+ memberCount: 0, // Will be populated from memberAgents in detail view
1858
+ schemaVersion: 1,
1859
+ status: group.status,
1860
+ tags: group.tags || [],
1861
+ title: group.name || group.identifier,
1862
+ updatedAt: group.updatedAt,
1863
+ }));
1864
+
1814
1865
  const result: DiscoverUserProfile = {
1815
1866
  agentGroups: transformedAgentGroups,
1816
1867
  agents: transformedAgents,
1868
+ favoriteAgentGroups: transformedFavoriteAgentGroups,
1869
+ favoriteAgents: transformedFavoriteAgents,
1817
1870
  forkedAgentGroups: transformedForkedAgentGroups,
1818
1871
  forkedAgents: transformedForkedAgents,
1819
1872
  user: {
@@ -1833,11 +1886,13 @@ export class DiscoverService {
1833
1886
  };
1834
1887
 
1835
1888
  log(
1836
- 'getUserInfo: returning user profile with %d agents, %d groups, %d forked agents, %d forked groups',
1889
+ 'getUserInfo: returning user profile with %d agents, %d groups, %d forked agents, %d forked groups, %d favorite agents, %d favorite groups',
1837
1890
  result.agents.length,
1838
1891
  result.agentGroups?.length || 0,
1839
1892
  result.forkedAgents?.length || 0,
1840
1893
  result.forkedAgentGroups?.length || 0,
1894
+ result.favoriteAgents?.length || 0,
1895
+ result.favoriteAgentGroups?.length || 0,
1841
1896
  );
1842
1897
  return result;
1843
1898
  } catch (error) {
@@ -75,7 +75,6 @@ export class ModelsService {
75
75
  const runtimeProvider = resolveRuntimeProvider(provider);
76
76
  const enableFetchOnClient = isEnableFetchOnClient(provider);
77
77
 
78
- console.log('enableFetchOnClient:', enableFetchOnClient);
79
78
  let res: Response;
80
79
  if (enableFetchOnClient) {
81
80
  const agentRuntime = await initializeWithClientStore({
@@ -1,6 +1,6 @@
1
1
  import { lambdaClient } from '@/libs/trpc/client';
2
2
 
3
- export type SocialTargetType = 'agent' | 'plugin';
3
+ export type SocialTargetType = 'agent' | 'plugin' | 'agent-group';
4
4
 
5
5
  export interface FollowStatus {
6
6
  isFollowing: boolean;