mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat: add agent group publish into market & use market group agents in lobehub (#11535)
* feat: add the publihs group button into group profiles * feat: add agent group detail page * feat: the /community/group_agent pages inital * feat: upload the agent group detail get fixed * feat: add the agent group add it to user way * feat: add agent group in agents list & item update * feat: update the market-sdk * feat: add group active tab as overview default
This commit is contained in:
@@ -204,7 +204,7 @@
|
||||
"@lobehub/desktop-ipc-typings": "workspace:*",
|
||||
"@lobehub/editor": "^3.11.0",
|
||||
"@lobehub/icons": "^4.0.2",
|
||||
"@lobehub/market-sdk": "0.28.1",
|
||||
"@lobehub/market-sdk": "0.29.0",
|
||||
"@lobehub/tts": "^4.0.2",
|
||||
"@lobehub/ui": "^4.19.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@lobechat/python-interpreter": "workspace:*",
|
||||
"@lobechat/web-crawler": "workspace:*",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@lobehub/market-sdk": "0.28.1",
|
||||
"@lobehub/market-sdk": "0.29.0",
|
||||
"@lobehub/market-types": "^1.12.3",
|
||||
"model-bank": "workspace:*",
|
||||
"type-fest": "^4.41.0",
|
||||
|
||||
@@ -42,6 +42,8 @@ export enum AssistantNavKey {
|
||||
|
||||
export type AgentStatus = 'published' | 'unpublished' | 'archived' | 'deprecated';
|
||||
|
||||
export type AgentType = 'agent' | 'agent-group';
|
||||
|
||||
export interface DiscoverAssistantItem extends Omit<LobeAgentSettings, 'meta'>, MetaData {
|
||||
author: string;
|
||||
category?: AssistantCategory;
|
||||
@@ -53,6 +55,7 @@ export interface DiscoverAssistantItem extends Omit<LobeAgentSettings, 'meta'>,
|
||||
pluginCount: number;
|
||||
status?: AgentStatus;
|
||||
tokenUsage: number;
|
||||
type?: AgentType;
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
@@ -60,6 +63,7 @@ export type AssistantMarketSource = 'legacy' | 'new';
|
||||
|
||||
export interface AssistantQueryParams {
|
||||
category?: string;
|
||||
includeAgentGroup?: boolean;
|
||||
locale?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
ownerId?: string;
|
||||
|
||||
196
packages/types/src/discover/groupAgents.ts
Normal file
196
packages/types/src/discover/groupAgents.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type { MetaData } from '../meta';
|
||||
|
||||
/**
|
||||
* Group Agent Member - represents a member agent in a group
|
||||
*/
|
||||
export interface GroupAgentMember {
|
||||
avatar?: string;
|
||||
category?: string;
|
||||
config?: Record<string, any>;
|
||||
description: string;
|
||||
displayOrder?: number;
|
||||
enabled?: boolean;
|
||||
identifier: string;
|
||||
name: string;
|
||||
role: 'supervisor' | 'participant';
|
||||
url: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent Status
|
||||
*/
|
||||
export type GroupAgentStatus = 'published' | 'unpublished' | 'archived' | 'deprecated';
|
||||
|
||||
/**
|
||||
* Group Agent Visibility
|
||||
*/
|
||||
export type GroupAgentVisibility = 'public' | 'private' | 'internal';
|
||||
|
||||
/**
|
||||
* Group Agent Category
|
||||
*/
|
||||
export type GroupAgentCategory =
|
||||
| 'productivity'
|
||||
| 'entertainment'
|
||||
| 'education'
|
||||
| 'development'
|
||||
| 'business'
|
||||
| 'other';
|
||||
|
||||
/**
|
||||
* Group Agent Config - similar to LobeAgentConfig but for groups
|
||||
*/
|
||||
export interface GroupAgentConfig {
|
||||
/**
|
||||
* Opening message when starting a conversation with the group
|
||||
*/
|
||||
openingMessage?: string;
|
||||
/**
|
||||
* Opening questions to guide users
|
||||
*/
|
||||
openingQuestions?: string[];
|
||||
/**
|
||||
* System role/prompt for the group
|
||||
*/
|
||||
systemRole?: string;
|
||||
/**
|
||||
* Additional configuration
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent Item - basic info for list display
|
||||
*/
|
||||
export interface DiscoverGroupAgentItem extends MetaData {
|
||||
author?: string;
|
||||
avatar?: string;
|
||||
backgroundColor?: string;
|
||||
category?: GroupAgentCategory;
|
||||
config?: GroupAgentConfig;
|
||||
createdAt: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
identifier: string;
|
||||
installCount?: number;
|
||||
isFeatured?: boolean;
|
||||
isOfficial?: boolean;
|
||||
/**
|
||||
* Number of knowledge bases across all member agents
|
||||
*/
|
||||
knowledgeCount?: number;
|
||||
/**
|
||||
* Number of member agents in the group
|
||||
*/
|
||||
memberCount: number;
|
||||
/**
|
||||
* Number of plugins across all member agents
|
||||
*/
|
||||
pluginCount?: number;
|
||||
status?: GroupAgentStatus;
|
||||
tags?: string[];
|
||||
title: string;
|
||||
/**
|
||||
* Estimated token usage for the group
|
||||
*/
|
||||
tokenUsage?: number;
|
||||
updatedAt: string;
|
||||
userName?: string;
|
||||
version?: string;
|
||||
versionNumber?: number;
|
||||
visibility?: GroupAgentVisibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent Version
|
||||
*/
|
||||
export interface DiscoverGroupAgentVersion {
|
||||
changelog?: string;
|
||||
createdAt?: string;
|
||||
isLatest?: boolean;
|
||||
isValidated?: boolean;
|
||||
status?: GroupAgentStatus;
|
||||
version: string;
|
||||
versionNumber: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent Detail - complete info for detail page
|
||||
*/
|
||||
export interface DiscoverGroupAgentDetail extends DiscoverGroupAgentItem {
|
||||
/**
|
||||
* Current version string
|
||||
*/
|
||||
currentVersion?: string;
|
||||
/**
|
||||
* Current version number
|
||||
*/
|
||||
currentVersionNumber?: number;
|
||||
/**
|
||||
* Example conversations (if available from config)
|
||||
*/
|
||||
examples?: any;
|
||||
/**
|
||||
* Member agents in the group
|
||||
*/
|
||||
memberAgents: GroupAgentMember[];
|
||||
/**
|
||||
* Owner ID
|
||||
*/
|
||||
ownerId?: string;
|
||||
/**
|
||||
* Related group agents
|
||||
*/
|
||||
related?: DiscoverGroupAgentItem[];
|
||||
/**
|
||||
* Summary text (extracted from description or config)
|
||||
*/
|
||||
summary?: string;
|
||||
/**
|
||||
* Version history
|
||||
*/
|
||||
versions?: DiscoverGroupAgentVersion[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent List Response
|
||||
*/
|
||||
export interface GroupAgentListResponse {
|
||||
currentPage: number;
|
||||
items: DiscoverGroupAgentItem[];
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent Query Parameters
|
||||
*/
|
||||
export interface GroupAgentQueryParams {
|
||||
category?: string;
|
||||
locale?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
ownerId?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
q?: string;
|
||||
sort?: 'createdAt' | 'updatedAt' | 'name' | 'recommended';
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent Detail Query Parameters
|
||||
*/
|
||||
export interface GroupAgentDetailParams {
|
||||
identifier: string;
|
||||
locale?: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Agent Category Item
|
||||
*/
|
||||
export interface GroupAgentCategoryItem {
|
||||
category: GroupAgentCategory;
|
||||
count: number;
|
||||
name: string;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DiscoverAssistantItem } from './assistants';
|
||||
import { DiscoverGroupAgentItem } from './groupAgents';
|
||||
|
||||
export * from './assistants';
|
||||
export * from './groupAgents';
|
||||
export * from './mcp';
|
||||
export * from './models';
|
||||
export * from './plugins';
|
||||
@@ -8,6 +10,7 @@ export * from './providers';
|
||||
|
||||
export enum DiscoverTab {
|
||||
Assistants = 'assistant',
|
||||
GroupAgents = 'group_agent',
|
||||
Home = 'home',
|
||||
Mcp = 'mcp',
|
||||
Models = 'model',
|
||||
@@ -58,9 +61,10 @@ export interface DiscoverUserInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* User profile with their published agents
|
||||
* User profile with their published agents and groups
|
||||
*/
|
||||
export interface DiscoverUserProfile {
|
||||
agentGroups?: DiscoverGroupAgentItem[];
|
||||
agents: DiscoverAssistantItem[];
|
||||
user: DiscoverUserInfo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { type ReactNode, createContext, memo, use } from 'react';
|
||||
|
||||
import { type DiscoverGroupAgentDetail } from '@/types/discover';
|
||||
|
||||
export type DetailContextConfig = Partial<DiscoverGroupAgentDetail>;
|
||||
|
||||
export const DetailContext = createContext<DetailContextConfig>({});
|
||||
|
||||
export const DetailProvider = memo<{ children: ReactNode; config?: DetailContextConfig }>(
|
||||
({ children, config = {} }) => {
|
||||
return <DetailContext value={config}>{children}</DetailContext>;
|
||||
},
|
||||
);
|
||||
|
||||
export const useDetailContext = () => {
|
||||
return use(DetailContext);
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Avatar, Flexbox, Tag } from '@lobehub/ui';
|
||||
import { Card, Typography } from 'antd';
|
||||
import { Crown, User } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const MemberCard = memo(
|
||||
({
|
||||
agent,
|
||||
currentVersion,
|
||||
}: {
|
||||
agent: any;
|
||||
currentVersion: any;
|
||||
}) => {
|
||||
const { t } = useTranslation('discover');
|
||||
const isSupervisor = agent.role === 'supervisor';
|
||||
|
||||
return (
|
||||
<Card hoverable>
|
||||
<Flexbox gap={12}>
|
||||
{/* Avatar and Basic Info */}
|
||||
<Flexbox align="center" gap={12} horizontal>
|
||||
<Avatar avatar={currentVersion.avatar || agent.name[0]} size={48} />
|
||||
<Flexbox flex={1} gap={4}>
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
{currentVersion.name || agent.name}
|
||||
</Title>
|
||||
{isSupervisor ? (
|
||||
<Tag color="gold" icon={<Crown size={12} />}>
|
||||
{t('members.supervisor', { defaultValue: 'Supervisor' })}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="blue" icon={<User size={12} />}>
|
||||
{t('members.participant', { defaultValue: 'Participant' })}
|
||||
</Tag>
|
||||
)}
|
||||
</Flexbox>
|
||||
<Text type="secondary">{agent.identifier}</Text>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
|
||||
{/* Description */}
|
||||
{currentVersion.description && (
|
||||
<Paragraph ellipsis={{ rows: 2 }} style={{ margin: 0 }} type="secondary">
|
||||
{currentVersion.description}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{/* System Role (if available) */}
|
||||
{currentVersion.config?.systemRole && (
|
||||
<Flexbox gap={4}>
|
||||
<Text strong>{t('members.systemRole', { defaultValue: 'System Role' })}:</Text>
|
||||
<Paragraph ellipsis={{ rows: 3 }} style={{ margin: 0 }} type="secondary">
|
||||
{currentVersion.config.systemRole}
|
||||
</Paragraph>
|
||||
</Flexbox>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<Flexbox gap={8} horizontal wrap="wrap">
|
||||
{currentVersion.version && (
|
||||
<Text type="secondary">
|
||||
{t('members.version', { defaultValue: 'Version' })}: {currentVersion.version}
|
||||
</Text>
|
||||
)}
|
||||
{currentVersion.tokenUsage !== undefined && (
|
||||
<Text type="secondary">
|
||||
{t('members.tokenUsage', { defaultValue: 'Token Usage' })}:{' '}
|
||||
{currentVersion.tokenUsage}
|
||||
</Text>
|
||||
)}
|
||||
</Flexbox>
|
||||
|
||||
{/* URL */}
|
||||
{currentVersion.url && (
|
||||
<Text
|
||||
copyable={{ text: currentVersion.url }}
|
||||
ellipsis
|
||||
style={{ fontSize: 12 }}
|
||||
type="secondary"
|
||||
>
|
||||
{currentVersion.url}
|
||||
</Text>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MemberCard.displayName = 'MemberCard';
|
||||
|
||||
const Members = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { memberAgents = [] } = useDetailContext();
|
||||
|
||||
// Sort: supervisors first, then by displayOrder
|
||||
const sortedMembers = [...(memberAgents || [])].sort((a: any, b: any) => {
|
||||
const aRole = a.role || a.agent?.role;
|
||||
const bRole = b.role || b.agent?.role;
|
||||
if (aRole === 'supervisor' && bRole !== 'supervisor') return -1;
|
||||
if (aRole !== 'supervisor' && bRole === 'supervisor') return 1;
|
||||
const aOrder = a.displayOrder || a.agent?.displayOrder || 0;
|
||||
const bOrder = b.displayOrder || b.agent?.displayOrder || 0;
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Title level={4}>
|
||||
{t('members.title', { defaultValue: 'Member Agents' })} ({memberAgents?.length || 0})
|
||||
</Title>
|
||||
|
||||
<Flexbox gap={12}>
|
||||
{sortedMembers.map((member: any, index) => {
|
||||
// Support both flat structure and nested structure
|
||||
const agent = member.agent || member;
|
||||
const currentVersion = member.currentVersion || member;
|
||||
return (
|
||||
<MemberCard
|
||||
agent={agent}
|
||||
currentVersion={currentVersion}
|
||||
key={agent.identifier || index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Members;
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { SOCIAL_URL } from '@lobechat/business-const';
|
||||
import { Flexbox, Icon, Tabs, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { BookOpenIcon, HistoryIcon, LayersIcon, ListIcon, SquareUserIcon, UsersIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDetailContext } from '../DetailProvider';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
link: css`
|
||||
color: ${cssVar.colorTextDescription};
|
||||
|
||||
&:hover {
|
||||
color: ${cssVar.colorInfo};
|
||||
}
|
||||
`,
|
||||
nav: css`
|
||||
border-block-end: 1px solid ${cssVar.colorBorder};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export enum GroupAgentNavKey {
|
||||
Overview = 'overview',
|
||||
SystemRole = 'systemRole',
|
||||
Versions = 'versions',
|
||||
}
|
||||
|
||||
interface NavProps {
|
||||
activeTab?: GroupAgentNavKey;
|
||||
mobile?: boolean;
|
||||
setActiveTab?: (tab: GroupAgentNavKey) => void;
|
||||
}
|
||||
|
||||
const Nav = memo<NavProps>(({ mobile, setActiveTab, activeTab = GroupAgentNavKey.Overview }) => {
|
||||
const { t } = useTranslation('discover');
|
||||
|
||||
const nav = (
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
compact={mobile}
|
||||
items={[
|
||||
{
|
||||
icon: <Icon icon={BookOpenIcon} size={16} />,
|
||||
key: GroupAgentNavKey.Overview,
|
||||
label: t('groupAgents.details.overview.title', { defaultValue: 'Overview' }),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={SquareUserIcon} size={16} />,
|
||||
key: GroupAgentNavKey.SystemRole,
|
||||
label: t('groupAgents.details.systemRole.title', { defaultValue: 'System Role' }),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={HistoryIcon} size={16} />,
|
||||
key: GroupAgentNavKey.Versions,
|
||||
label: t('groupAgents.details.versions.title', { defaultValue: 'Versions' }),
|
||||
},
|
||||
]}
|
||||
onChange={(key) => setActiveTab?.(key as GroupAgentNavKey)}
|
||||
/>
|
||||
);
|
||||
|
||||
return mobile ? (
|
||||
nav
|
||||
) : (
|
||||
<Flexbox align={'center'} className={styles.nav} horizontal justify={'space-between'}>
|
||||
{nav}
|
||||
<Flexbox gap={12} horizontal>
|
||||
<a className={styles.link} href={SOCIAL_URL.discord} rel="noreferrer" target="_blank">
|
||||
{t('groupAgents.details.nav.needHelp', { defaultValue: 'Need help?' })}
|
||||
</a>
|
||||
<a
|
||||
className={styles.link}
|
||||
href="https://github.com/lobehub/lobe-chat/issues/new/choose"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('groupAgents.details.nav.reportIssue', { defaultValue: 'Report issue' })}
|
||||
</a>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Nav;
|
||||
@@ -0,0 +1,213 @@
|
||||
import { BRANDING_NAME } from '@lobechat/business-const';
|
||||
import { Avatar, Block, Collapse, Flexbox, Grid, Text } from '@lobehub/ui';
|
||||
import { ChatList } from '@lobehub/ui/chat';
|
||||
import { createStaticStyles, useTheme } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
|
||||
|
||||
import Title from '../../../../../features/Title';
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
desc: css`
|
||||
flex: 1;
|
||||
margin: 0 !important;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
title: css`
|
||||
margin: 0 !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const MemberCard = memo(
|
||||
({
|
||||
agent,
|
||||
currentVersion,
|
||||
}: {
|
||||
agent: any;
|
||||
currentVersion: any;
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
height={'100%'}
|
||||
style={{
|
||||
cursor: 'default',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
variant={'outlined'}
|
||||
width={'100%'}
|
||||
>
|
||||
<Flexbox gap={12} padding={16}>
|
||||
{/* Avatar and Basic Info */}
|
||||
<Flexbox align={'flex-start'} gap={12} horizontal>
|
||||
<Avatar
|
||||
avatar={currentVersion.avatar || agent.name?.[0]}
|
||||
shape={'square'}
|
||||
size={40}
|
||||
style={{ flex: 'none' }}
|
||||
/>
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={4}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Text as={'h3'} className={styles.title} ellipsis>
|
||||
{currentVersion.name || agent.name}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
|
||||
{/* Description */}
|
||||
{currentVersion.description && currentVersion.description !== 'No description provided' && (
|
||||
<Text
|
||||
as={'p'}
|
||||
className={styles.desc}
|
||||
ellipsis={{
|
||||
rows: 2,
|
||||
}}
|
||||
>
|
||||
{currentVersion.description}
|
||||
</Text>
|
||||
)}
|
||||
</Flexbox>
|
||||
</Block>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MemberCard.displayName = 'MemberCard';
|
||||
|
||||
const Overview = memo(() => {
|
||||
const [userAvatar, username] = useUserStore((s) => [
|
||||
userProfileSelectors.userAvatar(s),
|
||||
userProfileSelectors.username(s),
|
||||
]);
|
||||
|
||||
const isSignedIn = useUserStore(authSelectors.isLogin);
|
||||
const { t } = useTranslation('discover');
|
||||
const theme = useTheme();
|
||||
const {
|
||||
examples = [],
|
||||
description,
|
||||
summary,
|
||||
avatar,
|
||||
title,
|
||||
backgroundColor,
|
||||
config,
|
||||
memberAgents = [],
|
||||
} = useDetailContext();
|
||||
|
||||
const data: any = [
|
||||
{
|
||||
content: config?.openingMessage,
|
||||
role: 'assistant',
|
||||
},
|
||||
...examples,
|
||||
].map((item, index) => {
|
||||
let meta = {
|
||||
avatar,
|
||||
backgroundColor: backgroundColor || 'transparent',
|
||||
title,
|
||||
};
|
||||
if (item.role === 'user') {
|
||||
meta = {
|
||||
avatar: isSignedIn && !!userAvatar ? userAvatar : DEFAULT_USER_AVATAR_URL,
|
||||
backgroundColor: 'transparent',
|
||||
title: isSignedIn && !!username ? username : BRANDING_NAME,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
id: index,
|
||||
...item,
|
||||
meta,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort: supervisors first, then by displayOrder
|
||||
const sortedMembers = [...(memberAgents || [])].sort((a: any, b: any) => {
|
||||
const aRole = a.role || a.agent?.role;
|
||||
const bRole = b.role || b.agent?.role;
|
||||
if (aRole === 'supervisor' && bRole !== 'supervisor') return -1;
|
||||
if (aRole !== 'supervisor' && bRole === 'supervisor') return 1;
|
||||
const aOrder = a.displayOrder || a.agent?.displayOrder || 0;
|
||||
const bOrder = b.displayOrder || b.agent?.displayOrder || 0;
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Collapse
|
||||
defaultActiveKey={['summary']}
|
||||
expandIconPlacement={'end'}
|
||||
items={[
|
||||
{
|
||||
children: summary || description,
|
||||
key: 'summary',
|
||||
label: t('groupAgents.details.summary.title', { defaultValue: 'Summary' }),
|
||||
},
|
||||
]}
|
||||
variant={'outlined'}
|
||||
/>
|
||||
|
||||
{/* Members Section */}
|
||||
{memberAgents.length > 0 && (
|
||||
<>
|
||||
<Title>
|
||||
{t('groupAgents.details.members.title', { defaultValue: 'Member Agents' })} (
|
||||
{memberAgents.length})
|
||||
</Title>
|
||||
<Grid rows={4} width={'100%'}>
|
||||
{sortedMembers.map((member: any, index) => {
|
||||
// Support both flat structure and nested structure
|
||||
const agent = member.agent || member;
|
||||
const currentVersion = member.currentVersion || member;
|
||||
return (
|
||||
<MemberCard
|
||||
agent={agent}
|
||||
currentVersion={currentVersion}
|
||||
key={agent.identifier || index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{data.length > 0 && config?.openingMessage && (
|
||||
<>
|
||||
<Title>
|
||||
{t('groupAgents.details.overview.example', { defaultValue: 'Conversation Example' })}
|
||||
</Title>
|
||||
<Block
|
||||
style={{
|
||||
background: theme.colorBgContainerSecondary,
|
||||
}}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<ChatList
|
||||
data={data}
|
||||
renderMessages={{
|
||||
default: ({ id, editableContent }) => <div id={id}>{editableContent}</div>,
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Block>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Overview;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Avatar, Flexbox, Text } from '@lobehub/ui';
|
||||
import { Grid } from '@lobehub/ui';
|
||||
import qs from 'query-string';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { type DiscoverGroupAgentItem } from '@/types/discover';
|
||||
|
||||
import Title from '../../../../../features/Title';
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const GroupAgentCard = memo<DiscoverGroupAgentItem>((item) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (!item.identifier) return;
|
||||
navigate(qs.stringifyUrl({
|
||||
url: urlJoin('/community/group_agent', item.identifier),
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
gap={12}
|
||||
onClick={handleClick}
|
||||
padding={16}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
border: '1px solid var(--lobe-border-color)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<Flexbox align="center" gap={12} horizontal>
|
||||
<Avatar avatar={item.avatar || item.title[0]} shape="square" size={48} />
|
||||
<Flexbox flex={1} gap={4}>
|
||||
<Text ellipsis style={{ fontWeight: 500 }}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text ellipsis style={{ fontSize: 12, opacity: 0.65 }} type="secondary">
|
||||
{item.description}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
const Related = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { related = [], category } = useDetailContext();
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Title
|
||||
more={t('groupAgents.details.related.more', { defaultValue: 'View More' })}
|
||||
moreLink={qs.stringifyUrl(
|
||||
{
|
||||
query: {
|
||||
category,
|
||||
},
|
||||
url: '/community/group_agent',
|
||||
},
|
||||
{ skipNull: true },
|
||||
)}
|
||||
>
|
||||
{t('groupAgents.details.related.listTitle', { defaultValue: 'Related Group Agents' })}
|
||||
</Title>
|
||||
{related.length > 0 ? (
|
||||
<Grid rows={4}>
|
||||
{related.map((item) => (
|
||||
<GroupAgentCard key={item.identifier} {...item} />
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Flexbox align="center" padding={32} style={{ color: '#999' }}>
|
||||
{t('groupAgents.details.related.empty', { defaultValue: 'No related group agents found' })}
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Related;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Tag as AntdTag, Flexbox } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface TagListProps {
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
const TagList = memo<TagListProps>(({ tags = [] }) => {
|
||||
if (!tags || tags.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flexbox gap={8} horizontal wrap={'wrap'}>
|
||||
{tags.map((tag, index) => (
|
||||
<AntdTag key={index}>{tag}</AntdTag>
|
||||
))}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default TagList;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Block, Flexbox, Icon, Tag } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { MessageCircleHeartIcon, MessageCircleQuestionIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Title from '../../../../../features/Title';
|
||||
import MarkdownRender from '../../../../features/MakedownRender';
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
import TagList from './TagList';
|
||||
|
||||
const SystemRole = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { tokenUsage, tags = [], config } = useDetailContext();
|
||||
|
||||
const { systemRole, openingMessage, openingQuestions } = config || {};
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
{systemRole && (
|
||||
<>
|
||||
<Title tag={tokenUsage && <Tag>{t('groupAgents.details.tokenUsage', { defaultValue: `${tokenUsage} tokens` })}</Tag>}>
|
||||
{t('groupAgents.details.systemRole.title', { defaultValue: 'System Role' })}
|
||||
</Title>
|
||||
<Block gap={16} padding={16} variant={'outlined'}>
|
||||
{<MarkdownRender>{systemRole.trimEnd()}</MarkdownRender>}
|
||||
<TagList tags={tags} />
|
||||
</Block>
|
||||
</>
|
||||
)}
|
||||
{openingMessage && (
|
||||
<>
|
||||
<Title>
|
||||
{t('groupAgents.details.systemRole.openingMessage', {
|
||||
defaultValue: 'Opening Message',
|
||||
})}
|
||||
</Title>
|
||||
<Block align={'flex-start'} gap={12} horizontal padding={16} variant={'outlined'}>
|
||||
<Icon
|
||||
color={cssVar.colorError}
|
||||
icon={MessageCircleHeartIcon}
|
||||
size={20}
|
||||
style={{
|
||||
marginTop: 4,
|
||||
}}
|
||||
/>
|
||||
<MarkdownRender>{openingMessage?.trimEnd()}</MarkdownRender>
|
||||
</Block>
|
||||
</>
|
||||
)}
|
||||
{openingQuestions && openingQuestions.length > 0 && (
|
||||
<>
|
||||
<Title tag={<Tag>{openingQuestions?.length}</Tag>}>
|
||||
{t('groupAgents.details.systemRole.openingQuestions', {
|
||||
defaultValue: 'Opening Questions',
|
||||
})}
|
||||
</Title>
|
||||
<Flexbox gap={8}>
|
||||
{openingQuestions?.map((item, key) => (
|
||||
<Block gap={12} horizontal key={key} padding={16} variant={'outlined'}>
|
||||
<Icon color={cssVar.colorWarning} icon={MessageCircleQuestionIcon} size={20} />
|
||||
<MarkdownRender>{item}</MarkdownRender>
|
||||
</Block>
|
||||
))}
|
||||
</Flexbox>
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default SystemRole;
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Block, Flexbox, Icon, Tag } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { CheckIcon, MinusIcon } from 'lucide-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import InlineTable from '@/components/InlineTable';
|
||||
import PublishedTime from '@/components/PublishedTime';
|
||||
|
||||
import Title from '../../../../../features/Title';
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const Versions = memo(() => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { versions = [], currentVersion } = useDetailContext();
|
||||
|
||||
const statusTagMap = useMemo(
|
||||
() => ({
|
||||
archived: {
|
||||
color: 'default' as const,
|
||||
label: t('groupAgents.details.version.status.archived', { defaultValue: 'Archived' }),
|
||||
},
|
||||
deprecated: {
|
||||
color: 'warning' as const,
|
||||
label: t('groupAgents.details.version.status.deprecated', { defaultValue: 'Deprecated' }),
|
||||
},
|
||||
published: {
|
||||
color: 'success' as const,
|
||||
label: t('groupAgents.details.version.status.published', { defaultValue: 'Published' }),
|
||||
},
|
||||
unpublished: {
|
||||
color: 'default' as const,
|
||||
label: t('groupAgents.details.version.status.unpublished', { defaultValue: 'Unpublished' }),
|
||||
},
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
if (!versions.length) {
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Title>
|
||||
{t('groupAgents.details.version.title', { defaultValue: 'Version History' })}
|
||||
</Title>
|
||||
<Block padding={24} variant={'outlined'}>
|
||||
{t('groupAgents.details.version.empty', { defaultValue: 'No version history available' })}
|
||||
</Block>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Title>
|
||||
{t('groupAgents.details.version.title', { defaultValue: 'Version History' })}
|
||||
</Title>
|
||||
<Block variant={'outlined'}>
|
||||
<InlineTable
|
||||
columns={[
|
||||
{
|
||||
dataIndex: 'version',
|
||||
render: (_: any, record: any) => {
|
||||
const statusKey =
|
||||
record.status &&
|
||||
Object.prototype.hasOwnProperty.call(statusTagMap, record.status)
|
||||
? (record.status as keyof typeof statusTagMap)
|
||||
: undefined;
|
||||
const statusMeta = statusKey ? statusTagMap[statusKey] : undefined;
|
||||
|
||||
return (
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<code style={{ fontSize: 14 }}>{record.version}</code>
|
||||
{(record.isLatest || record.version === currentVersion) && (
|
||||
<Tag color={'info'}>
|
||||
{t('groupAgents.details.version.table.isLatest', { defaultValue: 'Latest' })}
|
||||
</Tag>
|
||||
)}
|
||||
{statusMeta && <Tag color={statusMeta.color}>{statusMeta.label}</Tag>}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
title: t('groupAgents.details.version.table.version', { defaultValue: 'Version' }),
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
dataIndex: 'isValidated',
|
||||
render: (_: any, record: any) => (
|
||||
<Icon
|
||||
color={record.isValidated ? cssVar.colorSuccess : cssVar.colorTextDescription}
|
||||
icon={record.isValidated ? CheckIcon : MinusIcon}
|
||||
/>
|
||||
),
|
||||
title: t('groupAgents.details.version.table.isValidated', {
|
||||
defaultValue: 'Validated',
|
||||
}),
|
||||
},
|
||||
{
|
||||
align: 'end',
|
||||
dataIndex: 'createdAt',
|
||||
render: (_: any, record: any) => (
|
||||
<PublishedTime date={record.createdAt} showPrefix={false} />
|
||||
),
|
||||
title: t('groupAgents.details.version.table.publishAt', {
|
||||
defaultValue: 'Published At',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
dataSource={versions}
|
||||
rowKey={'version'}
|
||||
size={'middle'}
|
||||
/>
|
||||
</Block>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
Versions.displayName = 'GroupAgentVersions';
|
||||
|
||||
export default Versions;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { memo } from 'react';
|
||||
|
||||
import Sidebar from '../Sidebar';
|
||||
import Nav, { GroupAgentNavKey } from './Nav';
|
||||
import Overview from './Overview';
|
||||
import SystemRole from './SystemRole';
|
||||
import Versions from './Versions';
|
||||
|
||||
const Details = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
const { mobile = isMobile } = useResponsive();
|
||||
const [activeTabParam, setActiveTab] = useQueryState('activeTab');
|
||||
const activeTab = activeTabParam || GroupAgentNavKey.Overview;
|
||||
|
||||
return (
|
||||
<Flexbox gap={24}>
|
||||
{/* Navigation */}
|
||||
<Nav
|
||||
activeTab={activeTab as GroupAgentNavKey}
|
||||
mobile={mobile}
|
||||
setActiveTab={(tab) => setActiveTab(tab)}
|
||||
/>
|
||||
|
||||
<Flexbox
|
||||
gap={48}
|
||||
horizontal={!mobile}
|
||||
style={mobile ? { flexDirection: 'column-reverse' } : undefined}
|
||||
>
|
||||
{/* Main Content */}
|
||||
<Flexbox
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
width={'100%'}
|
||||
>
|
||||
{/* Tab Content */}
|
||||
{activeTab === GroupAgentNavKey.Overview && <Overview />}
|
||||
{activeTab === GroupAgentNavKey.SystemRole && <SystemRole />}
|
||||
{activeTab === GroupAgentNavKey.Versions && <Versions />}
|
||||
</Flexbox>
|
||||
|
||||
{/* Sidebar */}
|
||||
<Sidebar mobile={mobile} />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Details;
|
||||
@@ -0,0 +1,253 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ActionIcon,
|
||||
Avatar,
|
||||
Button,
|
||||
Flexbox,
|
||||
Icon,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipGroup,
|
||||
} from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { createStaticStyles, cssVar, useResponsive } from 'antd-style';
|
||||
import { BookmarkCheckIcon, BookmarkIcon, DotIcon, HeartIcon, UsersIcon } from 'lucide-react';
|
||||
import qs from 'query-string';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import PublishedTime from '@/components/PublishedTime';
|
||||
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
|
||||
import { socialService } from '@/services/social';
|
||||
|
||||
import { useDetailContext } from './DetailProvider';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
time: css`
|
||||
font-size: 12px;
|
||||
color: ${cssVar.colorTextDescription};
|
||||
`,
|
||||
}));
|
||||
|
||||
const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { message } = App.useApp();
|
||||
const data = useDetailContext();
|
||||
const { mobile = isMobile } = useResponsive();
|
||||
const { isAuthenticated, signIn, session } = useMarketAuth();
|
||||
const [favoriteLoading, setFavoriteLoading] = useState(false);
|
||||
const [likeLoading, setLikeLoading] = useState(false);
|
||||
|
||||
const {
|
||||
memberAgents = [],
|
||||
author,
|
||||
avatar,
|
||||
title,
|
||||
category,
|
||||
identifier,
|
||||
createdAt,
|
||||
userName,
|
||||
} = data;
|
||||
|
||||
const displayAvatar = avatar || title?.[0] || '👥';
|
||||
const memberCount = memberAgents?.length || 0;
|
||||
|
||||
// Set access token for social service
|
||||
if (session?.accessToken) {
|
||||
socialService.setAccessToken(session.accessToken);
|
||||
}
|
||||
|
||||
// TODO: Use 'group_agent' type when social service supports it
|
||||
// Fetch favorite status
|
||||
const { data: favoriteStatus, mutate: mutateFavorite } = useSWR(
|
||||
identifier && isAuthenticated ? ['favorite-status', 'agent', identifier] : null,
|
||||
() => socialService.checkFavoriteStatus('agent', identifier!),
|
||||
{ revalidateOnFocus: false },
|
||||
);
|
||||
|
||||
const isFavorited = favoriteStatus?.isFavorited ?? false;
|
||||
|
||||
// Fetch like status
|
||||
const { data: likeStatus, mutate: mutateLike } = useSWR(
|
||||
identifier && isAuthenticated ? ['like-status', 'agent', identifier] : null,
|
||||
() => socialService.checkLikeStatus('agent', identifier!),
|
||||
{ revalidateOnFocus: false },
|
||||
);
|
||||
const isLiked = likeStatus?.isLiked ?? false;
|
||||
|
||||
const handleFavoriteClick = async () => {
|
||||
if (!isAuthenticated) {
|
||||
await signIn();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!identifier) return;
|
||||
|
||||
setFavoriteLoading(true);
|
||||
try {
|
||||
if (isFavorited) {
|
||||
await socialService.removeFavorite('agent', identifier);
|
||||
message.success(t('assistant.unfavoriteSuccess'));
|
||||
} else {
|
||||
await socialService.addFavorite('agent', identifier);
|
||||
message.success(t('assistant.favoriteSuccess'));
|
||||
}
|
||||
await mutateFavorite();
|
||||
} catch {
|
||||
message.error(t('assistant.favoriteFailed'));
|
||||
} finally {
|
||||
setFavoriteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLikeClick = async () => {
|
||||
if (!isAuthenticated) {
|
||||
await signIn();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!identifier) return;
|
||||
|
||||
setLikeLoading(true);
|
||||
try {
|
||||
if (isLiked) {
|
||||
await socialService.unlike('agent', identifier);
|
||||
message.success(t('assistant.unlikeSuccess'));
|
||||
} else {
|
||||
await socialService.like('agent', identifier);
|
||||
message.success(t('assistant.likeSuccess'));
|
||||
}
|
||||
await mutateLike();
|
||||
} catch {
|
||||
message.error(t('assistant.likeFailed'));
|
||||
} finally {
|
||||
setLikeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cateButton = category ? (
|
||||
<Link
|
||||
to={qs.stringifyUrl({
|
||||
query: { category },
|
||||
url: '/community/group_agent',
|
||||
})}
|
||||
>
|
||||
<Button size={'middle'} variant={'outlined'}>
|
||||
{category}
|
||||
</Button>
|
||||
</Link>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Flexbox gap={12}>
|
||||
<Flexbox align={'flex-start'} gap={16} horizontal width={'100%'}>
|
||||
<Avatar avatar={displayAvatar} shape={'square'} size={mobile ? 48 : 64} />
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={4}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
gap={8}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
flex={1}
|
||||
gap={12}
|
||||
horizontal
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as={'h1'}
|
||||
ellipsis
|
||||
style={{ fontSize: mobile ? 18 : 24, margin: 0 }}
|
||||
title={identifier}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
<Tooltip title={isLiked ? t('assistant.unlike') : t('assistant.like')}>
|
||||
<ActionIcon
|
||||
icon={HeartIcon}
|
||||
loading={likeLoading}
|
||||
onClick={handleLikeClick}
|
||||
style={isLiked ? { color: '#ff4d4f' } : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={isFavorited ? t('assistant.unfavorite') : t('assistant.favorite')}>
|
||||
<ActionIcon
|
||||
icon={isFavorited ? BookmarkCheckIcon : BookmarkIcon}
|
||||
loading={favoriteLoading}
|
||||
onClick={handleFavoriteClick}
|
||||
variant={isFavorited ? 'outlined' : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
{(() => {
|
||||
// API returns author as object {avatar, name, userName}, but type definition says string
|
||||
const authorObj =
|
||||
typeof author === 'object' && author !== null ? (author as any) : null;
|
||||
const authorName = authorObj ? authorObj.name || authorObj.userName : author;
|
||||
|
||||
return authorName && userName ? (
|
||||
<Link style={{ color: 'inherit' }} to={urlJoin('/community/user', userName)}>
|
||||
{authorName}
|
||||
</Link>
|
||||
) : (
|
||||
authorName
|
||||
);
|
||||
})()}
|
||||
<Icon icon={DotIcon} />
|
||||
<PublishedTime
|
||||
className={styles.time}
|
||||
date={createdAt as string}
|
||||
template={'MMM DD, YYYY'}
|
||||
/>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<TooltipGroup>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
gap={mobile ? 12 : 24}
|
||||
horizontal
|
||||
style={{
|
||||
color: cssVar.colorTextSecondary,
|
||||
}}
|
||||
>
|
||||
{!mobile && cateButton}
|
||||
{Boolean(memberCount) && (
|
||||
<Tooltip
|
||||
styles={{ root: { pointerEvents: 'none' } }}
|
||||
title={t('groupAgents.memberCount', { defaultValue: 'Members' })}
|
||||
>
|
||||
<Flexbox align={'center'} gap={6} horizontal>
|
||||
<Icon icon={UsersIcon} />
|
||||
{memberCount}
|
||||
</Flexbox>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flexbox>
|
||||
</TooltipGroup>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,222 @@
|
||||
'use client';
|
||||
|
||||
import { Button, DropdownMenu, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { chatGroupService } from '@/services/chatGroup';
|
||||
import { discoverService } from '@/services/discover';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
buttonGroup: css`
|
||||
width: 100%;
|
||||
`,
|
||||
menuButton: css`
|
||||
padding-inline: 8px;
|
||||
border-start-start-radius: 0 !important;
|
||||
border-end-start-radius: 0 !important;
|
||||
`,
|
||||
primaryButton: css`
|
||||
border-start-end-radius: 0 !important;
|
||||
border-end-end-radius: 0 !important;
|
||||
`,
|
||||
}));
|
||||
|
||||
const AddGroupAgent = memo<{ mobile?: boolean }>(() => {
|
||||
const {
|
||||
avatar,
|
||||
description,
|
||||
tags,
|
||||
title,
|
||||
config,
|
||||
backgroundColor,
|
||||
identifier,
|
||||
memberAgents = [],
|
||||
} = useDetailContext();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { message, modal } = App.useApp();
|
||||
const { t } = useTranslation('discover');
|
||||
const navigate = useNavigate();
|
||||
const loadGroups = useAgentGroupStore((s) => s.loadGroups);
|
||||
|
||||
const meta = {
|
||||
avatar,
|
||||
backgroundColor,
|
||||
description,
|
||||
tags,
|
||||
title,
|
||||
};
|
||||
|
||||
// Check if a group with the same title already exists
|
||||
const checkDuplicateGroup = async () => {
|
||||
if (!title) return false;
|
||||
try {
|
||||
const groups = await chatGroupService.getGroups();
|
||||
return groups.some((g) => g.title === title);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const showDuplicateConfirmation = (callback: () => void) => {
|
||||
modal.confirm({
|
||||
cancelText: t('cancel', { ns: 'common' }),
|
||||
content: t('groupAgents.duplicateAdd.content', {
|
||||
defaultValue: 'This group agent has already been added. Do you want to add it again?',
|
||||
title,
|
||||
}),
|
||||
okText: t('groupAgents.duplicateAdd.ok', { defaultValue: 'Add Anyway' }),
|
||||
onOk: callback,
|
||||
title: t('groupAgents.duplicateAdd.title', { defaultValue: 'Group Already Added' }),
|
||||
});
|
||||
};
|
||||
|
||||
const createGroupFromMarket = async (shouldNavigate = true) => {
|
||||
if (!config) {
|
||||
message.error(
|
||||
t('groupAgents.noConfig', { defaultValue: 'Group configuration not available' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare group config
|
||||
const groupConfig = {
|
||||
config: {
|
||||
allowDM: config.allowDM,
|
||||
openingMessage: config.openingMessage,
|
||||
openingQuestions: config.openingQuestions,
|
||||
revealDM: config.revealDM,
|
||||
},
|
||||
content: config.systemRole,
|
||||
...meta,
|
||||
};
|
||||
|
||||
// Prepare member agents from market data
|
||||
const members = memberAgents.map((member: any) => {
|
||||
const agent = member.agent || member;
|
||||
const currentVersion = member.currentVersion || member;
|
||||
return {
|
||||
avatar: currentVersion.avatar,
|
||||
backgroundColor: currentVersion.backgroundColor,
|
||||
description: currentVersion.description,
|
||||
model: currentVersion.model,
|
||||
plugins: currentVersion.plugins,
|
||||
provider: currentVersion.provider,
|
||||
systemRole: currentVersion.systemRole || currentVersion.content,
|
||||
tags: currentVersion.tags,
|
||||
title: currentVersion.name || agent.name,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
// Create group with all members in one request
|
||||
const result = await chatGroupService.createGroupWithMembers(groupConfig, members);
|
||||
|
||||
// Refresh group list
|
||||
await loadGroups();
|
||||
|
||||
// Report installation to marketplace
|
||||
if (identifier) {
|
||||
discoverService.reportAgentInstall(identifier);
|
||||
discoverService.reportAgentEvent({
|
||||
event: 'add',
|
||||
identifier,
|
||||
source: location.pathname,
|
||||
});
|
||||
}
|
||||
|
||||
message.success(
|
||||
t('groupAgents.addSuccess', { defaultValue: 'Group agent added successfully!' }),
|
||||
);
|
||||
|
||||
if (shouldNavigate) {
|
||||
navigate(urlJoin('/group', result.groupId));
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to create group from market:', error);
|
||||
message.error(
|
||||
t('groupAgents.addError', {
|
||||
defaultValue: 'Failed to add group agent. Please try again.',
|
||||
}),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAndConverse = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const isDuplicate = await checkDuplicateGroup();
|
||||
if (isDuplicate) {
|
||||
showDuplicateConfirmation(() => createGroupFromMarket(true));
|
||||
} else {
|
||||
await createGroupFromMarket(true);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const isDuplicate = await checkDuplicateGroup();
|
||||
if (isDuplicate) {
|
||||
showDuplicateConfirmation(() => createGroupFromMarket(false));
|
||||
} else {
|
||||
await createGroupFromMarket(false);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'addGroup',
|
||||
label: t('groupAgents.addGroup', { defaultValue: 'Add Group' }),
|
||||
onClick: handleAdd,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.buttonGroup} gap={0} horizontal>
|
||||
<Button
|
||||
block
|
||||
className={styles.primaryButton}
|
||||
loading={isLoading}
|
||||
onClick={handleAddAndConverse}
|
||||
size={'large'}
|
||||
style={{ flex: 1, width: 'unset' }}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('groupAgents.addAndConverse', { defaultValue: 'Add & Start Conversation' })}
|
||||
</Button>
|
||||
<DropdownMenu
|
||||
items={menuItems}
|
||||
popupProps={{ style: { minWidth: 267 } }}
|
||||
triggerProps={{ disabled: isLoading }}
|
||||
>
|
||||
<Button
|
||||
className={styles.menuButton}
|
||||
disabled={isLoading}
|
||||
icon={<Icon icon={ChevronDownIcon} />}
|
||||
size={'large'}
|
||||
type={'primary'}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default AddGroupAgent;
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { OFFICIAL_URL } from '@/const/url';
|
||||
|
||||
import ShareButton from '../../../../features/ShareButton';
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
import AddGroupAgent from './AddGroupAgent';
|
||||
|
||||
const ActionButton = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const { avatar, title, description, tags, identifier } = useDetailContext();
|
||||
|
||||
return (
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<AddGroupAgent mobile={mobile} />
|
||||
{identifier && (
|
||||
<ShareButton
|
||||
meta={{
|
||||
avatar: avatar,
|
||||
desc: description,
|
||||
hashtags: tags,
|
||||
title: title,
|
||||
url: urlJoin(OFFICIAL_URL, '/community/group_agent', identifier),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Collapse } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDetailContext } from '../../DetailProvider';
|
||||
|
||||
const Summary = memo(() => {
|
||||
const { description, summary } = useDetailContext();
|
||||
const { t } = useTranslation('discover');
|
||||
|
||||
const displayDescription = summary || description || 'No description provided';
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
defaultActiveKey={['summary']}
|
||||
expandIconPlacement={'end'}
|
||||
items={[
|
||||
{
|
||||
children: (
|
||||
<p
|
||||
style={{
|
||||
color: cssVar.colorTextSecondary,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{displayDescription}
|
||||
</p>
|
||||
),
|
||||
key: 'summary',
|
||||
label: t('groupAgents.details.summary.title', {
|
||||
defaultValue: 'What can you use this group for?',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
size={'small'}
|
||||
variant={'borderless'}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Summary;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Flexbox, ScrollShadow } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
|
||||
import { GroupAgentNavKey } from '../Details/Nav';
|
||||
import ActionButton from './ActionButton';
|
||||
import Summary from './Summary';
|
||||
|
||||
const Sidebar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const { activeTab = GroupAgentNavKey.Overview } = useQuery() as { activeTab: GroupAgentNavKey };
|
||||
|
||||
if (mobile) {
|
||||
return (
|
||||
<Flexbox gap={32}>
|
||||
<ActionButton mobile />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollShadow
|
||||
flex={'none'}
|
||||
gap={32}
|
||||
hideScrollBar
|
||||
size={4}
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 76px)',
|
||||
paddingBottom: 24,
|
||||
position: 'sticky',
|
||||
top: 16,
|
||||
}}
|
||||
width={360}
|
||||
>
|
||||
<ActionButton />
|
||||
{activeTab !== GroupAgentNavKey.Overview && <Summary />}
|
||||
</ScrollShadow>
|
||||
);
|
||||
});
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { ExclamationCircleOutlined, FolderOpenOutlined } from '@ant-design/icons';
|
||||
import { Button, FluentEmoji, Text } from '@lobehub/ui';
|
||||
import { Result } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface StatusPageProps {
|
||||
status: 'unpublished' | 'archived' | 'deprecated';
|
||||
}
|
||||
|
||||
const StatusPage = memo<StatusPageProps>(({ status }) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('discover');
|
||||
|
||||
const handleBackToMarket = () => {
|
||||
navigate('/community');
|
||||
};
|
||||
|
||||
// Unpublished status
|
||||
if (status === 'unpublished') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
minHeight: '60vh',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
extra={
|
||||
<Button onClick={handleBackToMarket} size={'large'} type="primary">
|
||||
{t('groupAgents.status.backToMarket', { defaultValue: 'Back to Market' })}
|
||||
</Button>
|
||||
}
|
||||
icon={<FluentEmoji emoji={'⌛'} size={96} type={'anim'} />}
|
||||
subTitle={
|
||||
<Text fontSize={16} type={'secondary'}>
|
||||
{t('groupAgents.status.unpublished.subtitle', {
|
||||
defaultValue:
|
||||
'This group agent is under review. Please contact support@lobehub.com if you have questions.',
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
title={
|
||||
<Text fontSize={28} weight={'bold'}>
|
||||
{t('groupAgents.status.unpublished.title', { defaultValue: 'Under Review' })}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Archived/Deprecated status
|
||||
const isArchived = status === 'archived';
|
||||
const statusKey = isArchived ? 'archived' : 'deprecated';
|
||||
const statusIcon = isArchived ? (
|
||||
<FolderOpenOutlined style={{ color: '#8c8c8c' }} />
|
||||
) : (
|
||||
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
minHeight: '60vh',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
extra={
|
||||
<Button onClick={handleBackToMarket} type="primary">
|
||||
{t('groupAgents.status.backToMarket', { defaultValue: 'Back to Market' })}
|
||||
</Button>
|
||||
}
|
||||
icon={statusIcon}
|
||||
subTitle={
|
||||
<div style={{ color: '#666', lineHeight: 1.6 }}>
|
||||
<p>
|
||||
{t(`groupAgents.status.${statusKey}.subtitle`, {
|
||||
defaultValue: `This group agent has been ${statusKey}.`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
title={t(`groupAgents.status.${statusKey}.title`, {
|
||||
defaultValue: isArchived ? 'Archived' : 'Deprecated',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default StatusPage;
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
|
||||
import NotFound from '../components/NotFound';
|
||||
import { TocProvider } from '../features/Toc/useToc';
|
||||
import Details from './features/Details';
|
||||
import { DetailProvider } from './features/DetailProvider';
|
||||
import Header from './features/Header';
|
||||
import StatusPage from './features/StatusPage';
|
||||
import Loading from './loading';
|
||||
|
||||
interface GroupAgentDetailPageProps {
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
const GroupAgentDetailPage = memo<GroupAgentDetailPageProps>(({ mobile }) => {
|
||||
const params = useParams<{ slug: string }>();
|
||||
const identifier = decodeURIComponent(params.slug ?? '');
|
||||
const { version } = useQuery() as { version?: string };
|
||||
|
||||
// Fetch group agent detail
|
||||
const useGroupAgentDetail = useDiscoverStore((s) => s.useGroupAgentDetail);
|
||||
const { data, isLoading } = useGroupAgentDetail({ identifier, version });
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
if (!data) return <NotFound />;
|
||||
|
||||
// Check status and show appropriate page
|
||||
const status = (data as any)?.group?.status || (data as any)?.status;
|
||||
if (status === 'unpublished' || status === 'archived' || status === 'deprecated') {
|
||||
return <StatusPage status={status} />;
|
||||
}
|
||||
|
||||
// Transform API data to match DiscoverGroupAgentDetail type
|
||||
const apiConfig = (data as any)?.currentVersion?.config || {};
|
||||
|
||||
const transformedData = {
|
||||
// Top level fields
|
||||
author: (data as any)?.author,
|
||||
// From currentVersion
|
||||
avatar: (data as any)?.currentVersion?.avatar,
|
||||
backgroundColor: (data as any)?.currentVersion?.backgroundColor,
|
||||
category: (data as any)?.currentVersion?.category,
|
||||
commentCount: (data as any)?.group?.commentCount,
|
||||
|
||||
// From currentVersion.config - rename systemPrompt to systemRole for consistency
|
||||
config: {
|
||||
...apiConfig,
|
||||
allowDM: apiConfig.allowDM,
|
||||
// Rename systemPrompt -> systemRole
|
||||
openingMessage: apiConfig.openingMessage,
|
||||
openingQuestions: apiConfig.openingQuestions,
|
||||
revealDM: apiConfig.revealDM,
|
||||
summary: apiConfig.summary,
|
||||
systemRole: apiConfig.systemPrompt,
|
||||
},
|
||||
|
||||
createdAt: (data as any)?.group?.createdAt,
|
||||
// Version info
|
||||
currentVersion: (data as any)?.currentVersion?.version,
|
||||
currentVersionNumber: (data as any)?.currentVersion?.versionNumber,
|
||||
description: (data as any)?.currentVersion?.description,
|
||||
favoriteCount: (data as any)?.group?.favoriteCount,
|
||||
homepage: (data as any)?.group?.homepage,
|
||||
// From group
|
||||
identifier: (data as any)?.group?.identifier,
|
||||
installCount: (data as any)?.group?.installCount,
|
||||
likeCount: (data as any)?.group?.likeCount,
|
||||
locale: (data as any)?.locale,
|
||||
memberAgents: (data as any)?.memberAgents || [],
|
||||
status: (data as any)?.group?.status,
|
||||
summary: (data as any)?.summary,
|
||||
tags: (data as any)?.currentVersion?.tags,
|
||||
title: (data as any)?.currentVersion?.name || (data as any)?.group?.name,
|
||||
updatedAt: (data as any)?.group?.updatedAt,
|
||||
userName: (data as any)?.author?.userName,
|
||||
versions: (data as any)?.versions || [],
|
||||
visibility: (data as any)?.group?.visibility,
|
||||
};
|
||||
|
||||
return (
|
||||
<TocProvider>
|
||||
<DetailProvider config={transformedData}>
|
||||
<Flexbox gap={16} width={'100%'}>
|
||||
{/* Header Section */}
|
||||
<Header mobile={mobile} />
|
||||
|
||||
{/* Details Section */}
|
||||
<Details mobile={mobile} />
|
||||
</Flexbox>
|
||||
</DetailProvider>
|
||||
</TocProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export default GroupAgentDetailPage;
|
||||
@@ -0,0 +1 @@
|
||||
export { DetailsLoading as default } from '../../components/ListLoading';
|
||||
@@ -3,11 +3,17 @@
|
||||
import { type ReactNode, createContext, memo, use } from 'react';
|
||||
|
||||
import { type MarketUserProfile } from '@/layout/AuthProvider/MarketAuth/types';
|
||||
import { type DiscoverAssistantItem, type DiscoverUserInfo } from '@/types/discover';
|
||||
import {
|
||||
type DiscoverAssistantItem,
|
||||
type DiscoverGroupAgentItem,
|
||||
type DiscoverUserInfo,
|
||||
} from '@/types/discover';
|
||||
|
||||
export interface UserDetailContextConfig {
|
||||
agentCount: number;
|
||||
agentGroups?: DiscoverGroupAgentItem[];
|
||||
agents: DiscoverAssistantItem[];
|
||||
groupCount: number;
|
||||
isOwner: boolean;
|
||||
mobile?: boolean;
|
||||
onEditProfile?: (onSuccess?: (profile: MarketUserProfile) => void) => void;
|
||||
|
||||
@@ -6,11 +6,13 @@ import { memo } from 'react';
|
||||
import UserAgentList from './UserAgentList';
|
||||
import UserFavoriteAgents from './UserFavoriteAgents';
|
||||
import UserFavoritePlugins from './UserFavoritePlugins';
|
||||
import UserGroupList from './UserGroupList';
|
||||
|
||||
const UserContent = memo(() => {
|
||||
return (
|
||||
<Flexbox gap={32}>
|
||||
<UserAgentList />
|
||||
<UserGroupList />
|
||||
<UserFavoriteAgents />
|
||||
<UserFavoritePlugins />
|
||||
</Flexbox>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, Block, Flexbox, Icon, Tag, Text, Tooltip, TooltipGroup } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { ClockIcon, DownloadIcon, HeartIcon, UsersIcon } from 'lucide-react';
|
||||
import qs from 'query-string';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import PublishedTime from '@/components/PublishedTime';
|
||||
import { type DiscoverGroupAgentItem } from '@/types/discover';
|
||||
import { formatIntergerNumber } from '@/utils/format';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => {
|
||||
return {
|
||||
desc: css`
|
||||
flex: 1;
|
||||
margin: 0 !important;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
footer: css`
|
||||
margin-block-start: 16px;
|
||||
border-block-start: 1px dashed ${cssVar.colorBorder};
|
||||
background: ${cssVar.colorBgContainer};
|
||||
`,
|
||||
secondaryDesc: css`
|
||||
font-size: 12px;
|
||||
color: ${cssVar.colorTextDescription};
|
||||
`,
|
||||
statTag: css`
|
||||
border-radius: 4px;
|
||||
|
||||
font-family: ${cssVar.fontFamilyCode};
|
||||
font-size: 11px;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
`,
|
||||
title: css`
|
||||
margin: 0 !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 500 !important;
|
||||
|
||||
&:hover {
|
||||
color: ${cssVar.colorLink};
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
type UserGroupCardProps = DiscoverGroupAgentItem;
|
||||
|
||||
const UserGroupCard = memo<UserGroupCardProps>(
|
||||
({ avatar, title, description, createdAt, category, installCount, identifier, memberCount }) => {
|
||||
const { t } = useTranslation(['discover']);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const link = qs.stringifyUrl(
|
||||
{
|
||||
query: { source: 'new' },
|
||||
url: urlJoin('/community/group_agent', identifier),
|
||||
},
|
||||
{ skipNull: true },
|
||||
);
|
||||
|
||||
const handleCardClick = useCallback(() => {
|
||||
navigate(link);
|
||||
}, [link, navigate]);
|
||||
|
||||
return (
|
||||
<Block
|
||||
clickable
|
||||
height={'100%'}
|
||||
onClick={handleCardClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
variant={'outlined'}
|
||||
width={'100%'}
|
||||
>
|
||||
<Flexbox
|
||||
align={'flex-start'}
|
||||
gap={16}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
padding={16}
|
||||
width={'100%'}
|
||||
>
|
||||
<Flexbox
|
||||
gap={12}
|
||||
horizontal
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Avatar avatar={avatar} shape={'square'} size={40} style={{ flex: 'none' }} />
|
||||
<Flexbox
|
||||
flex={1}
|
||||
gap={2}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ color: 'inherit', flex: 1, overflow: 'hidden' }}
|
||||
to={link}
|
||||
>
|
||||
<Text as={'h3'} className={styles.title} ellipsis style={{ flex: 1 }}>
|
||||
{title}
|
||||
</Text>
|
||||
</Link>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Flexbox flex={1} gap={12} paddingInline={16}>
|
||||
<Text
|
||||
as={'p'}
|
||||
className={styles.desc}
|
||||
ellipsis={{
|
||||
rows: 3,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
<TooltipGroup>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
{memberCount !== undefined && memberCount > 0 && (
|
||||
<Tooltip
|
||||
placement={'top'}
|
||||
styles={{ root: { pointerEvents: 'none' } }}
|
||||
title={t('groupAgents.memberCount', { defaultValue: 'Members' })}
|
||||
>
|
||||
<Tag className={styles.statTag} icon={<Icon icon={UsersIcon} />}>
|
||||
{formatIntergerNumber(memberCount)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{installCount !== undefined && installCount > 0 && (
|
||||
<Tooltip
|
||||
placement={'top'}
|
||||
styles={{ root: { pointerEvents: 'none' } }}
|
||||
title={t('groupAgents.downloads', { defaultValue: 'Downloads' })}
|
||||
>
|
||||
<Tag className={styles.statTag} icon={<Icon icon={DownloadIcon} />}>
|
||||
{formatIntergerNumber(installCount)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flexbox>
|
||||
</TooltipGroup>
|
||||
</Flexbox>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.footer}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
padding={16}
|
||||
>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.secondaryDesc}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Icon icon={ClockIcon} size={14} />
|
||||
<PublishedTime
|
||||
className={styles.secondaryDesc}
|
||||
date={createdAt}
|
||||
template={'MMM DD, YYYY'}
|
||||
/>
|
||||
</Flexbox>
|
||||
{category && t(`category.groupAgent.${category}` as any, { defaultValue: category })}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Block>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default UserGroupCard;
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox, Grid, Tag, Text } from '@lobehub/ui';
|
||||
import { Pagination } from 'antd';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import AssistantEmpty from '../../../features/AssistantEmpty';
|
||||
import { useUserDetailContext } from './DetailProvider';
|
||||
import UserGroupCard from './UserGroupCard';
|
||||
|
||||
interface UserGroupListProps {
|
||||
pageSize?: number;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 10 }) => {
|
||||
const { t } = useTranslation('discover');
|
||||
const { agentGroups, groupCount } = useUserDetailContext();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const paginatedGroups = useMemo(() => {
|
||||
if (!agentGroups) return [];
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
return agentGroups.slice(startIndex, startIndex + pageSize);
|
||||
}, [agentGroups, currentPage, pageSize]);
|
||||
|
||||
if (!agentGroups || agentGroups.length === 0) return null;
|
||||
|
||||
const showPagination = agentGroups.length > pageSize;
|
||||
|
||||
return (
|
||||
<Flexbox gap={16}>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<Text fontSize={16} weight={500}>
|
||||
{t('user.publishedGroups', { defaultValue: '创作的群组' })}
|
||||
</Text>
|
||||
{groupCount > 0 && <Tag>{groupCount}</Tag>}
|
||||
</Flexbox>
|
||||
<Grid rows={rows} width={'100%'}>
|
||||
{paginatedGroups.map((item, index) => (
|
||||
<UserGroupCard key={item.identifier || index} {...item} />
|
||||
))}
|
||||
</Grid>
|
||||
{showPagination && (
|
||||
<Flexbox align={'center'} justify={'center'}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
onChange={(page) => setCurrentPage(page)}
|
||||
pageSize={pageSize}
|
||||
showSizeChanger={false}
|
||||
total={agentGroups.length}
|
||||
/>
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default UserGroupList;
|
||||
@@ -58,11 +58,13 @@ const UserDetailPage = memo<UserDetailPageProps>(({ mobile }) => {
|
||||
|
||||
const contextConfig = useMemo(() => {
|
||||
if (!data || !data.user) return null;
|
||||
const { user, agents } = data;
|
||||
const { user, agents, agentGroups } = data;
|
||||
const totalInstalls = agents.reduce((sum, agent) => sum + (agent.installCount || 0), 0);
|
||||
return {
|
||||
agentCount: agents.length,
|
||||
agentGroups: agentGroups || [],
|
||||
agents,
|
||||
groupCount: agentGroups?.length || 0,
|
||||
isOwner,
|
||||
mobile,
|
||||
onEditProfile: handleEditProfile,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Avatar, Block, Flexbox, Icon, Text } from '@lobehub/ui';
|
||||
import { Avatar, Block, Flexbox, Icon, Tag, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import { ClockIcon } from 'lucide-react';
|
||||
import qs from 'query-string';
|
||||
@@ -9,8 +9,8 @@ import urlJoin from 'url-join';
|
||||
|
||||
import PublishedTime from '@/components/PublishedTime';
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { type AssistantMarketSource, type DiscoverAssistantItem } from '@/types/discover';
|
||||
import { discoverService } from '@/services/discover';
|
||||
import { type AssistantMarketSource, type DiscoverAssistantItem } from '@/types/discover';
|
||||
|
||||
import TokenTag from './TokenTag';
|
||||
|
||||
@@ -68,13 +68,16 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
||||
installCount,
|
||||
backgroundColor,
|
||||
userName,
|
||||
type,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { source } = useQuery() as { source?: AssistantMarketSource };
|
||||
const isGroupAgent = type === 'agent-group';
|
||||
const basePath = isGroupAgent ? '/community/group_agent' : '/community/assistant';
|
||||
const link = qs.stringifyUrl(
|
||||
{
|
||||
query: { source },
|
||||
url: urlJoin('/community/assistant', identifier),
|
||||
url: urlJoin(basePath, identifier),
|
||||
},
|
||||
{ skipNull: true },
|
||||
);
|
||||
@@ -93,11 +96,13 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
discoverService.reportAgentEvent({
|
||||
event: 'click',
|
||||
identifier,
|
||||
source: location.pathname,
|
||||
}).catch(() => {});
|
||||
discoverService
|
||||
.reportAgentEvent({
|
||||
event: 'click',
|
||||
identifier,
|
||||
source: location.pathname,
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
navigate(link);
|
||||
}, [identifier, link, navigate]);
|
||||
@@ -115,6 +120,19 @@ const AssistantItem = memo<DiscoverAssistantItem>(
|
||||
variant={'outlined'}
|
||||
width={'100%'}
|
||||
>
|
||||
{isGroupAgent && (
|
||||
<Tag
|
||||
color="info"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
top: 12,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{t('groupAgents.tag', { defaultValue: '群组' })}
|
||||
</Tag>
|
||||
)}
|
||||
<Flexbox
|
||||
align={'flex-start'}
|
||||
gap={16}
|
||||
|
||||
@@ -16,6 +16,7 @@ const AssistantPage = memo(() => {
|
||||
const useAssistantList = useDiscoverStore((s) => s.useAssistantList);
|
||||
const { data, isLoading } = useAssistantList({
|
||||
category,
|
||||
includeAgentGroup: true,
|
||||
order,
|
||||
page,
|
||||
pageSize: 21,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
import { useGroupProfileStore } from '@/store/groupProfile';
|
||||
|
||||
import AutoSaveHint from '../Header/AutoSaveHint';
|
||||
import GroupPublishButton from '../Header/GroupPublishButton';
|
||||
import GroupHeader from './GroupHeader';
|
||||
|
||||
const GroupProfile = memo(() => {
|
||||
@@ -75,6 +76,7 @@ const GroupProfile = memo(() => {
|
||||
>
|
||||
{t('startConversation')}
|
||||
</Button>
|
||||
<GroupPublishButton />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<Divider />
|
||||
|
||||
@@ -19,7 +19,8 @@ const PublishResultModal = memo<PublishResultModalProps>(({ identifier, onCancel
|
||||
|
||||
const handleGoToMarket = () => {
|
||||
if (identifier) {
|
||||
navigate(`/community/assistant/${identifier}`);
|
||||
console.log('identifier', identifier);
|
||||
navigate(`/community/group_agent/${identifier}`);
|
||||
}
|
||||
onCancel();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, Flexbox, Modal } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { OriginalGroupInfo } from './types';
|
||||
|
||||
interface GroupForkConfirmModalProps {
|
||||
loading?: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
open: boolean;
|
||||
originalGroup: OriginalGroupInfo | null;
|
||||
}
|
||||
|
||||
const GroupForkConfirmModal = memo<GroupForkConfirmModalProps>(
|
||||
({ open, onCancel, onConfirm, originalGroup, loading }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
if (!originalGroup) return null;
|
||||
|
||||
const authorName = originalGroup.author?.name || originalGroup.author?.userName || 'Unknown';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
cancelText={t('cancel', { ns: 'common' })}
|
||||
centered
|
||||
closable
|
||||
confirmLoading={loading}
|
||||
okText={t('marketPublish.forkConfirm.confirmGroup')}
|
||||
onCancel={onCancel}
|
||||
onOk={onConfirm}
|
||||
open={open}
|
||||
title={t('marketPublish.forkConfirm.titleGroup')}
|
||||
width={480}
|
||||
>
|
||||
<Flexbox gap={16} style={{ marginTop: 16 }}>
|
||||
<Flexbox align="center" gap={12} horizontal>
|
||||
<Avatar avatar={originalGroup.avatar} size={48} style={{ flex: 'none' }} />
|
||||
<Flexbox gap={4}>
|
||||
<div style={{ fontWeight: 500 }}>{originalGroup.name}</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.6 }}>
|
||||
{t('marketPublish.forkConfirm.by', { author: authorName })}
|
||||
</div>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
|
||||
<p style={{ lineHeight: 1.6, margin: 0 }}>
|
||||
{t('marketPublish.forkConfirm.descriptionGroup')}
|
||||
</p>
|
||||
</Flexbox>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
GroupForkConfirmModal.displayName = 'GroupForkConfirmModal';
|
||||
|
||||
export default GroupForkConfirmModal;
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { FluentEmoji, Modal, Text } from '@lobehub/ui';
|
||||
import { Result } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface GroupPublishResultModalProps {
|
||||
identifier?: string;
|
||||
onCancel: () => void;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const GroupPublishResultModal = memo<GroupPublishResultModalProps>(
|
||||
({ identifier, onCancel, open }) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('setting');
|
||||
const { t: tCommon } = useTranslation('common');
|
||||
|
||||
const handleGoToMarket = () => {
|
||||
if (identifier) {
|
||||
navigate(`/community/group_agent/${identifier}`);
|
||||
}
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
cancelText={tCommon('cancel')}
|
||||
centered
|
||||
okText={t('marketPublish.resultModal.view')}
|
||||
onCancel={onCancel}
|
||||
onOk={handleGoToMarket}
|
||||
open={open}
|
||||
title={null}
|
||||
width={440}
|
||||
>
|
||||
<Result
|
||||
icon={<FluentEmoji emoji={'🎉'} size={96} type={'anim'} />}
|
||||
style={{
|
||||
paddingBottom: 32,
|
||||
paddingTop: 48,
|
||||
width: '100%',
|
||||
}}
|
||||
subTitle={
|
||||
<Text fontSize={14} type={'secondary'}>
|
||||
{t('marketPublish.resultModal.messageGroup')}
|
||||
</Text>
|
||||
}
|
||||
title={
|
||||
<Text fontSize={28} weight={'bold'}>
|
||||
{t('marketPublish.resultModal.title')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default GroupPublishResultModal;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { ShapesUploadIcon } from '@lobehub/ui/icons';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { message } from '@/components/AntdStaticMethods';
|
||||
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
|
||||
import { resolveMarketAuthError } from '@/layout/AuthProvider/MarketAuth/errors';
|
||||
|
||||
import GroupForkConfirmModal from './GroupForkConfirmModal';
|
||||
import type { MarketPublishAction, OriginalGroupInfo } from './types';
|
||||
import { useMarketGroupPublish } from './useMarketGroupPublish';
|
||||
|
||||
interface GroupPublishButtonProps {
|
||||
action: MarketPublishAction;
|
||||
onPublishSuccess?: (identifier: string) => void;
|
||||
}
|
||||
|
||||
const PublishButton = memo<GroupPublishButtonProps>(({ action, onPublishSuccess }) => {
|
||||
const { t } = useTranslation(['setting', 'marketAuth']);
|
||||
|
||||
const { isAuthenticated, isLoading, signIn } = useMarketAuth();
|
||||
const { checkOwnership, isCheckingOwnership, isPublishing, publish } = useMarketGroupPublish({
|
||||
action,
|
||||
onSuccess: onPublishSuccess,
|
||||
});
|
||||
|
||||
// Fork confirmation modal state
|
||||
const [showForkModal, setShowForkModal] = useState(false);
|
||||
const [originalGroupInfo, setOriginalGroupInfo] = useState<OriginalGroupInfo | null>(null);
|
||||
|
||||
const buttonConfig = useMemo(() => {
|
||||
if (action === 'upload') {
|
||||
return {
|
||||
authenticated: t('marketPublish.uploadGroup.tooltip'),
|
||||
unauthenticated: t('marketPublish.uploadGroup.tooltip'),
|
||||
} as const;
|
||||
}
|
||||
|
||||
const submitText = t('submitGroupModal.tooltips');
|
||||
|
||||
return {
|
||||
authenticated: submitText,
|
||||
unauthenticated: t('marketPublish.submitGroup.tooltip'),
|
||||
} as const;
|
||||
}, [action, t]);
|
||||
|
||||
const doPublish = useCallback(async () => {
|
||||
// Check ownership before publishing
|
||||
const { needsForkConfirm, originalGroup } = await checkOwnership();
|
||||
|
||||
if (needsForkConfirm && originalGroup) {
|
||||
// Show fork confirmation modal
|
||||
setOriginalGroupInfo(originalGroup);
|
||||
setShowForkModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// No confirmation needed, proceed with publish
|
||||
await publish();
|
||||
}, [checkOwnership, publish]);
|
||||
|
||||
const handleButtonClick = useCallback(async () => {
|
||||
if (!isAuthenticated) {
|
||||
try {
|
||||
await signIn();
|
||||
// After authentication, proceed with ownership check and publish
|
||||
await doPublish();
|
||||
} catch (error) {
|
||||
console.error(`[GroupPublishButton][${action}] Authorization failed:`, error);
|
||||
const normalizedError = resolveMarketAuthError(error);
|
||||
message.error({
|
||||
content: t(`errors.${normalizedError.code}`, { ns: 'marketAuth' }),
|
||||
key: 'market-auth',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// User is authenticated, check ownership and publish
|
||||
await doPublish();
|
||||
}, [action, doPublish, isAuthenticated, signIn, t]);
|
||||
|
||||
const handleForkConfirm = useCallback(async () => {
|
||||
setShowForkModal(false);
|
||||
setOriginalGroupInfo(null);
|
||||
// User confirmed, proceed with publish
|
||||
await publish();
|
||||
}, [publish]);
|
||||
|
||||
const handleForkCancel = useCallback(() => {
|
||||
setShowForkModal(false);
|
||||
setOriginalGroupInfo(null);
|
||||
}, []);
|
||||
|
||||
const buttonTitle = isAuthenticated ? buttonConfig.authenticated : buttonConfig.unauthenticated;
|
||||
const loading = isLoading || isCheckingOwnership || isPublishing;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon={ShapesUploadIcon}
|
||||
loading={loading}
|
||||
onClick={handleButtonClick}
|
||||
title={buttonTitle}
|
||||
>
|
||||
{t('publishToCommunity')}
|
||||
</Button>
|
||||
<GroupForkConfirmModal
|
||||
loading={isPublishing}
|
||||
onCancel={handleForkCancel}
|
||||
onConfirm={handleForkConfirm}
|
||||
open={showForkModal}
|
||||
originalGroup={originalGroupInfo}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PublishButton.displayName = 'GroupPublishButton';
|
||||
|
||||
export default PublishButton;
|
||||
@@ -0,0 +1,46 @@
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
|
||||
import GroupPublishResultModal from './GroupPublishResultModal';
|
||||
import PublishButton from './PublishButton';
|
||||
|
||||
/**
|
||||
* Group Publish Button Component
|
||||
*
|
||||
* Simplified version - backend handles ownership check automatically.
|
||||
* The action type (submit vs upload) is determined by backend based on:
|
||||
* 1. Whether the identifier exists
|
||||
* 2. Whether the current user is the owner
|
||||
*/
|
||||
const GroupPublishButton = memo(() => {
|
||||
const currentGroup = useAgentGroupStore(agentGroupSelectors.currentGroup, isEqual);
|
||||
|
||||
const [showResultModal, setShowResultModal] = useState(false);
|
||||
const [publishedIdentifier, setPublishedIdentifier] = useState<string>();
|
||||
|
||||
const handlePublishSuccess = useCallback((identifier: string) => {
|
||||
setPublishedIdentifier(identifier);
|
||||
setShowResultModal(true);
|
||||
}, []);
|
||||
|
||||
// Determine action based on whether we have an existing marketIdentifier
|
||||
// Backend will verify ownership and decide to create new or update
|
||||
// marketIdentifier is stored in editorData
|
||||
const action = currentGroup?.editorData?.marketIdentifier ? 'upload' : 'submit';
|
||||
|
||||
return (
|
||||
<>
|
||||
<PublishButton action={action} onPublishSuccess={handlePublishSuccess} />
|
||||
<GroupPublishResultModal
|
||||
identifier={publishedIdentifier}
|
||||
onCancel={() => setShowResultModal(false)}
|
||||
open={showResultModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default GroupPublishButton;
|
||||
@@ -0,0 +1,12 @@
|
||||
export type MarketPublishAction = 'submit' | 'upload';
|
||||
|
||||
export interface OriginalGroupInfo {
|
||||
author?: {
|
||||
avatar?: string;
|
||||
name?: string;
|
||||
userName?: string;
|
||||
};
|
||||
avatar?: string;
|
||||
identifier: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { message } from '@/components/AntdStaticMethods';
|
||||
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { globalGeneralSelectors } from '@/store/global/selectors';
|
||||
|
||||
import type { MarketPublishAction, OriginalGroupInfo } from './types';
|
||||
import { generateDefaultChangelog } from './utils';
|
||||
|
||||
interface UseMarketGroupPublishOptions {
|
||||
action: MarketPublishAction;
|
||||
onSuccess?: (identifier: string) => void;
|
||||
}
|
||||
|
||||
export interface CheckOwnershipResult {
|
||||
needsForkConfirm: boolean;
|
||||
originalGroup: OriginalGroupInfo | null;
|
||||
}
|
||||
|
||||
export const useMarketGroupPublish = ({ action, onSuccess }: UseMarketGroupPublishOptions) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [isPublishing, setIsPublishing] = useState(false);
|
||||
const [isCheckingOwnership, setIsCheckingOwnership] = useState(false);
|
||||
const isPublishingRef = useRef(false);
|
||||
const { isAuthenticated } = useMarketAuth();
|
||||
|
||||
// Group data from store
|
||||
const currentGroup = useAgentGroupStore(agentGroupSelectors.currentGroup);
|
||||
const currentGroupMeta = useAgentGroupStore(agentGroupSelectors.currentGroupMeta, isEqual);
|
||||
const currentGroupConfig = useAgentGroupStore(agentGroupSelectors.currentGroupConfig, isEqual);
|
||||
const currentGroupAgents = useAgentGroupStore(agentGroupSelectors.currentGroupAgents);
|
||||
const updateGroupMeta = useAgentGroupStore((s) => s.updateGroupMeta);
|
||||
const language = useGlobalStore(globalGeneralSelectors.currentLanguage);
|
||||
|
||||
const isSubmit = action === 'submit';
|
||||
|
||||
/**
|
||||
* Check ownership before publishing
|
||||
* Returns whether fork confirmation is needed and original group info
|
||||
*/
|
||||
const checkOwnership = useCallback(async (): Promise<CheckOwnershipResult> => {
|
||||
// marketIdentifier is stored in editorData
|
||||
const identifier = currentGroup?.editorData?.marketIdentifier as string | undefined;
|
||||
|
||||
// No identifier means new group, no need to check
|
||||
if (!identifier) {
|
||||
return { needsForkConfirm: false, originalGroup: null };
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCheckingOwnership(true);
|
||||
const result = await lambdaClient.market.agentGroup.checkOwnership.query({ identifier });
|
||||
|
||||
// If group doesn't exist or user is owner, no confirmation needed
|
||||
if (!result.exists || result.isOwner) {
|
||||
return { needsForkConfirm: false, originalGroup: null };
|
||||
}
|
||||
|
||||
// User is not owner, need fork confirmation
|
||||
return {
|
||||
needsForkConfirm: true,
|
||||
originalGroup: result.originalGroup as OriginalGroupInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[useMarketGroupPublish] Failed to check ownership:', error);
|
||||
// On error, proceed without confirmation
|
||||
return { needsForkConfirm: false, originalGroup: null };
|
||||
} finally {
|
||||
setIsCheckingOwnership(false);
|
||||
}
|
||||
}, [currentGroup]);
|
||||
|
||||
const publish = useCallback(async () => {
|
||||
// Prevent duplicate publishing
|
||||
if (isPublishingRef.current) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
if (!isAuthenticated) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
if (!currentGroup) {
|
||||
message.error({ content: t('marketPublish.modal.messages.noGroup') });
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const messageKey = isSubmit ? 'submit-group' : 'upload-group-version';
|
||||
const loadingMessage = isSubmit
|
||||
? t('marketPublish.modal.loading.submitGroup')
|
||||
: t('marketPublish.modal.loading.uploadGroup');
|
||||
|
||||
const changelog = generateDefaultChangelog();
|
||||
|
||||
try {
|
||||
isPublishingRef.current = true;
|
||||
setIsPublishing(true);
|
||||
message.loading({ content: loadingMessage, key: messageKey });
|
||||
|
||||
// Prepare member agents data
|
||||
const memberAgents = currentGroupAgents.map((agent, index) => ({
|
||||
// Only include avatar if it's not null/undefined
|
||||
...(agent.avatar ? { avatar: agent.avatar } : {}),
|
||||
config: {
|
||||
// Include agent configuration
|
||||
model: agent.model,
|
||||
params: agent.params,
|
||||
systemRole: agent.systemRole,
|
||||
},
|
||||
// Market requires at least 1 character for description
|
||||
description: agent.description || 'No description provided',
|
||||
displayOrder: index,
|
||||
identifier: agent.id, // Use local agent ID as identifier
|
||||
name: agent.title || 'Untitled Agent',
|
||||
role: agent.isSupervisor ? ('supervisor' as const) : ('participant' as const),
|
||||
// TODO: Construct proper A2A URL for the agent
|
||||
url: `https://api.lobehub.com/a2a/agents/${agent.id}`,
|
||||
}));
|
||||
|
||||
// Use tRPC publishOrCreate
|
||||
const result = await lambdaClient.market.agentGroup.publishOrCreate.mutate({
|
||||
// Only include avatar if it's not null/undefined
|
||||
...(currentGroupMeta.avatar ? { avatar: currentGroupMeta.avatar } : {}),
|
||||
// Only include backgroundColor if it's not null/undefined
|
||||
...(currentGroup.backgroundColor ? { backgroundColor: currentGroup.backgroundColor } : {}),
|
||||
category: 'productivity', // TODO: Allow user to select category
|
||||
changelog,
|
||||
// Include group-level config (systemPrompt from content, openingMessage, etc.)
|
||||
config: {
|
||||
// Group systemPrompt is stored in currentGroup.content
|
||||
...(currentGroup.content !== undefined &&
|
||||
currentGroup.content !== null && {
|
||||
systemPrompt: currentGroup.content,
|
||||
}),
|
||||
...(currentGroupConfig.openingMessage !== undefined && {
|
||||
openingMessage: currentGroupConfig.openingMessage,
|
||||
}),
|
||||
...(currentGroupConfig.openingQuestions !== undefined &&
|
||||
currentGroupConfig.openingQuestions.length > 0 && {
|
||||
openingQuestions: currentGroupConfig.openingQuestions,
|
||||
}),
|
||||
...(currentGroupConfig.allowDM !== undefined && { allowDM: currentGroupConfig.allowDM }),
|
||||
...(currentGroupConfig.revealDM !== undefined && { revealDM: currentGroupConfig.revealDM }),
|
||||
},
|
||||
// Market requires at least 1 character for description
|
||||
description: currentGroupMeta.description || 'No description provided',
|
||||
// marketIdentifier is stored in editorData
|
||||
identifier: currentGroup.editorData?.marketIdentifier as string | undefined,
|
||||
memberAgents,
|
||||
name: currentGroupMeta.title || 'Untitled Group',
|
||||
visibility: 'public', // TODO: Allow user to select visibility
|
||||
});
|
||||
|
||||
// Save marketIdentifier to editorData if new group
|
||||
if (result.isNewGroup) {
|
||||
await updateGroupMeta({
|
||||
editorData: {
|
||||
...currentGroup.editorData,
|
||||
marketIdentifier: result.identifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
message.success({
|
||||
content: t('submitAgentModal.success'),
|
||||
key: messageKey,
|
||||
});
|
||||
|
||||
onSuccess?.(result.identifier);
|
||||
return { identifier: result.identifier, success: true };
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : t('unknownError', { ns: 'common' });
|
||||
message.error({
|
||||
content: t('marketPublish.modal.messages.publishFailed', {
|
||||
message: errorMessage,
|
||||
}),
|
||||
key: messageKey,
|
||||
});
|
||||
return { success: false };
|
||||
} finally {
|
||||
isPublishingRef.current = false;
|
||||
setIsPublishing(false);
|
||||
}
|
||||
}, [
|
||||
currentGroup,
|
||||
currentGroupAgents,
|
||||
currentGroupConfig,
|
||||
currentGroupMeta,
|
||||
isAuthenticated,
|
||||
isSubmit,
|
||||
language,
|
||||
onSuccess,
|
||||
t,
|
||||
updateGroupMeta,
|
||||
]);
|
||||
|
||||
return {
|
||||
checkOwnership,
|
||||
isCheckingOwnership,
|
||||
isPublishing,
|
||||
publish,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { customAlphabet } from 'nanoid/non-secure';
|
||||
|
||||
/**
|
||||
* Generate a market identifier (8-character lowercase alphanumeric string)
|
||||
* Format: [a-z0-9]{8}
|
||||
* @returns A unique 8-character market identifier
|
||||
*/
|
||||
export const generateMarketIdentifier = () => {
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
const generate = customAlphabet(alphabet, 8);
|
||||
return generate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a default changelog based on current timestamp
|
||||
* @returns A timestamp-based changelog string
|
||||
*/
|
||||
export const generateDefaultChangelog = () => {
|
||||
const now = new Date();
|
||||
const formattedDate = now.toISOString().split('T')[0];
|
||||
return `Release ${formattedDate}`;
|
||||
};
|
||||
@@ -168,6 +168,13 @@ export const desktopRoutes: RouteConfig[] = [
|
||||
),
|
||||
path: 'assistant/:slug',
|
||||
},
|
||||
{
|
||||
element: dynamicElement(
|
||||
() => import('../(main)/community/(detail)/group_agent'),
|
||||
'Desktop > Discover > Detail > Group Agent',
|
||||
),
|
||||
path: 'group_agent/:slug',
|
||||
},
|
||||
{
|
||||
element: dynamicElement(
|
||||
() => import('../(main)/community/(detail)/model'),
|
||||
|
||||
@@ -187,9 +187,13 @@ export default {
|
||||
'llm.waitingForMoreLinkAriaLabel': 'Open the Provider request form',
|
||||
'marketPublish.forkConfirm.by': 'by {{author}}',
|
||||
'marketPublish.forkConfirm.confirm': 'Confirm Publish',
|
||||
'marketPublish.forkConfirm.confirmGroup': 'Confirm Publish',
|
||||
'marketPublish.forkConfirm.description':
|
||||
'You are about to publish a derivative version based on an existing agent from the community. Your new agent will be created as a separate entry in the marketplace.',
|
||||
'marketPublish.forkConfirm.descriptionGroup':
|
||||
'You are about to publish a derivative version based on an existing group from the community. Your new group will be created as a separate entry in the marketplace.',
|
||||
'marketPublish.forkConfirm.title': 'Publish Derivative Agent',
|
||||
'marketPublish.forkConfirm.titleGroup': 'Publish Derivative Group',
|
||||
'marketPublish.modal.changelog.extra':
|
||||
'Describe the key changes and improvements in this version',
|
||||
'marketPublish.modal.changelog.label': 'Changelog',
|
||||
@@ -209,11 +213,14 @@ export default {
|
||||
'marketPublish.modal.identifier.required': 'Please enter the agent identifier',
|
||||
'marketPublish.modal.loading.fetchingRemote': 'Loading remote data...',
|
||||
'marketPublish.modal.loading.submit': 'Submitting Agent...',
|
||||
'marketPublish.modal.loading.submitGroup': 'Submitting Group...',
|
||||
'marketPublish.modal.loading.upload': 'Publishing new version...',
|
||||
'marketPublish.modal.loading.uploadGroup': 'Publishing new group version...',
|
||||
'marketPublish.modal.messages.createVersionFailed': 'Failed to create version: {{message}}',
|
||||
'marketPublish.modal.messages.fetchRemoteFailed': 'Failed to fetch remote agent data',
|
||||
'marketPublish.modal.messages.missingIdentifier':
|
||||
'This Agent doesn’t have a Community identifier yet.',
|
||||
'marketPublish.modal.messages.noGroup': 'No group selected',
|
||||
'marketPublish.modal.messages.notAuthenticated': 'Sign in to your Community account first.',
|
||||
'marketPublish.modal.messages.publishFailed': 'Publish failed: {{message}}',
|
||||
'marketPublish.modal.submitButton': 'Publish',
|
||||
@@ -221,12 +228,16 @@ export default {
|
||||
'marketPublish.modal.title.upload': 'Publish New Version',
|
||||
'marketPublish.resultModal.message':
|
||||
'Your Agent has been submitted for review. Once approved, it will go live automatically.',
|
||||
'marketPublish.resultModal.messageGroup':
|
||||
'Your Group has been submitted for review. Once approved, it will go live automatically.',
|
||||
'marketPublish.resultModal.title': 'Submission Successful',
|
||||
'marketPublish.resultModal.view': 'View in Community',
|
||||
'marketPublish.submit.button': 'Share to Community',
|
||||
'marketPublish.submit.tooltip': 'Share this Agent to the Community',
|
||||
'marketPublish.submitGroup.tooltip': 'Share this Group to the Community',
|
||||
'marketPublish.upload.button': 'Publish New Version',
|
||||
'marketPublish.upload.tooltip': 'Publish a new version to Agent Community',
|
||||
'marketPublish.uploadGroup.tooltip': 'Publish a new version to Group Community',
|
||||
'memory.enabled.desc':
|
||||
'Allow LobeHub to extract preferences and info from conversations and use them later. You can view, edit, or clear memory anytime.',
|
||||
'memory.enabled.title': 'Enable Memory',
|
||||
@@ -551,6 +562,7 @@ export default {
|
||||
'submitAgentModal.placeholder': 'Enter a unique identifier for the agent, e.g. web-development',
|
||||
'submitAgentModal.success': 'Agent submitted successfully',
|
||||
'submitAgentModal.tooltips': 'Share to Agent Community',
|
||||
'submitGroupModal.tooltips': 'Share to Group Community',
|
||||
'sync.device.deviceName.hint': 'Add a name for easy identification',
|
||||
'sync.device.deviceName.placeholder': 'Enter device name',
|
||||
'sync.device.deviceName.title': 'Device Name',
|
||||
|
||||
296
src/server/routers/lambda/market/agentGroup.ts
Normal file
296
src/server/routers/lambda/market/agentGroup.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import debug from 'debug';
|
||||
import { customAlphabet } from 'nanoid/non-secure';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { marketSDK, marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
import { type TrustedClientUserInfo, generateTrustedClientToken } from '@/libs/trusted-client';
|
||||
|
||||
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
||||
|
||||
interface MarketUserInfo {
|
||||
accountId: number;
|
||||
sub: string;
|
||||
}
|
||||
|
||||
const log = debug('lambda-router:market:agent-group');
|
||||
|
||||
/**
|
||||
* Generate a market identifier (8-character lowercase alphanumeric string)
|
||||
* Format: [a-z0-9]{8}
|
||||
*/
|
||||
const generateMarketIdentifier = () => {
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
const generate = customAlphabet(alphabet, 8);
|
||||
return generate();
|
||||
};
|
||||
|
||||
interface FetchMarketUserInfoOptions {
|
||||
accessToken?: string;
|
||||
userInfo?: TrustedClientUserInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Market user info using either trustedClientToken or accessToken
|
||||
* Returns the Market accountId which is different from LobeChat userId
|
||||
*/
|
||||
const fetchMarketUserInfo = async (
|
||||
options: FetchMarketUserInfoOptions,
|
||||
): Promise<MarketUserInfo | null> => {
|
||||
const { userInfo, accessToken } = options;
|
||||
|
||||
try {
|
||||
const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (userInfo) {
|
||||
const trustedClientToken = generateTrustedClientToken(userInfo);
|
||||
if (trustedClientToken) {
|
||||
headers['x-lobe-trust-token'] = trustedClientToken;
|
||||
log('Using trustedClientToken for user info fetch');
|
||||
}
|
||||
}
|
||||
|
||||
if (!headers['x-lobe-trust-token'] && accessToken) {
|
||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
log('Using accessToken for user info fetch');
|
||||
}
|
||||
|
||||
if (!headers['x-lobe-trust-token'] && !headers['Authorization']) {
|
||||
log('No authentication method available for fetching user info');
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(userInfoUrl, {
|
||||
headers,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
log('Failed to fetch Market user info: %s %s', response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await response.json()) as MarketUserInfo;
|
||||
} catch (error) {
|
||||
log('Error fetching Market user info: %O', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Authenticated procedure for agent group management
|
||||
const agentGroupProcedure = authedProcedure
|
||||
.use(serverDatabase)
|
||||
.use(marketUserInfo)
|
||||
.use(marketSDK)
|
||||
.use(async ({ ctx, next }) => {
|
||||
const { UserModel } = await import('@/database/models/user');
|
||||
const userModel = new UserModel(ctx.serverDB, ctx.userId);
|
||||
|
||||
let marketOidcAccessToken: string | undefined;
|
||||
try {
|
||||
const userState = await userModel.getUserState(async () => ({}));
|
||||
marketOidcAccessToken = userState.settings?.market?.accessToken;
|
||||
log('marketOidcAccessToken from DB exists=%s', !!marketOidcAccessToken);
|
||||
} catch (error) {
|
||||
log('Failed to get marketOidcAccessToken from DB: %O', error);
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
marketOidcAccessToken,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Schema definitions
|
||||
const memberAgentSchema = z.object({
|
||||
avatar: z.string().nullish(),
|
||||
category: z.string().optional(),
|
||||
config: z.record(z.any()),
|
||||
description: z.string(),
|
||||
displayOrder: z.number().optional(),
|
||||
identifier: z.string(),
|
||||
name: z.string(),
|
||||
role: z.enum(['supervisor', 'participant']),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
const publishOrCreateGroupSchema = z.object({
|
||||
avatar: z.string().nullish(),
|
||||
backgroundColor: z.string().nullish(),
|
||||
category: z.string().optional(),
|
||||
changelog: z.string().optional(),
|
||||
config: z
|
||||
.object({
|
||||
allowDM: z.boolean().optional(),
|
||||
openingMessage: z.string().optional(),
|
||||
openingQuestions: z.array(z.string()).optional(),
|
||||
revealDM: z.boolean().optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
description: z.string(),
|
||||
identifier: z.string().optional(),
|
||||
memberAgents: z.array(memberAgentSchema),
|
||||
name: z.string(),
|
||||
visibility: z.enum(['public', 'private', 'internal']).optional(),
|
||||
});
|
||||
|
||||
export const agentGroupRouter = router({
|
||||
/**
|
||||
* Check if current user owns the specified group
|
||||
*/
|
||||
checkOwnership: agentGroupProcedure
|
||||
.input(z.object({ identifier: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('checkOwnership input: %O', input);
|
||||
|
||||
try {
|
||||
const groupDetail = await ctx.marketSDK.agentGroups.getAgentGroupDetail(input.identifier);
|
||||
|
||||
if (!groupDetail) {
|
||||
return {
|
||||
exists: false,
|
||||
isOwner: false,
|
||||
originalGroup: null,
|
||||
};
|
||||
}
|
||||
|
||||
const userInfo = ctx.marketUserInfo as TrustedClientUserInfo | undefined;
|
||||
const accessToken = (ctx as { marketOidcAccessToken?: string }).marketOidcAccessToken;
|
||||
let currentAccountId: number | null = null;
|
||||
|
||||
const marketUserInfoResult = await fetchMarketUserInfo({ accessToken, userInfo });
|
||||
currentAccountId = marketUserInfoResult?.accountId ?? null;
|
||||
|
||||
const ownerId = groupDetail.group.ownerId;
|
||||
const isOwner = currentAccountId !== null && `${ownerId}` === `${currentAccountId}`;
|
||||
|
||||
log(
|
||||
'checkOwnership result: isOwner=%s, currentAccountId=%s, ownerId=%s',
|
||||
isOwner,
|
||||
currentAccountId,
|
||||
ownerId,
|
||||
);
|
||||
|
||||
return {
|
||||
exists: true,
|
||||
isOwner,
|
||||
originalGroup: isOwner
|
||||
? null
|
||||
: {
|
||||
// TODO: Add author info from group detail
|
||||
author: undefined,
|
||||
avatar: groupDetail.group.avatar,
|
||||
identifier: groupDetail.group.identifier,
|
||||
name: groupDetail.group.name,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
log('Error checking ownership: %O', error);
|
||||
return {
|
||||
exists: false,
|
||||
isOwner: false,
|
||||
originalGroup: null,
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Unified publish or create agent group flow
|
||||
* 1. Check if identifier exists and if current user is owner
|
||||
* 2. If not owner or no identifier, create new group
|
||||
* 3. Create new version for the group if updating
|
||||
*/
|
||||
publishOrCreate: agentGroupProcedure
|
||||
.input(publishOrCreateGroupSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('publishOrCreate input: %O', input);
|
||||
|
||||
const { identifier: inputIdentifier, name, memberAgents, ...groupData } = input;
|
||||
let finalIdentifier = inputIdentifier;
|
||||
let isNewGroup = false;
|
||||
|
||||
try {
|
||||
// Step 1: Check ownership if identifier is provided
|
||||
if (inputIdentifier) {
|
||||
try {
|
||||
const groupDetail =
|
||||
await ctx.marketSDK.agentGroups.getAgentGroupDetail(inputIdentifier);
|
||||
log('Group detail for ownership check: ownerId=%s', groupDetail?.group.ownerId);
|
||||
|
||||
const userInfo = ctx.marketUserInfo as TrustedClientUserInfo | undefined;
|
||||
const accessToken = (ctx as { marketOidcAccessToken?: string }).marketOidcAccessToken;
|
||||
let currentAccountId: number | null = null;
|
||||
|
||||
const marketUserInfoResult = await fetchMarketUserInfo({ accessToken, userInfo });
|
||||
currentAccountId = marketUserInfoResult?.accountId ?? null;
|
||||
log('Market user info: accountId=%s', currentAccountId);
|
||||
|
||||
const ownerId = groupDetail?.group.ownerId;
|
||||
|
||||
log('Ownership check: currentAccountId=%s, ownerId=%s', currentAccountId, ownerId);
|
||||
|
||||
if (!currentAccountId || `${ownerId}` !== `${currentAccountId}`) {
|
||||
// Not the owner, need to create a new group
|
||||
log('User is not owner, will create new group');
|
||||
finalIdentifier = undefined;
|
||||
isNewGroup = true;
|
||||
}
|
||||
} catch (detailError) {
|
||||
// Group not found or error, create new
|
||||
log('Group not found or error, will create new: %O', detailError);
|
||||
finalIdentifier = undefined;
|
||||
isNewGroup = true;
|
||||
}
|
||||
} else {
|
||||
isNewGroup = true;
|
||||
}
|
||||
|
||||
// Step 2: Create new group or update existing
|
||||
if (!finalIdentifier || isNewGroup) {
|
||||
// Generate a unique 8-character identifier
|
||||
finalIdentifier = generateMarketIdentifier();
|
||||
isNewGroup = true;
|
||||
|
||||
log('Creating new group with identifier: %s', finalIdentifier);
|
||||
|
||||
await ctx.marketSDK.agentGroups.createAgentGroup({
|
||||
...groupData,
|
||||
identifier: finalIdentifier,
|
||||
// @ts-ignore
|
||||
memberAgents,
|
||||
name,
|
||||
});
|
||||
} else {
|
||||
// Update existing group - create new version
|
||||
log('Creating new version for group: %s', finalIdentifier);
|
||||
|
||||
await ctx.marketSDK.agentGroups.createAgentGroupVersion({
|
||||
...groupData,
|
||||
identifier: finalIdentifier,
|
||||
// @ts-ignore
|
||||
memberAgents,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: finalIdentifier,
|
||||
isNewGroup,
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
log('Error in publishOrCreate: %O', error);
|
||||
throw new TRPCError({
|
||||
cause: error,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Failed to publish group',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/types/discover';
|
||||
|
||||
import { agentRouter } from './agent';
|
||||
import { agentGroupRouter } from './agentGroup';
|
||||
import { oidcRouter } from './oidc';
|
||||
import { socialRouter } from './social';
|
||||
import { userRouter } from './user';
|
||||
@@ -44,6 +45,9 @@ export const marketRouter = router({
|
||||
// ============================== Agent Management (authenticated) ==============================
|
||||
agent: agentRouter,
|
||||
|
||||
// ============================== Agent Group Management (authenticated) ==============================
|
||||
agentGroup: agentGroupRouter,
|
||||
|
||||
// ============================== Assistant Market ==============================
|
||||
getAssistantCategories: marketProcedure
|
||||
.input(
|
||||
@@ -120,6 +124,7 @@ export const marketRouter = router({
|
||||
.object({
|
||||
category: z.string().optional(),
|
||||
connectionType: z.nativeEnum(McpConnectionType).optional(),
|
||||
includeAgentGroup: z.boolean().optional(),
|
||||
locale: z.string().optional(),
|
||||
order: z.enum(['asc', 'desc']).optional(),
|
||||
ownerId: z.string().optional(),
|
||||
@@ -145,6 +150,95 @@ export const marketRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
// ============================== Group Agent Market (Discovery) ==============================
|
||||
getGroupAgentCategories: marketProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
locale: z.string().optional(),
|
||||
q: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('getGroupAgentCategories input: %O', input);
|
||||
|
||||
try {
|
||||
return await ctx.discoverService.getGroupAgentCategories(input);
|
||||
} catch (error) {
|
||||
log('Error fetching group agent categories: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to fetch group agent categories',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getGroupAgentDetail: marketProcedure
|
||||
.input(
|
||||
z.object({
|
||||
identifier: z.string(),
|
||||
locale: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('getGroupAgentDetail input: %O', input);
|
||||
|
||||
try {
|
||||
return await ctx.discoverService.getGroupAgentDetail(input);
|
||||
} catch (error) {
|
||||
log('Error fetching group agent detail: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to fetch group agent detail',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getGroupAgentIdentifiers: marketProcedure.query(async ({ ctx }) => {
|
||||
log('getGroupAgentIdentifiers called');
|
||||
|
||||
try {
|
||||
return await ctx.discoverService.getGroupAgentIdentifiers();
|
||||
} catch (error) {
|
||||
log('Error fetching group agent identifiers: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to fetch group agent identifiers',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getGroupAgentList: marketProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
category: z.string().optional(),
|
||||
locale: z.string().optional(),
|
||||
order: z.enum(['asc', 'desc']).optional(),
|
||||
ownerId: z.string().optional(),
|
||||
page: z.number().optional(),
|
||||
pageSize: z.number().optional(),
|
||||
q: z.string().optional(),
|
||||
sort: z.enum(['createdAt', 'updatedAt', 'name', 'recommended']).optional(),
|
||||
})
|
||||
.optional(),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
log('getGroupAgentList input: %O', input);
|
||||
|
||||
try {
|
||||
return await ctx.discoverService.getGroupAgentList(input);
|
||||
} catch (error) {
|
||||
log('Error fetching group agent list: %O', error);
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to fetch group agent list',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getLegacyPluginList: marketProcedure
|
||||
.input(
|
||||
z
|
||||
@@ -167,7 +261,7 @@ export const marketRouter = router({
|
||||
}),
|
||||
|
||||
// ============================== MCP Market ==============================
|
||||
getMcpCategories: marketProcedure
|
||||
getMcpCategories: marketProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@@ -351,7 +445,7 @@ getMcpCategories: marketProcedure
|
||||
}),
|
||||
|
||||
// ============================== Plugin Market ==============================
|
||||
getPluginCategories: marketProcedure
|
||||
getPluginCategories: marketProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@@ -439,7 +533,7 @@ getPluginCategories: marketProcedure
|
||||
}),
|
||||
|
||||
// ============================== Providers ==============================
|
||||
getProviderDetail: marketProcedure
|
||||
getProviderDetail: marketProcedure
|
||||
.input(
|
||||
z.object({
|
||||
identifier: z.string(),
|
||||
@@ -503,7 +597,7 @@ getProviderDetail: marketProcedure
|
||||
}),
|
||||
|
||||
// ============================== User Profile ==============================
|
||||
getUserInfo: marketProcedure
|
||||
getUserInfo: marketProcedure
|
||||
.input(
|
||||
z.object({
|
||||
locale: z.string().optional(),
|
||||
@@ -675,6 +769,42 @@ getUserInfo: marketProcedure
|
||||
}
|
||||
}),
|
||||
|
||||
reportGroupAgentEvent: marketProcedure
|
||||
.input(
|
||||
z.object({
|
||||
event: z.enum(['add', 'chat', 'click']),
|
||||
identifier: z.string(),
|
||||
source: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('reportGroupAgentEvent input: %O', input);
|
||||
|
||||
try {
|
||||
await ctx.discoverService.createGroupAgentEvent(input);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error reporting Group Agent event: %O', error);
|
||||
return { success: false };
|
||||
}
|
||||
}),
|
||||
|
||||
reportGroupAgentInstall: marketProcedure
|
||||
.input(
|
||||
z.object({
|
||||
identifier: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
log('reportGroupAgentInstall input: %O', input);
|
||||
try {
|
||||
await ctx.discoverService.increaseGroupAgentInstallCount(input.identifier);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
log('Error reporting group agent installation: %O', error);
|
||||
return { success: false };
|
||||
}
|
||||
}),
|
||||
|
||||
reportMcpEvent: marketProcedure
|
||||
.input(
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
type ModelQueryParams,
|
||||
ModelSorts,
|
||||
type PluginListResponse,
|
||||
type PluginQueryParams as PluginQueryParams,
|
||||
type PluginQueryParams,
|
||||
PluginSorts,
|
||||
type ProviderListResponse,
|
||||
type ProviderQueryParams,
|
||||
@@ -718,6 +718,7 @@ export class DiscoverService {
|
||||
q,
|
||||
sort = AssistantSorts.CreatedAt,
|
||||
ownerId,
|
||||
includeAgentGroup,
|
||||
} = rest;
|
||||
|
||||
try {
|
||||
@@ -740,9 +741,10 @@ export class DiscoverService {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const data = await this.market.agents.getAgentList({
|
||||
category,
|
||||
// includeAgentGroup may not be in SDK type definition yet, using 'as any'
|
||||
includeAgentGroup,
|
||||
locale: normalizedLocale,
|
||||
order,
|
||||
ownerId,
|
||||
@@ -752,7 +754,7 @@ export class DiscoverService {
|
||||
sort: apiSort,
|
||||
status: 'published',
|
||||
visibility: 'public',
|
||||
});
|
||||
} as any);
|
||||
|
||||
const transformedItems: DiscoverAssistantItem[] = (data.items || []).map((item: any) => {
|
||||
const normalizedAuthor = this.normalizeAuthorField(item.author);
|
||||
@@ -773,6 +775,7 @@ export class DiscoverService {
|
||||
tags: item.tags || [],
|
||||
title: item.name || item.identifier,
|
||||
tokenUsage: item.tokenUsage || 0,
|
||||
type: item.type,
|
||||
userName: normalizedAuthor.userName,
|
||||
};
|
||||
});
|
||||
@@ -922,7 +925,6 @@ export class DiscoverService {
|
||||
await this.market.plugins.createEvent(params);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* report plugin call result to marketplace
|
||||
*/
|
||||
@@ -1735,14 +1737,18 @@ export class DiscoverService {
|
||||
|
||||
try {
|
||||
// Call Market SDK to get user info
|
||||
const response: UserInfoResponse = await this.market.user.getUserInfo(username, { locale });
|
||||
const response = (await this.market.user.getUserInfo(username, {
|
||||
locale,
|
||||
})) as UserInfoResponse & {
|
||||
agentGroups?: any[];
|
||||
};
|
||||
|
||||
if (!response?.user) {
|
||||
log('getUserInfo: user not found for username=%s', username);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { user, agents } = response;
|
||||
const { user, agents, agentGroups } = response;
|
||||
|
||||
// Transform agents to DiscoverAssistantItem format
|
||||
const transformedAgents: DiscoverAssistantItem[] = (agents || []).map((agent: any) => ({
|
||||
@@ -1763,7 +1769,27 @@ export class DiscoverService {
|
||||
tokenUsage: agent.tokenUsage || 0,
|
||||
}));
|
||||
|
||||
// Transform agentGroups to DiscoverGroupAgentItem format
|
||||
const transformedAgentGroups = (agentGroups || []).map((group: any) => ({
|
||||
author: user.displayName || user.userName || user.namespace || '',
|
||||
avatar: group.avatar || '👥',
|
||||
category: group.category as any,
|
||||
createdAt: group.createdAt,
|
||||
description: group.description || '',
|
||||
homepage: `https://lobehub.com/discover/group_agent/${group.identifier}`,
|
||||
identifier: group.identifier,
|
||||
installCount: group.installCount || 0,
|
||||
isFeatured: group.isFeatured || false,
|
||||
isOfficial: group.isOfficial || false,
|
||||
memberCount: 0, // Will be populated from memberAgents in detail view
|
||||
schemaVersion: 1,
|
||||
tags: group.tags || [],
|
||||
title: group.name || group.identifier,
|
||||
updatedAt: group.updatedAt,
|
||||
}));
|
||||
|
||||
const result: DiscoverUserProfile = {
|
||||
agentGroups: transformedAgentGroups,
|
||||
agents: transformedAgents,
|
||||
user: {
|
||||
avatarUrl: user.avatarUrl || null,
|
||||
@@ -1781,11 +1807,101 @@ export class DiscoverService {
|
||||
},
|
||||
};
|
||||
|
||||
log('getUserInfo: returning user profile with %d agents', result.agents.length);
|
||||
log(
|
||||
'getUserInfo: returning user profile with %d agents and %d groups',
|
||||
result.agents.length,
|
||||
result.agentGroups?.length || 0,
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
log('getUserInfo: error fetching user info: %O', error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================== Group Agent Market Methods ==============================
|
||||
|
||||
getGroupAgentCategories = async (params?: CategoryListQuery) => {
|
||||
try {
|
||||
// TODO: SDK method not yet available, using fallback
|
||||
const response = await (this.market.agentGroups as any).getAgentGroupCategories?.(params);
|
||||
return response || { items: [] };
|
||||
} catch (error) {
|
||||
log('getGroupAgentCategories: error: %O', error);
|
||||
return { items: [] };
|
||||
}
|
||||
};
|
||||
|
||||
getGroupAgentDetail = async (params: {
|
||||
identifier: string;
|
||||
locale?: string;
|
||||
version?: string;
|
||||
}) => {
|
||||
try {
|
||||
const response = await this.market.agentGroups.getAgentGroupDetail(params.identifier, {
|
||||
locale: params.locale,
|
||||
version: params.version ? Number(params.version) : undefined,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
log('getGroupAgentDetail: error: %O', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getGroupAgentIdentifiers = async () => {
|
||||
try {
|
||||
// TODO: SDK method not yet available, using fallback
|
||||
const response = await (this.market.agentGroups as any).getAgentGroupIdentifiers?.();
|
||||
return response || { identifiers: [] };
|
||||
} catch (error) {
|
||||
log('getGroupAgentIdentifiers: error: %O', error);
|
||||
return { identifiers: [] };
|
||||
}
|
||||
};
|
||||
|
||||
getGroupAgentList = async (params?: {
|
||||
category?: string;
|
||||
locale?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
ownerId?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
q?: string;
|
||||
sort?: 'createdAt' | 'updatedAt' | 'name' | 'recommended';
|
||||
}) => {
|
||||
try {
|
||||
const response = await this.market.agentGroups.getAgentGroupList({
|
||||
...params,
|
||||
status: 'published' as any,
|
||||
visibility: 'public' as any,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
log('getGroupAgentList: error: %O', error);
|
||||
return { currentPage: 1, items: [], totalCount: 0, totalPages: 1 };
|
||||
}
|
||||
};
|
||||
|
||||
createGroupAgentEvent = async (params: {
|
||||
event: 'add' | 'chat' | 'click';
|
||||
identifier: string;
|
||||
source?: string;
|
||||
}) => {
|
||||
try {
|
||||
// TODO: SDK method not yet available
|
||||
await (this.market.agentGroups as any).createAgentGroupEvent?.(params);
|
||||
} catch (error) {
|
||||
log('createGroupAgentEvent: error: %O', error);
|
||||
}
|
||||
};
|
||||
|
||||
increaseGroupAgentInstallCount = async (identifier: string) => {
|
||||
try {
|
||||
// TODO: SDK method not yet available
|
||||
await (this.market.agentGroups as any).increaseInstallCount?.(identifier);
|
||||
} catch (error) {
|
||||
log('increaseGroupAgentInstallCount: error: %O', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ import {
|
||||
type AssistantMarketSource,
|
||||
type AssistantQueryParams,
|
||||
type DiscoverAssistantDetail,
|
||||
type DiscoverGroupAgentDetail,
|
||||
type DiscoverMcpDetail,
|
||||
type DiscoverModelDetail,
|
||||
type DiscoverPluginDetail,
|
||||
type DiscoverProviderDetail,
|
||||
type DiscoverUserProfile,
|
||||
type GroupAgentListResponse,
|
||||
type GroupAgentQueryParams,
|
||||
type IdentifiersResponse,
|
||||
type McpListResponse,
|
||||
type McpQueryParams,
|
||||
@@ -448,6 +451,60 @@ class DiscoverService {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================== Group Agent Market ==============================
|
||||
|
||||
getGroupAgentCategories = async (params: CategoryListQuery = {}): Promise<CategoryItem[]> => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
return lambdaClient.market.getGroupAgentCategories.query({
|
||||
...params,
|
||||
locale,
|
||||
});
|
||||
};
|
||||
|
||||
getGroupAgentDetail = async (params: {
|
||||
identifier: string;
|
||||
locale?: string;
|
||||
version?: string;
|
||||
}): Promise<any> => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
return lambdaClient.market.getGroupAgentDetail.query({
|
||||
identifier: params.identifier,
|
||||
locale,
|
||||
version: params.version,
|
||||
});
|
||||
};
|
||||
|
||||
getGroupAgentIdentifiers = async (): Promise<IdentifiersResponse> => {
|
||||
return lambdaClient.market.getGroupAgentIdentifiers.query();
|
||||
};
|
||||
|
||||
getGroupAgentList = async (
|
||||
params: GroupAgentQueryParams = {},
|
||||
): Promise<any> => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
return lambdaClient.market.getGroupAgentList.query(
|
||||
{
|
||||
...params,
|
||||
locale,
|
||||
page: params.page ? Number(params.page) : 1,
|
||||
pageSize: params.pageSize ? Number(params.pageSize) : 20,
|
||||
},
|
||||
{ context: { showNotification: false } },
|
||||
);
|
||||
};
|
||||
|
||||
reportGroupAgentEvent = async (params: {
|
||||
event: 'add' | 'chat' | 'click';
|
||||
identifier: string;
|
||||
source?: string;
|
||||
}): Promise<void> => {
|
||||
await lambdaClient.market.reportGroupAgentEvent.mutate(params);
|
||||
};
|
||||
|
||||
reportGroupAgentInstall = async (identifier: string): Promise<void> => {
|
||||
await lambdaClient.market.reportGroupAgentInstall.mutate({ identifier });
|
||||
};
|
||||
}
|
||||
|
||||
export const discoverService = new DiscoverService();
|
||||
|
||||
80
src/store/discover/slices/groupAgent/action.ts
Normal file
80
src/store/discover/slices/groupAgent/action.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { type CategoryItem, type CategoryListQuery } from '@lobehub/market-sdk';
|
||||
import useSWR, { type SWRResponse } from 'swr';
|
||||
import type { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { discoverService } from '@/services/discover';
|
||||
import { type DiscoverStore } from '@/store/discover';
|
||||
import { globalHelpers } from '@/store/global/helpers';
|
||||
import {
|
||||
type DiscoverGroupAgentDetail,
|
||||
type GroupAgentListResponse,
|
||||
type GroupAgentQueryParams,
|
||||
type IdentifiersResponse,
|
||||
} from '@/types/discover';
|
||||
|
||||
export interface GroupAgentAction {
|
||||
useGroupAgentCategories: (params?: CategoryListQuery) => SWRResponse<CategoryItem[]>;
|
||||
useGroupAgentDetail: (params: {
|
||||
identifier: string;
|
||||
version?: string;
|
||||
}) => SWRResponse<DiscoverGroupAgentDetail | undefined>;
|
||||
useGroupAgentIdentifiers: () => SWRResponse<IdentifiersResponse>;
|
||||
useGroupAgentList: (params?: GroupAgentQueryParams) => SWRResponse<GroupAgentListResponse>;
|
||||
}
|
||||
|
||||
export const createGroupAgentSlice: StateCreator<
|
||||
DiscoverStore,
|
||||
[['zustand/devtools', never]],
|
||||
[],
|
||||
GroupAgentAction
|
||||
> = () => ({
|
||||
useGroupAgentCategories: (params = {}) => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
return useSWR(
|
||||
['group-agent-categories', locale, ...Object.values(params)].filter(Boolean).join('-'),
|
||||
async () => discoverService.getGroupAgentCategories(params),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
useGroupAgentDetail: (params) => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
return useSWR(
|
||||
['group-agent-details', locale, params.identifier, params.version]
|
||||
.filter(Boolean)
|
||||
.join('-'),
|
||||
async () => discoverService.getGroupAgentDetail(params),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
useGroupAgentIdentifiers: () => {
|
||||
return useSWR(
|
||||
'group-agent-identifiers',
|
||||
async () => discoverService.getGroupAgentIdentifiers(),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
useGroupAgentList: (params = {}) => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
return useSWR(
|
||||
['group-agent-list', locale, ...Object.values(params)].filter(Boolean).join('-'),
|
||||
async () =>
|
||||
discoverService.getGroupAgentList({
|
||||
...params,
|
||||
page: params.page ? Number(params.page) : 1,
|
||||
pageSize: params.pageSize ? Number(params.pageSize) : 20,
|
||||
}),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { type StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { createDevtools } from '../middleware/createDevtools';
|
||||
import { type AssistantAction, createAssistantSlice } from './slices/assistant/action';
|
||||
import { type GroupAgentAction, createGroupAgentSlice } from './slices/groupAgent/action';
|
||||
import { type MCPAction, createMCPSlice } from './slices/mcp';
|
||||
import { type ModelAction, createModelSlice } from './slices/model/action';
|
||||
import { type PluginAction, createPluginSlice } from './slices/plugin/action';
|
||||
@@ -15,6 +16,7 @@ import { type UserAction, createUserSlice } from './slices/user';
|
||||
|
||||
export type DiscoverStore = MCPAction &
|
||||
AssistantAction &
|
||||
GroupAgentAction &
|
||||
ProviderAction &
|
||||
ModelAction &
|
||||
PluginAction &
|
||||
@@ -26,6 +28,7 @@ const createStore: StateCreator<DiscoverStore, [['zustand/devtools', never]]> =
|
||||
) => ({
|
||||
...createMCPSlice(...parameters),
|
||||
...createAssistantSlice(...parameters),
|
||||
...createGroupAgentSlice(...parameters),
|
||||
...createProviderSlice(...parameters),
|
||||
...createModelSlice(...parameters),
|
||||
...createPluginSlice(...parameters),
|
||||
|
||||
Reference in New Issue
Block a user