mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ 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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 || '',
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user