feat: add the agent/group profiles page the states and forked by tag (#11784)

* feat: add the agent/group profiles page the states and forked by tag

* fix: delete console.log

* feat: inject the marketAccessToken in ctx midddleware
This commit is contained in:
Shinji-Li
2026-01-24 22:22:56 +08:00
committed by GitHub
parent 63e1ddd34c
commit 1458100e64
11 changed files with 406 additions and 32 deletions

View File

@@ -8,6 +8,7 @@ export interface LobeChatGroupMetaConfig {
avatar?: string;
backgroundColor?: string;
description: string;
marketIdentifier?: string;
title: string;
}
@@ -44,6 +45,7 @@ export const InsertChatGroupSchema = z.object({
editorData: z.record(z.string(), z.any()).optional().nullable(),
groupId: z.string().optional().nullable(),
id: z.string().optional(),
marketIdentifier: z.string().optional().nullable(),
pinned: z.boolean().optional().nullable(),
title: z.string().optional().nullable(),
});
@@ -86,6 +88,7 @@ export interface NewChatGroup {
description?: string | null;
groupId?: string | null;
id?: string;
marketIdentifier?: string | null;
pinned?: boolean | null;
title?: string | null;
userId: string;
@@ -104,6 +107,7 @@ export interface ChatGroupItem {
editorData?: Record<string, any> | null;
groupId?: string | null;
id: string;
marketIdentifier?: string | null;
pinned?: boolean | null;
title?: string | null;
updatedAt: Date;

View File

@@ -0,0 +1,86 @@
'use client';
import { Icon, Tag } from '@lobehub/ui';
import { GitFork } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { marketApiService } from '@/services/marketApi';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import type { AgentForkSourceResponse } from '@/types/discover';
/**
* Agent Fork Tag Component
* Displays fork source information if the agent is forked from another agent
*/
const AgentForkTag = memo(() => {
const { t } = useTranslation('setting');
const navigate = useNavigate();
const [forkSource, setForkSource] = useState<AgentForkSourceResponse['source']>(null);
const [loading, setLoading] = useState(false);
const meta = useAgentStore(agentSelectors.currentAgentMeta);
const marketIdentifier = meta?.marketIdentifier;
useEffect(() => {
if (!marketIdentifier) {
setForkSource(null);
return;
}
const fetchAgentAndForkInfo = async () => {
try {
setLoading(true);
// Get agent detail to check if it's a fork
const agentDetail = await marketApiService.getAgentDetail(marketIdentifier);
// If forkedFromAgentId exists, get fork source info
if (agentDetail.forkedFromAgentId) {
const forkSourceResponse = await marketApiService.getAgentForkSource(marketIdentifier);
console.log('forkSourceResponse', forkSourceResponse);
setForkSource(forkSourceResponse.source);
} else {
setForkSource(null);
}
} catch (error) {
console.error('Failed to fetch agent fork info:', error);
setForkSource(null);
} finally {
setLoading(false);
}
};
fetchAgentAndForkInfo();
}, [marketIdentifier]);
if (loading || !forkSource) return null;
const handleClick = () => {
if (forkSource?.identifier) {
navigate(`/community/agent/${forkSource.identifier}`);
}
};
return (
<Tag
bordered={false}
color="default"
icon={<Icon icon={GitFork} />}
onClick={handleClick}
style={{ cursor: 'pointer', marginRight: 8 }}
title={t('marketPublish.forkFrom.tooltip', {
agent: forkSource.name,
defaultValue: `Forked from ${forkSource.name}`,
})}
>
{t('marketPublish.forkFrom.label', { defaultValue: 'Forked from' })} {forkSource.name}
</Tag>
);
});
AgentForkTag.displayName = 'AgentForkTag';
export default AgentForkTag;

View File

@@ -0,0 +1,82 @@
'use client';
import { Tag } from '@lobehub/ui';
import { memo, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { marketApiService } from '@/services/marketApi';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import type { AgentStatus } from '@/types/discover';
/**
* Agent Status Tag Component
* Displays the market status of the agent (published/unpublished/archived/deprecated)
*/
const AgentStatusTag = memo(() => {
const { t } = useTranslation('setting');
const [status, setStatus] = useState<AgentStatus | null>(null);
const [loading, setLoading] = useState(false);
const meta = useAgentStore(agentSelectors.currentAgentMeta);
const marketIdentifier = meta?.marketIdentifier;
useEffect(() => {
if (!marketIdentifier) {
setStatus(null);
return;
}
const fetchAgentStatus = async () => {
try {
setLoading(true);
const agentDetail = await marketApiService.getAgentDetail(marketIdentifier);
setStatus(agentDetail.status as AgentStatus | null);
} catch (error) {
console.error('Failed to fetch agent status:', error);
setStatus(null);
} finally {
setLoading(false);
}
};
fetchAgentStatus();
}, [marketIdentifier]);
const statusConfig = useMemo(() => {
if (!status) return null;
const configs = {
archived: {
color: 'orange',
label: t('marketPublish.status.archived', { defaultValue: 'Archived' }),
},
deprecated: {
color: 'red',
label: t('marketPublish.status.deprecated', { defaultValue: 'Deprecated' }),
},
published: {
color: 'green',
label: t('marketPublish.status.published', { defaultValue: 'Published' }),
},
unpublished: {
color: 'default',
label: t('marketPublish.status.unpublished', { defaultValue: 'Unpublished' }),
},
};
return configs[status];
}, [status, t]);
if (loading || !statusConfig) return null;
return (
<Tag bordered={false} color={statusConfig.color} style={{ marginRight: 8 }}>
{statusConfig.label}
</Tag>
);
});
AgentStatusTag.displayName = 'AgentStatusTag';
export default AgentStatusTag;

View File

@@ -17,6 +17,9 @@ import { useFileStore } from '@/store/file';
import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
import AgentForkTag from '../Header/AgentForkTag';
import AgentStatusTag from '../Header/AgentStatusTag';
const MAX_AVATAR_SIZE = 1024 * 1024; // 1MB limit for server actions
const AgentHeader = memo(() => {
@@ -88,6 +91,7 @@ const AgentHeader = memo(() => {
return (
<Flexbox
gap={16}
horizontal
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
@@ -97,6 +101,7 @@ const AgentHeader = memo(() => {
cursor: 'default',
}}
>
{/* Avatar Section */}
<EmojiPicker
allowDelete={!!meta.avatar}
allowUpload
@@ -147,21 +152,28 @@ const AgentHeader = memo(() => {
size={72}
value={meta.avatar}
/>
<Input
onChange={(e) => {
setLocalTitle(e.target.value);
debouncedSaveTitle(e.target.value);
}}
placeholder={t('settingAgent.name.placeholder', { ns: 'setting' })}
style={{
fontSize: 36,
fontWeight: 600,
padding: 0,
width: '100%',
}}
value={localTitle}
variant={'borderless'}
/>
{/* Title and Tags Section */}
<Flexbox flex={1} gap={8} style={{ minWidth: 0 }}>
<Input
onChange={(e) => {
setLocalTitle(e.target.value);
debouncedSaveTitle(e.target.value);
}}
placeholder={t('settingAgent.name.placeholder', { ns: 'setting' })}
style={{
fontSize: 36,
fontWeight: 600,
padding: 0,
width: '100%',
}}
value={localTitle}
variant={'borderless'}
/>
<Flexbox gap={8} horizontal>
<AgentStatusTag />
<AgentForkTag />
</Flexbox>
</Flexbox>
</Flexbox>
);
});

View File

@@ -132,6 +132,7 @@ const ForkGroupAndChat = memo<{ mobile?: boolean }>(() => {
// Group content is the supervisor's systemRole (for backward compatibility)
content: config.systemRole || supervisorConfig?.systemRole,
...meta,
marketIdentifier: forkResult.group.identifier, // Store the new market identifier
};
// Step 5: Prepare member agents from market data

View File

@@ -0,0 +1,80 @@
'use client';
import { Icon, Tag } from '@lobehub/ui';
import { GitFork } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { marketApiService } from '@/services/marketApi';
import { useAgentGroupStore } from '@/store/agentGroup';
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
import type { AgentGroupForkSourceResponse } from '@/types/discover';
/**
* Group Fork Tag Component
* Displays fork source information if the group is forked from another group
*/
const GroupForkTag = memo(() => {
const { t } = useTranslation('setting');
const navigate = useNavigate();
const [forkSource, setForkSource] = useState<AgentGroupForkSourceResponse['source']>(null);
const [loading, setLoading] = useState(false);
const groupMeta = useAgentGroupStore(agentGroupSelectors.currentGroupMeta);
const marketIdentifier = groupMeta?.marketIdentifier;
useEffect(() => {
if (!marketIdentifier) {
setForkSource(null);
return;
}
const fetchGroupForkInfo = async () => {
try {
setLoading(true);
// Get fork source info from market using the marketIdentifier
const forkSourceResponse =
await marketApiService.getAgentGroupForkSource(marketIdentifier);
setForkSource(forkSourceResponse.source);
} catch (error) {
console.error('Failed to fetch group fork info:', error);
setForkSource(null);
} finally {
setLoading(false);
}
};
fetchGroupForkInfo();
}, [marketIdentifier]);
if (loading || !forkSource) return null;
const handleClick = () => {
if (forkSource?.identifier) {
navigate(`/community/group_agent/${forkSource.identifier}`);
}
};
return (
<Tag
bordered={false}
color="default"
icon={<Icon icon={GitFork} />}
onClick={handleClick}
style={{ cursor: 'pointer' }}
title={t('marketPublish.forkFrom.tooltip', {
agent: forkSource.name,
defaultValue: `Forked from ${forkSource.name}`,
})}
>
{t('marketPublish.forkFrom.label', { defaultValue: 'Forked from' })} {forkSource.name}
</Tag>
);
});
GroupForkTag.displayName = 'GroupForkTag';
export default GroupForkTag;

View File

@@ -18,6 +18,9 @@ import { useFileStore } from '@/store/file';
import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
import GroupForkTag from './GroupForkTag';
import GroupStatusTag from './GroupStatusTag';
const MAX_AVATAR_SIZE = 1024 * 1024; // 1MB limit for server actions
const GroupHeader = memo(() => {
@@ -89,6 +92,7 @@ const GroupHeader = memo(() => {
return (
<Flexbox
gap={16}
horizontal
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
@@ -98,6 +102,7 @@ const GroupHeader = memo(() => {
cursor: 'default',
}}
>
{/* Avatar Section */}
<EmojiPicker
allowDelete={!!groupMeta.avatar}
allowUpload
@@ -159,21 +164,28 @@ const GroupHeader = memo(() => {
size={72}
value={groupMeta.avatar}
/>
<Input
onChange={(e) => {
setLocalTitle(e.target.value);
debouncedSaveTitle(e.target.value);
}}
placeholder={t('name.placeholder')}
style={{
fontSize: 36,
fontWeight: 600,
padding: 0,
width: '100%',
}}
value={localTitle}
variant={'borderless'}
/>
{/* Title and Tags Section */}
<Flexbox flex={1} gap={8} style={{ minWidth: 0 }}>
<Input
onChange={(e) => {
setLocalTitle(e.target.value);
debouncedSaveTitle(e.target.value);
}}
placeholder={t('name.placeholder')}
style={{
fontSize: 36,
fontWeight: 600,
padding: 0,
width: '100%',
}}
value={localTitle}
variant={'borderless'}
/>
<Flexbox gap={8} horizontal>
<GroupStatusTag />
<GroupForkTag />
</Flexbox>
</Flexbox>
</Flexbox>
);
});

View File

@@ -0,0 +1,84 @@
'use client';
import { Tag } from '@lobehub/ui';
import { memo, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { marketApiService } from '@/services/marketApi';
import { useAgentGroupStore } from '@/store/agentGroup';
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
import type { AgentStatus } from '@/types/discover';
/**
* Group Status Tag Component
* Displays the market status of the agent group
*/
const GroupStatusTag = memo(() => {
const { t } = useTranslation('setting');
const [status, setStatus] = useState<AgentStatus | null>(null);
const [loading, setLoading] = useState(false);
const meta = useAgentGroupStore(agentGroupSelectors.currentGroupMeta);
const marketIdentifier = meta?.marketIdentifier;
useEffect(() => {
if (!marketIdentifier) {
setStatus(null);
return;
}
const fetchGroupStatus = async () => {
try {
setLoading(true);
// TODO: Use getAgentGroupDetail when available
// For now, groups might not have a separate detail endpoint
// This is a placeholder - adjust based on actual API
setStatus('published'); // Temporary: assume published if has identifier
} catch (error) {
console.error('Failed to fetch group status:', error);
setStatus(null);
} finally {
setLoading(false);
}
};
fetchGroupStatus();
}, [marketIdentifier]);
const statusConfig = useMemo(() => {
if (!status) return null;
const configs = {
archived: {
color: 'orange',
label: t('marketPublish.status.archived', { defaultValue: 'Archived' }),
},
deprecated: {
color: 'red',
label: t('marketPublish.status.deprecated', { defaultValue: 'Deprecated' }),
},
published: {
color: 'green',
label: t('marketPublish.status.published', { defaultValue: 'Published' }),
},
unpublished: {
color: 'default',
label: t('marketPublish.status.unpublished', { defaultValue: 'Unpublished' }),
},
};
return configs[status];
}, [status, t]);
if (loading || !statusConfig) return null;
return (
<Tag bordered={false} color={statusConfig.color}>
{statusConfig.label}
</Tag>
);
});
GroupStatusTag.displayName = 'GroupStatusTag';
export default GroupStatusTag;

View File

@@ -6,6 +6,7 @@ import type { TrustedClientUserInfo } from '@/libs/trusted-client';
import { trpc } from '../init';
interface ContextWithServerDB {
marketAccessToken?: string;
serverDB?: LobeChatDatabase;
userId?: string | null;
}
@@ -39,8 +40,19 @@ export const marketUserInfo = trpc.middleware(async (opts) => {
userId: ctx.userId,
};
// Fetch market access token from user_settings.market
const userModel = new UserModel(ctx.serverDB, ctx.userId);
const userSettings = await userModel.getUserSettings();
const marketTokenFromDB = (userSettings?.market as any)?.accessToken;
// Prioritize database token over cookie token
const marketAccessToken = marketTokenFromDB || ctx.marketAccessToken;
return opts.next({
ctx: { marketUserInfo },
ctx: {
marketAccessToken,
marketUserInfo,
},
});
} catch {
// If fetching user info fails, continue without it

View File

@@ -45,7 +45,7 @@ export class MarketApiService {
}
// Get agent detail by identifier
async getAgentDetail(identifier: string): Promise<AgentItemDetail> {
async getAgentDetail(identifier: string): Promise<AgentItemDetail & { forkedFromAgentId?: string }> {
return lambdaClient.market.agent.getAgentDetail.query({
identifier,
}) as Promise<AgentItemDetail>;

View File

@@ -22,6 +22,7 @@ const groupMeta = (groupId: string) => (s: ChatGroupStore) => {
avatar: group?.avatar || undefined,
backgroundColor: group?.backgroundColor || undefined,
description: group?.description || '',
marketIdentifier: group?.marketIdentifier || undefined,
title: group?.title || '',
});
};