mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80bce41dea |
@@ -495,6 +495,7 @@
|
||||
"skillStore.networkError": "Network error, please try again",
|
||||
"skillStore.search": "Search skills by name or keyword, press Enter to search…",
|
||||
"skillStore.tabs.community": "Community",
|
||||
"skillStore.tabs.custom": "Custom",
|
||||
"skillStore.tabs.lobehub": "LobeHub",
|
||||
"skillStore.title": "Skill Store",
|
||||
"startConversation": "Start Conversation",
|
||||
|
||||
@@ -495,6 +495,7 @@
|
||||
"skillStore.networkError": "网络错误,请重试",
|
||||
"skillStore.search": "搜索技能名称或关键词,按回车键搜索…",
|
||||
"skillStore.tabs.community": "社区",
|
||||
"skillStore.tabs.custom": "自定义",
|
||||
"skillStore.tabs.lobehub": "LobeHub",
|
||||
"skillStore.title": "技能商店",
|
||||
"startConversation": "开始对话",
|
||||
|
||||
@@ -53,6 +53,7 @@ const ProfileEditor = memo(() => {
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<ModelSelect
|
||||
listHeight={400}
|
||||
onChange={updateConfig}
|
||||
value={{
|
||||
model: config.model,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import McpSettingsModal from '@/features/MCP/MCPSettings/McpSettingsModal';
|
||||
import PluginDetailModal from '@/features/PluginDetailModal';
|
||||
import EditCustomPlugin from './EditCustomPlugin';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useServerConfigStore } from '@/store/serverConfig';
|
||||
@@ -14,6 +13,8 @@ import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
|
||||
import { type LobeToolType } from '@/types/tool/tool';
|
||||
|
||||
import EditCustomPlugin from './EditCustomPlugin';
|
||||
|
||||
interface ActionsProps {
|
||||
identifier: string;
|
||||
isMCP?: boolean;
|
||||
@@ -46,9 +47,6 @@ const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
|
||||
const [showModal, setModal] = useState(false);
|
||||
const [mcpSettingsOpen, setMcpSettingsOpen] = useState(false);
|
||||
|
||||
// 自定义插件(包括自定义 MCP)使用 EditCustomPlugin
|
||||
// 社区 MCP 使用 McpSettingsModal
|
||||
// 传统插件使用 PluginDetailModal
|
||||
const isCommunityMCP = !isCustomPlugin && isMCP;
|
||||
const showConfigureButton = isCustomPlugin || isMCP || hasSettings;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { type KlavisServerType } from '@lobechat/const';
|
||||
import { ActionIcon, Avatar, DropdownMenu, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { App, Button } from 'antd';
|
||||
import { createStyles, cssVar } from 'antd-style';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { Loader2, MoreVerticalIcon, SquareArrowOutUpRight, Unplug } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -17,10 +17,10 @@ import { userProfileSelectors } from '@/store/user/selectors';
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
const POLL_TIMEOUT_MS = 15_000;
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
connected: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorSuccess};
|
||||
color: ${cssVar.colorSuccess};
|
||||
`,
|
||||
container: css`
|
||||
padding-block: 12px;
|
||||
@@ -28,11 +28,17 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
`,
|
||||
disconnected: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorTextTertiary};
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
`,
|
||||
disconnectedIcon: css`
|
||||
opacity: 0.5;
|
||||
`,
|
||||
disconnectedTitle: css`
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
`,
|
||||
error: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorError};
|
||||
color: ${cssVar.colorError};
|
||||
`,
|
||||
icon: css`
|
||||
display: flex;
|
||||
@@ -44,20 +50,20 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
|
||||
background: ${token.colorFillTertiary};
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
`,
|
||||
pending: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorWarning};
|
||||
color: ${cssVar.colorWarning};
|
||||
`,
|
||||
title: css`
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: ${token.colorText};
|
||||
color: ${cssVar.colorText};
|
||||
|
||||
&:hover {
|
||||
color: ${token.colorPrimary};
|
||||
color: ${cssVar.colorPrimary};
|
||||
}
|
||||
`,
|
||||
}));
|
||||
@@ -69,7 +75,6 @@ interface KlavisSkillItemProps {
|
||||
|
||||
const KlavisSkillItem = memo<KlavisSkillItemProps>(({ serverType, server }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const { styles } = useStyles();
|
||||
const { modal } = App.useApp();
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isWaitingAuth, setIsWaitingAuth] = useState(false);
|
||||
@@ -323,9 +328,14 @@ const KlavisSkillItem = memo<KlavisSkillItemProps>(({ serverType, server }) => {
|
||||
justify="space-between"
|
||||
>
|
||||
<Flexbox align="center" gap={16} horizontal style={{ flex: 1, overflow: 'hidden' }}>
|
||||
<div className={styles.icon}>{renderIcon()}</div>
|
||||
<div className={`${styles.icon} ${!isConnected ? styles.disconnectedIcon : ''}`}>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
<Flexbox gap={4} style={{ overflow: 'hidden' }}>
|
||||
<span className={styles.title} onClick={() => setDetailOpen(true)}>
|
||||
<span
|
||||
className={`${styles.title} ${!isConnected ? styles.disconnectedTitle : ''}`}
|
||||
onClick={() => setDetailOpen(true)}
|
||||
>
|
||||
{serverType.label}
|
||||
</span>
|
||||
{!isConnected && renderStatus()}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { type LobehubSkillProviderType } from '@lobechat/const';
|
||||
import { ActionIcon, Avatar, DropdownMenu, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { App, Button } from 'antd';
|
||||
import { createStyles, cssVar } from 'antd-style';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { Loader2, MoreVerticalIcon, SquareArrowOutUpRight, Unplug } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
const POLL_TIMEOUT_MS = 15_000;
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
connected: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorSuccess};
|
||||
color: ${cssVar.colorSuccess};
|
||||
`,
|
||||
container: css`
|
||||
padding-block: 12px;
|
||||
@@ -29,17 +29,17 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
`,
|
||||
disconnected: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorTextTertiary};
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
`,
|
||||
disconnectedIcon: css`
|
||||
opacity: 0.5;
|
||||
`,
|
||||
disconnectedTitle: css`
|
||||
color: ${token.colorTextTertiary};
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
`,
|
||||
error: css`
|
||||
font-size: 14px;
|
||||
color: ${token.colorError};
|
||||
color: ${cssVar.colorError};
|
||||
`,
|
||||
icon: css`
|
||||
display: flex;
|
||||
@@ -51,16 +51,16 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
|
||||
background: ${token.colorFillTertiary};
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
`,
|
||||
title: css`
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: ${token.colorText};
|
||||
color: ${cssVar.colorText};
|
||||
|
||||
&:hover {
|
||||
color: ${token.colorPrimary};
|
||||
color: ${cssVar.colorPrimary};
|
||||
}
|
||||
`,
|
||||
}));
|
||||
@@ -72,7 +72,6 @@ interface LobehubSkillItemProps {
|
||||
|
||||
const LobehubSkillItem = memo<LobehubSkillItemProps>(({ provider, server }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const { styles } = useStyles();
|
||||
const { modal } = App.useApp();
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isWaitingAuth, setIsWaitingAuth] = useState(false);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox, Modal } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo, useState } from 'react';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { Suspense, memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PluginAvatar from '@/components/Plugins/PluginAvatar';
|
||||
import PluginTag from '@/components/Plugins/PluginTag';
|
||||
import PluginDetailModal from '@/features/PluginDetailModal';
|
||||
import Actions from './Actions';
|
||||
import McpDetail from '@/features/MCP/MCPDetail';
|
||||
import McpDetailLoading from '@/features/MCP/MCPDetail/Loading';
|
||||
import PluginDetailModal from '@/features/PluginDetailModal';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
import { type LobeToolType } from '@/types/tool/tool';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
import Actions from './Actions';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
padding-block: 12px;
|
||||
padding-inline: 0;
|
||||
@@ -29,16 +31,16 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
|
||||
background: ${token.colorFillTertiary};
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
`,
|
||||
title: css`
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: ${token.colorText};
|
||||
color: ${cssVar.colorText};
|
||||
|
||||
&:hover {
|
||||
color: ${token.colorPrimary};
|
||||
color: ${cssVar.colorPrimary};
|
||||
}
|
||||
`,
|
||||
}));
|
||||
@@ -54,7 +56,6 @@ interface McpSkillItemProps {
|
||||
|
||||
const McpSkillItem = memo<McpSkillItemProps>(
|
||||
({ identifier, title, avatar, type, runtimeType, author }) => {
|
||||
const { styles } = useStyles();
|
||||
const { t } = useTranslation('plugin');
|
||||
const isMCP = runtimeType === 'mcp';
|
||||
const isCustomPlugin = type === 'customPlugin';
|
||||
@@ -94,7 +95,9 @@ const McpSkillItem = memo<McpSkillItemProps>(
|
||||
title={t('dev.title.skillDetails')}
|
||||
width={800}
|
||||
>
|
||||
<McpDetail identifier={identifier} noSettings />
|
||||
<Suspense fallback={<McpDetailLoading />}>
|
||||
<McpDetail identifier={identifier} noSettings />
|
||||
</Suspense>
|
||||
</Modal>
|
||||
)}
|
||||
{isCustomPlugin && (
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
getLobehubSkillProviderById,
|
||||
} from '@lobechat/const';
|
||||
import { Divider } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -29,11 +29,11 @@ import { KlavisServerStatus } from '@/store/tool/slices/klavisStore';
|
||||
import { LobehubSkillStatus } from '@/store/tool/slices/lobehubSkillStore/types';
|
||||
import { type LobeToolType } from '@/types/tool/tool';
|
||||
|
||||
import McpSkillItem from './McpSkillItem';
|
||||
import KlavisSkillItem from './KlavisSkillItem';
|
||||
import LobehubSkillItem from './LobehubSkillItem';
|
||||
import McpSkillItem from './McpSkillItem';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -41,18 +41,17 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
`,
|
||||
description: css`
|
||||
margin-block-end: 8px;
|
||||
color: ${token.colorTextSecondary};
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
`,
|
||||
empty: css`
|
||||
padding: 24px;
|
||||
color: ${token.colorTextTertiary};
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
text-align: center;
|
||||
`,
|
||||
}));
|
||||
|
||||
const SkillList = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
const { styles } = useStyles();
|
||||
|
||||
const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
|
||||
const isKlavisEnabled = useServerConfigStore(serverConfigSelectors.enableKlavis);
|
||||
@@ -90,16 +89,51 @@ const SkillList = memo(() => {
|
||||
|
||||
// If RECOMMENDED_SKILLS is configured, use it to build the list
|
||||
if (RECOMMENDED_SKILLS.length > 0) {
|
||||
const addedLobehubIds = new Set<string>();
|
||||
const addedKlavisIds = new Set<string>();
|
||||
|
||||
for (const skill of RECOMMENDED_SKILLS) {
|
||||
if (skill.type === RecommendedSkillType.Lobehub && isLobehubSkillEnabled) {
|
||||
const provider = getLobehubSkillProviderById(skill.id);
|
||||
if (provider) {
|
||||
integrationItems.push({ provider, type: 'lobehub' });
|
||||
addedLobehubIds.add(skill.id);
|
||||
}
|
||||
} else if (skill.type === RecommendedSkillType.Klavis && isKlavisEnabled) {
|
||||
const serverType = getKlavisServerByServerIdentifier(skill.id);
|
||||
if (serverType) {
|
||||
integrationItems.push({ serverType, type: 'klavis' });
|
||||
addedKlavisIds.add(skill.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also add connected Lobehub skills that are not in RECOMMENDED_SKILLS
|
||||
if (isLobehubSkillEnabled) {
|
||||
for (const server of allLobehubSkillServers) {
|
||||
if (
|
||||
server.status === LobehubSkillStatus.CONNECTED &&
|
||||
!addedLobehubIds.has(server.identifier)
|
||||
) {
|
||||
const provider = getLobehubSkillProviderById(server.identifier);
|
||||
if (provider) {
|
||||
integrationItems.push({ provider, type: 'lobehub' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also add connected Klavis skills that are not in RECOMMENDED_SKILLS
|
||||
if (isKlavisEnabled) {
|
||||
for (const server of allKlavisServers) {
|
||||
if (
|
||||
server.status === KlavisServerStatus.CONNECTED &&
|
||||
!addedKlavisIds.has(server.identifier)
|
||||
) {
|
||||
const serverType = getKlavisServerByServerIdentifier(server.identifier);
|
||||
if (serverType) {
|
||||
integrationItems.push({ serverType, type: 'klavis' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { BadgeCheck, CircleUser, Package } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MCPTag from './MCPTag';
|
||||
|
||||
interface PluginTagProps {
|
||||
author?: string;
|
||||
isMCP?: boolean;
|
||||
@@ -11,28 +13,39 @@ interface PluginTagProps {
|
||||
type: 'builtin' | 'customPlugin' | 'plugin';
|
||||
}
|
||||
|
||||
const PluginTag = memo<PluginTagProps>(({ showIcon = true, author, type, showText = true }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const isCustom = type === 'customPlugin';
|
||||
const isOfficial = author === 'LobeHub';
|
||||
const PluginTag = memo<PluginTagProps>(
|
||||
({ showIcon = true, author, type, showText = true, isMCP }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const isCustom = type === 'customPlugin';
|
||||
const isOfficial = author === 'LobeHub';
|
||||
|
||||
const customTag = (
|
||||
<Tag color={'warning'} icon={showIcon && <Icon icon={Package} />} size={'small'}>
|
||||
{t('store.customPlugin')}
|
||||
</Tag>
|
||||
);
|
||||
const customTag = (
|
||||
<Tag color={'warning'} icon={showIcon && <Icon icon={Package} />} size={'small'}>
|
||||
{t('store.customPlugin')}
|
||||
</Tag>
|
||||
);
|
||||
|
||||
if (isCustom) return customTag;
|
||||
if (isMCP) {
|
||||
return (
|
||||
<>
|
||||
<MCPTag showIcon={showIcon} showText={false} />
|
||||
{isCustom && customTag}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color={isOfficial ? 'success' : undefined}
|
||||
icon={showIcon && <Icon icon={isOfficial ? BadgeCheck : CircleUser} />}
|
||||
size={'small'}
|
||||
>
|
||||
{showText && (author || t('store.communityPlugin'))}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
if (isCustom) return customTag;
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color={isOfficial ? 'success' : undefined}
|
||||
icon={showIcon && <Icon icon={isOfficial ? BadgeCheck : CircleUser} />}
|
||||
size={'small'}
|
||||
>
|
||||
{showText && (author || t('store.communityPlugin'))}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default PluginTag;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flexbox, Icon, type ItemType, usePopoverContext } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { ChevronRight, Settings, Store } from 'lucide-react';
|
||||
import { ChevronRight, ExternalLink, Settings, Store } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -53,7 +53,7 @@ const PopoverContent = memo<PopoverContentProps>(({ items, enableKlavis, onOpenS
|
||||
<div className={toolsListStyles.itemIcon}>
|
||||
<Icon icon={Store} size={20} />
|
||||
</div>
|
||||
<div className={toolsListStyles.itemContent}>{t('tools.plugins.store')}</div>
|
||||
<div className={toolsListStyles.itemContent}>{t('skillStore.title')}</div>
|
||||
<Icon className={styles.trailingIcon} icon={ChevronRight} size={16} />
|
||||
</div>
|
||||
<div
|
||||
@@ -69,7 +69,7 @@ const PopoverContent = memo<PopoverContentProps>(({ items, enableKlavis, onOpenS
|
||||
<Icon icon={Settings} size={20} />
|
||||
</div>
|
||||
<div className={toolsListStyles.itemContent}>{t('tools.plugins.management')}</div>
|
||||
<Icon className={styles.trailingIcon} icon={ChevronRight} size={16} />
|
||||
<Icon className={styles.trailingIcon} icon={ExternalLink} size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</Flexbox>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
RECOMMENDED_SKILLS,
|
||||
RecommendedSkillType,
|
||||
} from '@lobechat/const';
|
||||
import { Avatar, Flexbox, Icon, Image, type ItemType } from '@lobehub/ui';
|
||||
import { Avatar, Icon, Image, type ItemType } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { ToyBrick } from 'lucide-react';
|
||||
@@ -72,12 +72,6 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
||||
s.togglePlugin,
|
||||
]);
|
||||
const builtinList = useToolStore(builtinToolSelectors.metaList, isEqual);
|
||||
const enablePluginCount = useAgentStore(
|
||||
(s) =>
|
||||
agentByIdSelectors
|
||||
.getAgentPluginsById(agentId)(s)
|
||||
.filter((i) => !builtinList.some((b) => b.identifier === i)).length,
|
||||
);
|
||||
const plugins = useAgentStore((s) => agentByIdSelectors.getAgentPluginsById(agentId)(s));
|
||||
|
||||
// Klavis 相关状态
|
||||
@@ -219,10 +213,21 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
||||
);
|
||||
|
||||
// Skills 列表项(包含 LobeHub Skill 和 Klavis)
|
||||
const skillItems = useMemo(
|
||||
() => [...lobehubSkillItems, ...klavisServerItems],
|
||||
[lobehubSkillItems, klavisServerItems],
|
||||
);
|
||||
// 已连接的排在前面
|
||||
const skillItems = useMemo(() => {
|
||||
const allItems = [...lobehubSkillItems, ...klavisServerItems];
|
||||
|
||||
return allItems.sort((a, b) => {
|
||||
const isConnectedA =
|
||||
installedLobehubIds.has(a.key as string) || installedKlavisIds.has(a.key as string);
|
||||
const isConnectedB =
|
||||
installedLobehubIds.has(b.key as string) || installedKlavisIds.has(b.key as string);
|
||||
|
||||
if (isConnectedA && !isConnectedB) return -1;
|
||||
if (!isConnectedA && isConnectedB) return 1;
|
||||
return 0;
|
||||
});
|
||||
}, [lobehubSkillItems, klavisServerItems, installedLobehubIds, installedKlavisIds]);
|
||||
|
||||
// 区分社区插件和自定义插件
|
||||
const communityPlugins = list.filter((item) => item.type !== 'customPlugin');
|
||||
@@ -250,47 +255,55 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
||||
),
|
||||
});
|
||||
|
||||
// 构建 Skills 分组的 children
|
||||
const skillGroupChildren: ItemType[] = [
|
||||
// 1. LobeHub Skill 和 Klavis
|
||||
// 构建 LobeHub 分组的 children(包含内置工具和 LobeHub Skill/Klavis)
|
||||
const lobehubGroupChildren: ItemType[] = [
|
||||
// 1. 内置工具
|
||||
...builtinItems,
|
||||
// 2. LobeHub Skill 和 Klavis(作为内置技能)
|
||||
...skillItems,
|
||||
// 2. divider (如果有 skillItems 且有社区插件)
|
||||
...(skillItems.length > 0 && communityPlugins.length > 0
|
||||
? [{ key: 'divider-skill-community', type: 'divider' as const }]
|
||||
: []),
|
||||
// 3. 社区插件
|
||||
...communityPlugins.map(mapPluginToItem),
|
||||
// 4. divider (如果有自定义插件)
|
||||
...(customPlugins.length > 0
|
||||
? [{ key: 'divider-community-custom', type: 'divider' as const }]
|
||||
: []),
|
||||
// 5. 自定义插件
|
||||
...customPlugins.map(mapPluginToItem),
|
||||
];
|
||||
|
||||
// 构建 Community 分组的 children(只包含社区插件)
|
||||
const communityGroupChildren: ItemType[] = communityPlugins.map(mapPluginToItem);
|
||||
|
||||
// 构建 Custom 分组的 children(只包含自定义插件)
|
||||
const customGroupChildren: ItemType[] = customPlugins.map(mapPluginToItem);
|
||||
|
||||
// 市场 tab 的 items
|
||||
const marketItems: ItemType[] = [
|
||||
{
|
||||
children: builtinItems,
|
||||
key: 'builtins',
|
||||
label: t('tools.builtins.groupName'),
|
||||
type: 'group',
|
||||
},
|
||||
{
|
||||
children: skillGroupChildren,
|
||||
key: 'plugins',
|
||||
label: (
|
||||
<Flexbox align={'center'} gap={40} horizontal justify={'space-between'}>
|
||||
{t('tools.plugins.groupName')}
|
||||
{enablePluginCount === 0 ? null : (
|
||||
<div style={{ fontSize: 12, marginInlineEnd: 4 }}>
|
||||
{t('tools.plugins.enabled', { num: enablePluginCount })}
|
||||
</div>
|
||||
)}
|
||||
</Flexbox>
|
||||
),
|
||||
type: 'group',
|
||||
},
|
||||
// LobeHub 分组
|
||||
...(lobehubGroupChildren.length > 0
|
||||
? [
|
||||
{
|
||||
children: lobehubGroupChildren,
|
||||
key: 'lobehub',
|
||||
label: t('skillStore.tabs.lobehub'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Community 分组
|
||||
...(communityGroupChildren.length > 0
|
||||
? [
|
||||
{
|
||||
children: communityGroupChildren,
|
||||
key: 'community',
|
||||
label: t('skillStore.tabs.community'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Custom 分组(只有在有自定义插件时才显示)
|
||||
...(customGroupChildren.length > 0
|
||||
? [
|
||||
{
|
||||
children: customGroupChildren,
|
||||
key: 'custom',
|
||||
label: t('skillStore.tabs.custom'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
// 已安装 tab 的 items - 只显示已安装的插件
|
||||
@@ -319,15 +332,6 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
||||
),
|
||||
}));
|
||||
|
||||
if (enabledBuiltinItems.length > 0) {
|
||||
installedItems.push({
|
||||
children: enabledBuiltinItems,
|
||||
key: 'installed-builtins',
|
||||
label: t('tools.builtins.groupName'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
// 已连接的 Klavis 服务器
|
||||
const connectedKlavisItems = klavisServerItems.filter((item) =>
|
||||
checked.includes(item.key as string),
|
||||
@@ -338,9 +342,30 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
||||
checked.includes(item.key as string),
|
||||
);
|
||||
|
||||
// 合并已启用的 LobeHub Skill 和 Klavis
|
||||
// 合并已启用的 LobeHub Skill 和 Klavis(作为内置技能)
|
||||
const enabledSkillItems = [...connectedLobehubSkillItems, ...connectedKlavisItems];
|
||||
|
||||
// 构建内置工具分组的 children(包含内置工具和 LobeHub Skill/Klavis)
|
||||
const allBuiltinItems: ItemType[] = [
|
||||
// 1. 内置工具
|
||||
...enabledBuiltinItems,
|
||||
// 2. divider (如果有内置工具且有 skill items)
|
||||
...(enabledBuiltinItems.length > 0 && enabledSkillItems.length > 0
|
||||
? [{ key: 'installed-divider-builtin-skill', type: 'divider' as const }]
|
||||
: []),
|
||||
// 3. LobeHub Skill 和 Klavis
|
||||
...enabledSkillItems,
|
||||
];
|
||||
|
||||
if (allBuiltinItems.length > 0) {
|
||||
installedItems.push({
|
||||
children: allBuiltinItems,
|
||||
key: 'installed-lobehub',
|
||||
label: t('skillStore.tabs.lobehub'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
// 已启用的社区插件
|
||||
const enabledCommunityPlugins = communityPlugins
|
||||
.filter((item) => checked.includes(item.identifier))
|
||||
@@ -389,29 +414,22 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
||||
),
|
||||
}));
|
||||
|
||||
// 构建 Skills 分组的 children(带 divider)
|
||||
const allSkillItems: ItemType[] = [
|
||||
// 1. LobeHub Skill 和 Klavis
|
||||
...enabledSkillItems,
|
||||
// 2. divider (如果有 skillItems 且有社区插件)
|
||||
...(enabledSkillItems.length > 0 && enabledCommunityPlugins.length > 0
|
||||
? [{ key: 'installed-divider-skill-community', type: 'divider' as const }]
|
||||
: []),
|
||||
// 3. 社区插件
|
||||
...enabledCommunityPlugins,
|
||||
// 4. divider (如果有自定义插件)
|
||||
...(enabledCustomPlugins.length > 0
|
||||
? [{ key: 'installed-divider-community-custom', type: 'divider' as const }]
|
||||
: []),
|
||||
// 5. 自定义插件
|
||||
...enabledCustomPlugins,
|
||||
];
|
||||
|
||||
if (allSkillItems.length > 0) {
|
||||
// Community 分组(只包含社区插件)
|
||||
if (enabledCommunityPlugins.length > 0) {
|
||||
installedItems.push({
|
||||
children: allSkillItems,
|
||||
key: 'installed-plugins',
|
||||
label: t('tools.plugins.groupName'),
|
||||
children: enabledCommunityPlugins,
|
||||
key: 'installed-community',
|
||||
label: t('skillStore.tabs.community'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
// Custom 分组(只包含自定义插件)
|
||||
if (enabledCustomPlugins.length > 0) {
|
||||
installedItems.push({
|
||||
children: enabledCustomPlugins,
|
||||
key: 'installed-custom',
|
||||
label: t('skillStore.tabs.custom'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ interface ModelOption {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ModelSelectProps extends Pick<SelectProps, 'loading' | 'size' | 'style' | 'variant'> {
|
||||
interface ModelSelectProps extends Pick<
|
||||
SelectProps,
|
||||
'listHeight' | 'loading' | 'size' | 'style' | 'variant'
|
||||
> {
|
||||
defaultValue?: { model: string; provider?: string };
|
||||
onChange?: (props: { model: string; provider: string }) => void;
|
||||
requiredAbilities?: (keyof EnabledProviderWithModels['children'][number]['abilities'])[];
|
||||
@@ -38,7 +41,17 @@ interface ModelSelectProps extends Pick<SelectProps, 'loading' | 'size' | 'style
|
||||
}
|
||||
|
||||
const ModelSelect = memo<ModelSelectProps>(
|
||||
({ value, onChange, showAbility = true, requiredAbilities, loading, size, style, variant }) => {
|
||||
({
|
||||
value,
|
||||
onChange,
|
||||
showAbility = true,
|
||||
requiredAbilities,
|
||||
listHeight,
|
||||
loading,
|
||||
size,
|
||||
style,
|
||||
variant,
|
||||
}) => {
|
||||
const enabledList = useEnabledChatModels();
|
||||
|
||||
const options = useMemo<SelectProps['options']>(() => {
|
||||
@@ -92,6 +105,7 @@ const ModelSelect = memo<ModelSelectProps>(
|
||||
popup: { root: styles.popup },
|
||||
}}
|
||||
defaultValue={`${value?.provider}/${value?.model}`}
|
||||
listHeight={listHeight}
|
||||
loading={loading}
|
||||
onChange={(value, option) => {
|
||||
const model = value.split('/').slice(1).join('/');
|
||||
|
||||
@@ -36,6 +36,7 @@ const PluginDetailModal = memo<PluginDetailModalProps>(
|
||||
return (
|
||||
<Modal
|
||||
allowFullscreen
|
||||
destroyOnHidden
|
||||
footer={null}
|
||||
onCancel={onClose}
|
||||
onOk={() => {
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
LOBEHUB_SKILL_PROVIDERS,
|
||||
type LobehubSkillProviderType,
|
||||
} from '@lobechat/const';
|
||||
import { Avatar, Button, Flexbox, Icon, type ItemType, Segmented } from '@lobehub/ui';
|
||||
import { Avatar, Button, Flexbox, Icon, type ItemType } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { ArrowRight, PlusIcon, Store, ToyBrick } from 'lucide-react';
|
||||
import { PlusIcon, ToyBrick } from 'lucide-react';
|
||||
import React, { Suspense, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -34,44 +34,19 @@ import {
|
||||
import { type LobeToolMetaWithAvailability } from '@/store/tool/slices/builtin/selectors';
|
||||
|
||||
import PluginTag from './PluginTag';
|
||||
import PopoverContent from './PopoverContent';
|
||||
|
||||
const WEB_BROWSING_IDENTIFIER = 'lobe-web-browsing';
|
||||
|
||||
type TabType = 'all' | 'installed';
|
||||
|
||||
const prefixCls = 'ant';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
dropdown: css`
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
border: 1px solid ${cssVar.colorBorderSecondary};
|
||||
border-radius: ${cssVar.borderRadiusLG};
|
||||
|
||||
background: ${cssVar.colorBgElevated};
|
||||
box-shadow: ${cssVar.boxShadowSecondary};
|
||||
|
||||
.${prefixCls}-dropdown-menu {
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
padding: ${cssVar.paddingXS};
|
||||
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
||||
background: transparent;
|
||||
`,
|
||||
icon: css`
|
||||
flex: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-inline-end: ${cssVar.marginXS};
|
||||
`,
|
||||
scroller: css`
|
||||
overflow: hidden auto;
|
||||
`,
|
||||
}));
|
||||
|
||||
/**
|
||||
@@ -157,9 +132,9 @@ const AgentTool = memo<AgentToolProps>(
|
||||
const allLobehubSkillServers = useToolStore(lobehubSkillStoreSelectors.getServers, isEqual);
|
||||
const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
|
||||
|
||||
// Plugin store modal state
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [skillStoreOpen, setSkillStoreOpen] = useState(false);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
// Tab state for dual-column layout
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
@@ -235,10 +210,11 @@ const AgentTool = memo<AgentToolProps>(
|
||||
);
|
||||
|
||||
// Set default tab based on installed plugins (only on first load)
|
||||
// Only show 'installed' tab by default if more than 5 plugins are enabled
|
||||
useEffect(() => {
|
||||
if (!isInitializedRef.current && plugins.length >= 0) {
|
||||
isInitializedRef.current = true;
|
||||
setActiveTab(plugins.length > 0 ? 'installed' : 'all');
|
||||
setActiveTab(plugins.length > 5 ? 'installed' : 'all');
|
||||
}
|
||||
}, [plugins.length]);
|
||||
|
||||
@@ -331,17 +307,14 @@ const AgentTool = memo<AgentToolProps>(
|
||||
}
|
||||
};
|
||||
|
||||
// Build dropdown menu items (adapted from useControls)
|
||||
const enablePluginCount = plugins.filter(
|
||||
(id) => !builtinList.some((b) => b.identifier === id),
|
||||
).length;
|
||||
|
||||
// 合并 builtin 工具、LobeHub Skill Providers 和 Klavis 服务器
|
||||
const builtinItems = useMemo(
|
||||
() => [
|
||||
// 原有的 builtin 工具
|
||||
...filteredBuiltinList.map((item) => ({
|
||||
icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
|
||||
icon: (
|
||||
<Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none', marginRight: 0 }} />
|
||||
),
|
||||
key: item.identifier,
|
||||
label: (
|
||||
<ToolItem
|
||||
@@ -364,70 +337,85 @@ const AgentTool = memo<AgentToolProps>(
|
||||
[filteredBuiltinList, klavisServerItems, lobehubSkillItems, isToolEnabled, handleToggleTool],
|
||||
);
|
||||
|
||||
// Plugin items for dropdown
|
||||
const pluginItems = useMemo(
|
||||
() =>
|
||||
installedPluginList.map((item) => ({
|
||||
icon: item?.avatar ? (
|
||||
<PluginAvatar avatar={item.avatar} size={20} />
|
||||
) : (
|
||||
<Icon icon={ToyBrick} size={20} />
|
||||
),
|
||||
key: item.identifier,
|
||||
label: (
|
||||
<ToolItem
|
||||
checked={plugins.includes(item.identifier)}
|
||||
id={item.identifier}
|
||||
label={item.title}
|
||||
onUpdate={async () => {
|
||||
setUpdating(true);
|
||||
await togglePlugin(item.identifier);
|
||||
setUpdating(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[installedPluginList, plugins, togglePlugin],
|
||||
// 区分社区插件和自定义插件
|
||||
const communityPlugins = installedPluginList.filter((item) => item.type !== 'customPlugin');
|
||||
const customPlugins = installedPluginList.filter((item) => item.type === 'customPlugin');
|
||||
|
||||
// 生成插件列表项的函数
|
||||
const mapPluginToItem = useCallback(
|
||||
(item: (typeof installedPluginList)[0]) => ({
|
||||
icon: item?.avatar ? (
|
||||
<PluginAvatar avatar={item.avatar} size={20} />
|
||||
) : (
|
||||
<Icon icon={ToyBrick} size={20} />
|
||||
),
|
||||
key: item.identifier,
|
||||
label: (
|
||||
<ToolItem
|
||||
checked={plugins.includes(item.identifier)}
|
||||
id={item.identifier}
|
||||
label={item.title}
|
||||
onUpdate={async () => {
|
||||
setUpdating(true);
|
||||
await togglePlugin(item.identifier);
|
||||
setUpdating(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[plugins, togglePlugin],
|
||||
);
|
||||
|
||||
// Community 插件列表项
|
||||
const communityPluginItems = useMemo(
|
||||
() => communityPlugins.map(mapPluginToItem),
|
||||
[communityPlugins, mapPluginToItem],
|
||||
);
|
||||
|
||||
// Custom 插件列表项
|
||||
const customPluginItems = useMemo(
|
||||
() => customPlugins.map(mapPluginToItem),
|
||||
[customPlugins, mapPluginToItem],
|
||||
);
|
||||
|
||||
// All tab items (市场 tab)
|
||||
const allTabItems: ItemType[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
children: builtinItems,
|
||||
key: 'builtins',
|
||||
label: t('tools.builtins.groupName'),
|
||||
type: 'group',
|
||||
},
|
||||
{
|
||||
children: pluginItems,
|
||||
key: 'plugins',
|
||||
label: (
|
||||
<Flexbox align={'center'} gap={40} horizontal justify={'space-between'}>
|
||||
{t('tools.plugins.groupName')}
|
||||
{enablePluginCount === 0 ? null : (
|
||||
<div style={{ fontSize: 12, marginInlineEnd: 4 }}>
|
||||
{t('tools.plugins.enabled', { num: enablePluginCount })}
|
||||
</div>
|
||||
)}
|
||||
</Flexbox>
|
||||
),
|
||||
type: 'group',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
extra: <Icon icon={ArrowRight} />,
|
||||
icon: Store,
|
||||
key: 'plugin-store',
|
||||
label: t('tools.plugins.store'),
|
||||
onClick: () => {
|
||||
setModalOpen(true);
|
||||
},
|
||||
},
|
||||
// LobeHub 分组
|
||||
...(builtinItems.length > 0
|
||||
? [
|
||||
{
|
||||
children: builtinItems,
|
||||
key: 'lobehub',
|
||||
label: t('skillStore.tabs.lobehub'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Community 分组
|
||||
...(communityPluginItems.length > 0
|
||||
? [
|
||||
{
|
||||
children: communityPluginItems,
|
||||
key: 'community',
|
||||
label: t('skillStore.tabs.community'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Custom 分组
|
||||
...(customPluginItems.length > 0
|
||||
? [
|
||||
{
|
||||
children: customPluginItems,
|
||||
key: 'custom',
|
||||
label: t('skillStore.tabs.custom'),
|
||||
type: 'group' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[builtinItems, pluginItems, enablePluginCount, t],
|
||||
[builtinItems, communityPluginItems, customPluginItems, t],
|
||||
);
|
||||
|
||||
// Installed tab items - 只显示已启用的
|
||||
@@ -464,24 +452,24 @@ const AgentTool = memo<AgentToolProps>(
|
||||
plugins.includes(item.key as string),
|
||||
);
|
||||
|
||||
// 合并 builtin、LobeHub Skill 和 Klavis
|
||||
const allBuiltinItems = [
|
||||
// LobeHub 分组(builtin + LobeHub Skill + Klavis)
|
||||
const lobehubGroupItems = [
|
||||
...enabledBuiltinItems,
|
||||
...connectedKlavisItems,
|
||||
...connectedLobehubSkillItems,
|
||||
...connectedKlavisItems,
|
||||
];
|
||||
|
||||
if (allBuiltinItems.length > 0) {
|
||||
if (lobehubGroupItems.length > 0) {
|
||||
items.push({
|
||||
children: allBuiltinItems,
|
||||
key: 'installed-builtins',
|
||||
label: t('tools.builtins.groupName'),
|
||||
children: lobehubGroupItems,
|
||||
key: 'installed-lobehub',
|
||||
label: t('skillStore.tabs.lobehub'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
// 已启用的插件
|
||||
const installedPlugins = installedPluginList
|
||||
// 已启用的社区插件
|
||||
const enabledCommunityPlugins = communityPlugins
|
||||
.filter((item) => plugins.includes(item.identifier))
|
||||
.map((item) => ({
|
||||
icon: item?.avatar ? (
|
||||
@@ -504,11 +492,44 @@ const AgentTool = memo<AgentToolProps>(
|
||||
),
|
||||
}));
|
||||
|
||||
if (installedPlugins.length > 0) {
|
||||
if (enabledCommunityPlugins.length > 0) {
|
||||
items.push({
|
||||
children: installedPlugins,
|
||||
key: 'installed-plugins',
|
||||
label: t('tools.plugins.groupName'),
|
||||
children: enabledCommunityPlugins,
|
||||
key: 'installed-community',
|
||||
label: t('skillStore.tabs.community'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
|
||||
// 已启用的自定义插件
|
||||
const enabledCustomPlugins = customPlugins
|
||||
.filter((item) => plugins.includes(item.identifier))
|
||||
.map((item) => ({
|
||||
icon: item?.avatar ? (
|
||||
<PluginAvatar avatar={item.avatar} size={20} />
|
||||
) : (
|
||||
<Icon icon={ToyBrick} size={20} />
|
||||
),
|
||||
key: item.identifier,
|
||||
label: (
|
||||
<ToolItem
|
||||
checked={true}
|
||||
id={item.identifier}
|
||||
label={item.title}
|
||||
onUpdate={async () => {
|
||||
setUpdating(true);
|
||||
await togglePlugin(item.identifier);
|
||||
setUpdating(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
if (enabledCustomPlugins.length > 0) {
|
||||
items.push({
|
||||
children: enabledCustomPlugins,
|
||||
key: 'installed-custom',
|
||||
label: t('skillStore.tabs.custom'),
|
||||
type: 'group',
|
||||
});
|
||||
}
|
||||
@@ -518,7 +539,8 @@ const AgentTool = memo<AgentToolProps>(
|
||||
filteredBuiltinList,
|
||||
klavisServerItems,
|
||||
lobehubSkillItems,
|
||||
installedPluginList,
|
||||
communityPlugins,
|
||||
customPlugins,
|
||||
plugins,
|
||||
isToolEnabled,
|
||||
handleToggleTool,
|
||||
@@ -553,10 +575,42 @@ const AgentTool = memo<AgentToolProps>(
|
||||
}, [plugins, isSearchEnabled, showWebBrowsing]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={button}>
|
||||
{/* Plugin Selector and Tags */}
|
||||
<Flexbox align="center" gap={8} horizontal wrap={'wrap'}>
|
||||
{/* Second Row: Selected Plugins as Tags */}
|
||||
{/* Plugin Selector Dropdown - Using Action component pattern */}
|
||||
<ActionDropdown
|
||||
maxWidth={400}
|
||||
menu={{
|
||||
items: currentItems,
|
||||
style: {
|
||||
// let only the custom scroller scroll
|
||||
maxHeight: 'unset',
|
||||
overflowY: 'visible',
|
||||
},
|
||||
}}
|
||||
minWidth={400}
|
||||
onOpenChange={setDropdownOpen}
|
||||
open={dropdownOpen}
|
||||
placement={'bottomLeft'}
|
||||
popupRender={(menu) => (
|
||||
<PopoverContent
|
||||
activeTab={effectiveTab}
|
||||
installedTabItems={installedTabItems}
|
||||
menu={menu}
|
||||
onClose={() => setDropdownOpen(false)}
|
||||
onOpenStore={() => {
|
||||
setDropdownOpen(false);
|
||||
setSkillStoreOpen(true);
|
||||
}}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
)}
|
||||
trigger={'click'}
|
||||
>
|
||||
{button}
|
||||
</ActionDropdown>
|
||||
{/* Selected Plugins as Tags */}
|
||||
{allEnabledTools.map((pluginId) => {
|
||||
return (
|
||||
<PluginTag
|
||||
@@ -568,65 +622,9 @@ const AgentTool = memo<AgentToolProps>(
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Plugin Selector Dropdown - Using Action component pattern */}
|
||||
|
||||
<Suspense fallback={button}>
|
||||
<ActionDropdown
|
||||
maxHeight={500}
|
||||
maxWidth={400}
|
||||
menu={{
|
||||
items: currentItems,
|
||||
style: {
|
||||
// let only the custom scroller scroll
|
||||
maxHeight: 'unset',
|
||||
overflowY: 'visible',
|
||||
},
|
||||
}}
|
||||
minHeight={isKlavisEnabledInEnv || isLobehubSkillEnabled ? 500 : undefined}
|
||||
minWidth={400}
|
||||
placement={'bottomLeft'}
|
||||
popupRender={(menu) => (
|
||||
<div className={styles.dropdown}>
|
||||
{/* stopPropagation prevents dropdown's onClick from calling preventDefault on Segmented */}
|
||||
<div className={styles.header} onClick={(e) => e.stopPropagation()}>
|
||||
<Segmented
|
||||
block
|
||||
onChange={(v) => setActiveTab(v as TabType)}
|
||||
options={[
|
||||
{
|
||||
label: t('tools.tabs.all', { defaultValue: 'All' }),
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
|
||||
value: 'installed',
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
value={effectiveTab}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.scroller}
|
||||
style={{
|
||||
maxHeight: 500,
|
||||
minHeight: isKlavisEnabledInEnv || isLobehubSkillEnabled ? 500 : undefined,
|
||||
}}
|
||||
>
|
||||
{menu}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
trigger={'click'}
|
||||
>
|
||||
{button}
|
||||
</ActionDropdown>
|
||||
</Suspense>
|
||||
</Flexbox>
|
||||
|
||||
{/* PluginStore Modal - rendered outside Flexbox to avoid event interference */}
|
||||
{modalOpen && <SkillStore open={modalOpen} setOpen={setModalOpen} />}
|
||||
</>
|
||||
<SkillStore open={skillStoreOpen} setOpen={setSkillStoreOpen} />
|
||||
</Suspense>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Empty as EmptyComponent } from '@lobehub/ui';
|
||||
import { Plug2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Empty = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
return (
|
||||
<EmptyComponent
|
||||
description={t('tools.installed.empty', {
|
||||
defaultValue: 'No skills enabled',
|
||||
})}
|
||||
icon={Plug2}
|
||||
style={{ paddingBlock: 40 }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Empty.displayName = 'ToolEmpty';
|
||||
|
||||
export default Empty;
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Avatar, Icon, Tag } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { AlertCircle, X } from 'lucide-react';
|
||||
import { AlertCircle, Loader2, X } from 'lucide-react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -51,6 +51,21 @@ const LobehubSkillIcon = memo<Pick<LobehubSkillProviderType, 'icon' | 'label'>>(
|
||||
);
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
loadingIcon: css`
|
||||
flex-shrink: 0;
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
animation: spin 1s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`,
|
||||
notInstalledTag: css`
|
||||
border-color: ${cssVar.colorWarningBorder};
|
||||
background: ${cssVar.colorWarningBg};
|
||||
@@ -196,11 +211,18 @@ const PluginTag = memo<PluginTagProps>(
|
||||
type: 'plugin' as const,
|
||||
};
|
||||
|
||||
const displayTitle = isLoading ? 'Loading...' : meta.title;
|
||||
// Use identifier as title when loading, otherwise use meta.title
|
||||
const displayTitle = meta.title;
|
||||
const isDesktopOnly = showDesktopOnlyLabel && !meta.availableInWeb;
|
||||
|
||||
// Render icon based on type
|
||||
const renderIcon = () => {
|
||||
// Show loading spinner when loading
|
||||
if (isLoading) {
|
||||
return <Loader2 className={styles.loadingIcon} size={14} />;
|
||||
}
|
||||
|
||||
// Show warning icon when not installed
|
||||
if (!meta.isInstalled) {
|
||||
return <AlertCircle className={styles.warningIcon} size={14} />;
|
||||
}
|
||||
@@ -234,24 +256,28 @@ const PluginTag = memo<PluginTagProps>(
|
||||
if (isDesktopOnly) {
|
||||
text += ` (${t('tools.desktopOnly', { defaultValue: 'Desktop Only' })})`;
|
||||
}
|
||||
if (!meta.isInstalled) {
|
||||
// Don't show "Not Installed" when loading
|
||||
if (!meta.isInstalled && !isLoading) {
|
||||
text += ` (${t('tools.notInstalled', { defaultValue: 'Not Installed' })})`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
// Only show error state when not installed and not loading
|
||||
const showErrorState = !meta.isInstalled && !isLoading;
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className={styles.tag}
|
||||
closable
|
||||
closeIcon={<X size={12} />}
|
||||
color={meta.isInstalled ? undefined : 'error'}
|
||||
color={showErrorState ? 'error' : undefined}
|
||||
icon={renderIcon()}
|
||||
onClose={onRemove}
|
||||
title={
|
||||
meta.isInstalled
|
||||
? undefined
|
||||
: t('tools.notInstalledWarning', { defaultValue: 'This tool is not installed' })
|
||||
showErrorState
|
||||
? t('tools.notInstalledWarning', { defaultValue: 'This tool is not installed' })
|
||||
: undefined
|
||||
}
|
||||
variant={isDarkMode ? 'filled' : 'outlined'}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Flexbox, Icon, type ItemType, Segmented } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { ArrowRight, ExternalLink, Settings, Store } from 'lucide-react';
|
||||
import { type ReactNode, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Empty from './Empty';
|
||||
|
||||
type TabType = 'all' | 'installed';
|
||||
|
||||
const prefixCls = 'ant';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
dropdown: css`
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
.${prefixCls}-dropdown-menu {
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
`,
|
||||
footerItem: css`
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
padding-block: 8px;
|
||||
padding-inline: 12px;
|
||||
border-radius: 6px;
|
||||
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
footerItemContent: css`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`,
|
||||
footerItemIcon: css`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`,
|
||||
header: css`
|
||||
padding: ${cssVar.paddingXS};
|
||||
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
||||
background: transparent;
|
||||
`,
|
||||
scroller: css`
|
||||
overflow: hidden auto;
|
||||
`,
|
||||
trailingIcon: css`
|
||||
opacity: 0.5;
|
||||
`,
|
||||
}));
|
||||
|
||||
interface PopoverContentProps {
|
||||
activeTab: TabType;
|
||||
installedTabItems: ItemType[];
|
||||
menu: ReactNode;
|
||||
onClose?: () => void;
|
||||
onOpenStore: () => void;
|
||||
onTabChange: (tab: TabType) => void;
|
||||
}
|
||||
|
||||
const PopoverContent = memo<PopoverContentProps>(
|
||||
({ menu, activeTab, onTabChange, installedTabItems, onOpenStore, onClose }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.dropdown} style={{ maxHeight: 500 }}>
|
||||
{/* stopPropagation prevents dropdown's onClick from calling preventDefault on Segmented */}
|
||||
<div className={styles.header} onClick={(e) => e.stopPropagation()}>
|
||||
<Segmented
|
||||
block
|
||||
onChange={(v) => onTabChange(v as TabType)}
|
||||
options={[
|
||||
{
|
||||
label: t('tools.tabs.all', { defaultValue: 'All' }),
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
|
||||
value: 'installed',
|
||||
},
|
||||
]}
|
||||
size="small"
|
||||
value={activeTab}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.scroller} style={{ flex: 1 }}>
|
||||
{activeTab === 'installed' && installedTabItems.length === 0 ? <Empty /> : menu}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
borderBlockStart: `1px solid ${cssVar.colorBorderSecondary}`,
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
<div className={styles.footerItem} onClick={onOpenStore} role="button" tabIndex={0}>
|
||||
<div className={styles.footerItemIcon}>
|
||||
<Icon icon={Store} size={20} />
|
||||
</div>
|
||||
<div className={styles.footerItemContent}>{t('skillStore.title')}</div>
|
||||
<Icon className={styles.trailingIcon} icon={ArrowRight} size={16} />
|
||||
</div>
|
||||
<div
|
||||
className={styles.footerItem}
|
||||
onClick={() => {
|
||||
onClose?.();
|
||||
navigate('/settings/skill');
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className={styles.footerItemIcon}>
|
||||
<Icon icon={Settings} size={20} />
|
||||
</div>
|
||||
<div className={styles.footerItemContent}>{t('tools.plugins.management')}</div>
|
||||
<Icon className={styles.trailingIcon} icon={ExternalLink} size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
PopoverContent.displayName = 'PopoverContent';
|
||||
|
||||
export default PopoverContent;
|
||||
@@ -4,12 +4,13 @@ import { ActionIcon, Block, DropdownMenu, Flexbox, Icon, Modal } from '@lobehub/
|
||||
import { App, Button } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { MoreVerticalIcon, Plus, Trash2 } from 'lucide-react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { Suspense, memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PluginAvatar from '@/components/Plugins/PluginAvatar';
|
||||
import MCPInstallProgress from '@/features/MCP/MCPInstallProgress';
|
||||
import McpDetail from '@/features/MCP/MCPDetail';
|
||||
import McpDetailLoading from '@/features/MCP/MCPDetail/Loading';
|
||||
import MCPInstallProgress from '@/features/MCP/MCPInstallProgress';
|
||||
import { useMarketAuth } from '@/layout/AuthProvider/MarketAuth';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
@@ -17,10 +18,9 @@ import { useToolStore } from '@/store/tool';
|
||||
import { mcpStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
||||
import { type DiscoverMcpItem } from '@/types/discover';
|
||||
|
||||
import { useItemStyles } from '../style';
|
||||
import { itemStyles } from '../style';
|
||||
|
||||
const Item = memo<DiscoverMcpItem>(({ name, description, icon, identifier }) => {
|
||||
const { styles } = useItemStyles();
|
||||
const { t } = useTranslation('plugin');
|
||||
const { modal } = App.useApp();
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
@@ -114,7 +114,7 @@ const Item = memo<DiscoverMcpItem>(({ name, description, icon, identifier }) =>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flexbox className={styles.container} gap={0}>
|
||||
<Flexbox className={itemStyles.container} gap={0}>
|
||||
<Block
|
||||
align={'center'}
|
||||
gap={12}
|
||||
@@ -127,8 +127,8 @@ const Item = memo<DiscoverMcpItem>(({ name, description, icon, identifier }) =>
|
||||
>
|
||||
<PluginAvatar avatar={icon} size={40} />
|
||||
<Flexbox flex={1} gap={4} style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
<span className={styles.title}>{name}</span>
|
||||
{description && <span className={styles.description}>{description}</span>}
|
||||
<span className={itemStyles.title}>{name}</span>
|
||||
{description && <span className={itemStyles.description}>{description}</span>}
|
||||
</Flexbox>
|
||||
<div onClick={(e) => e.stopPropagation()}>{renderAction()}</div>
|
||||
</Block>
|
||||
@@ -147,7 +147,9 @@ const Item = memo<DiscoverMcpItem>(({ name, description, icon, identifier }) =>
|
||||
title={t('dev.title.skillDetails')}
|
||||
width={800}
|
||||
>
|
||||
<McpDetail identifier={identifier} noSettings />
|
||||
<Suspense fallback={<McpDetailLoading />}>
|
||||
<McpDetail identifier={identifier} noSettings />
|
||||
</Suspense>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,13 +6,14 @@ import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import AddSkillButton from './AddSkillButton';
|
||||
|
||||
import CommunityList from './CommunityList';
|
||||
import CustomList from './CustomList';
|
||||
import LobeHubList from './LobeHubList';
|
||||
import Search from './Search';
|
||||
|
||||
export enum SkillStoreTab {
|
||||
Community = 'community',
|
||||
Custom = 'custom',
|
||||
LobeHub = 'lobehub',
|
||||
}
|
||||
|
||||
@@ -24,14 +25,17 @@ export const Content = memo(() => {
|
||||
const options: SegmentedOptions = [
|
||||
{ label: t('skillStore.tabs.lobehub'), value: SkillStoreTab.LobeHub },
|
||||
{ label: t('skillStore.tabs.community'), value: SkillStoreTab.Community },
|
||||
{ label: t('skillStore.tabs.custom'), value: SkillStoreTab.Custom },
|
||||
];
|
||||
|
||||
const isLobeHub = activeTab === SkillStoreTab.LobeHub;
|
||||
const isCommunity = activeTab === SkillStoreTab.Community;
|
||||
const isCustom = activeTab === SkillStoreTab.Custom;
|
||||
|
||||
return (
|
||||
<Flexbox gap={8} style={{ maxHeight: '75vh' }} width={'100%'}>
|
||||
<Flexbox gap={8} paddingInline={16}>
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<Segmented
|
||||
block
|
||||
onChange={(v) => setActiveTab(v as SkillStoreTab)}
|
||||
@@ -47,9 +51,12 @@ export const Content = memo(() => {
|
||||
<Flexbox flex={1} style={{ display: isLobeHub ? 'flex' : 'none', overflow: 'auto' }}>
|
||||
<LobeHubList keywords={lobehubKeywords} />
|
||||
</Flexbox>
|
||||
<Flexbox flex={1} style={{ display: !isLobeHub ? 'flex' : 'none', overflow: 'auto' }}>
|
||||
<Flexbox flex={1} style={{ display: isCommunity ? 'flex' : 'none', overflow: 'auto' }}>
|
||||
<CommunityList />
|
||||
</Flexbox>
|
||||
<Flexbox flex={1} style={{ display: isCustom ? 'flex' : 'none', overflow: 'auto' }}>
|
||||
<CustomList />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon, Block, DropdownMenu, Flexbox, Icon } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { MoreVerticalIcon, PackageSearch, Trash2 } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PluginAvatar from '@/components/Plugins/PluginAvatar';
|
||||
import PluginDetailModal from '@/features/PluginDetailModal';
|
||||
import DevModal from '@/features/PluginDevModal';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
|
||||
import { itemStyles } from '../style';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
title: css`
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: ${cssVar.colorPrimary};
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
interface ItemProps {
|
||||
avatar?: string;
|
||||
description?: string;
|
||||
identifier: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Item = memo<ItemProps>(({ identifier, title, description, avatar }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const { modal } = App.useApp();
|
||||
const [configOpen, setConfigOpen] = useState(false);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
|
||||
const [customPlugin, uninstallPlugin, updateCustomPlugin, pluginManifest] = useToolStore((s) => [
|
||||
pluginSelectors.getCustomPluginById(identifier)(s),
|
||||
s.uninstallPlugin,
|
||||
s.updateCustomPlugin,
|
||||
pluginSelectors.getToolManifestById(identifier)(s),
|
||||
]);
|
||||
|
||||
const [togglePlugin, isPluginEnabledInAgent] = useAgentStore((s) => [
|
||||
s.togglePlugin,
|
||||
agentSelectors.currentAgentPlugins(s).includes(identifier),
|
||||
]);
|
||||
|
||||
const handleDelete = () => {
|
||||
modal.confirm({
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
if (isPluginEnabledInAgent) {
|
||||
await togglePlugin(identifier, false);
|
||||
}
|
||||
await uninstallPlugin(identifier);
|
||||
},
|
||||
title: t('store.actions.confirmUninstall'),
|
||||
type: 'error',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flexbox className={itemStyles.container} gap={0}>
|
||||
<Block
|
||||
align={'center'}
|
||||
gap={12}
|
||||
horizontal
|
||||
paddingBlock={12}
|
||||
paddingInline={12}
|
||||
variant={'filled'}
|
||||
>
|
||||
<PluginAvatar avatar={avatar} size={40} />
|
||||
<Flexbox flex={1} gap={4} style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
<span className={styles.title} onClick={() => setDetailOpen(true)}>
|
||||
{title || identifier}
|
||||
</span>
|
||||
{description && <span className={itemStyles.description}>{description}</span>}
|
||||
</Flexbox>
|
||||
<Flexbox horizontal>
|
||||
<ActionIcon
|
||||
icon={PackageSearch}
|
||||
onClick={() => setConfigOpen(true)}
|
||||
title={t('store.actions.manifest')}
|
||||
/>
|
||||
<DropdownMenu
|
||||
items={[
|
||||
{
|
||||
danger: true,
|
||||
icon: <Icon icon={Trash2} />,
|
||||
key: 'uninstall',
|
||||
label: t('store.actions.uninstall'),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
]}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<ActionIcon icon={MoreVerticalIcon} />
|
||||
</DropdownMenu>
|
||||
</Flexbox>
|
||||
</Block>
|
||||
</Flexbox>
|
||||
{customPlugin && (
|
||||
<DevModal
|
||||
mode="edit"
|
||||
onDelete={async () => {
|
||||
if (isPluginEnabledInAgent) {
|
||||
await togglePlugin(identifier, false);
|
||||
}
|
||||
await uninstallPlugin(identifier);
|
||||
}}
|
||||
onOpenChange={setConfigOpen}
|
||||
onSave={async (value) => {
|
||||
await updateCustomPlugin(identifier, value);
|
||||
}}
|
||||
open={configOpen}
|
||||
value={customPlugin}
|
||||
/>
|
||||
)}
|
||||
<PluginDetailModal
|
||||
id={identifier}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
open={detailOpen}
|
||||
schema={pluginManifest?.settings}
|
||||
tab="info"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Item.displayName = 'CustomListItem';
|
||||
|
||||
export default Item;
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { createStaticStyles, responsive } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
|
||||
import Empty from '../Empty';
|
||||
import Item from './Item';
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
min-height: 50vh;
|
||||
`,
|
||||
grid: css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
padding-block-end: 16px;
|
||||
padding-inline: 16px;
|
||||
|
||||
${responsive.sm} {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
export const CustomList = memo(() => {
|
||||
const customPlugins = useToolStore(pluginSelectors.installedCustomPluginMetaList, isEqual);
|
||||
const searchKeywords = useToolStore((s) => s.customPluginSearchKeywords || '');
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const lowerKeywords = searchKeywords.toLowerCase().trim();
|
||||
if (!lowerKeywords) return customPlugins;
|
||||
|
||||
return customPlugins.filter((plugin) => {
|
||||
const title = plugin.title?.toLowerCase() || '';
|
||||
const identifier = plugin.identifier?.toLowerCase() || '';
|
||||
return title.includes(lowerKeywords) || identifier.includes(lowerKeywords);
|
||||
});
|
||||
}, [customPlugins, searchKeywords]);
|
||||
|
||||
const hasSearchKeywords = Boolean(searchKeywords && searchKeywords.trim());
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
return <Empty search={hasSearchKeywords} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.grid}>
|
||||
{filteredItems.map((plugin) => (
|
||||
<Item
|
||||
avatar={plugin.avatar}
|
||||
description={plugin.description}
|
||||
identifier={plugin.identifier}
|
||||
key={plugin.identifier}
|
||||
title={plugin.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CustomList.displayName = 'CustomList';
|
||||
|
||||
export default CustomList;
|
||||
@@ -3,6 +3,8 @@ import { Plug2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import AddSkillButton from './AddSkillButton';
|
||||
|
||||
interface SkillEmptyProps extends Omit<EmptyProps, 'icon'> {
|
||||
search?: boolean;
|
||||
}
|
||||
@@ -13,6 +15,7 @@ const Empty = memo<SkillEmptyProps>(({ search, ...rest }) => {
|
||||
return (
|
||||
<Center height="100%" style={{ minHeight: '50vh' }} width="100%">
|
||||
<EmptyComponent
|
||||
action={!search && <AddSkillButton />}
|
||||
description={search ? t('skillStore.emptySearch') : t('skillStore.empty')}
|
||||
descriptionProps={{
|
||||
fontSize: 14,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Loader2, MoreVerticalIcon, Plus, Unplug } from 'lucide-react';
|
||||
import React, { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useItemStyles } from '../style';
|
||||
import { itemStyles } from '../style';
|
||||
import { useSkillConnect } from './useSkillConnect';
|
||||
|
||||
interface ItemProps {
|
||||
@@ -25,7 +25,6 @@ interface ItemProps {
|
||||
const Item = memo<ItemProps>(
|
||||
({ description, icon, identifier, label, onOpenDetail, serverName, type }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const { styles } = useItemStyles();
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const { handleConnect, handleDisconnect, isConnected, isConnecting } = useSkillConnect({
|
||||
@@ -91,7 +90,7 @@ const Item = memo<ItemProps>(
|
||||
return (
|
||||
<Block
|
||||
align={'center'}
|
||||
className={styles.container}
|
||||
className={itemStyles.container}
|
||||
gap={12}
|
||||
horizontal
|
||||
onClick={onOpenDetail}
|
||||
@@ -102,9 +101,9 @@ const Item = memo<ItemProps>(
|
||||
>
|
||||
{renderIcon()}
|
||||
<Flexbox flex={1} gap={4} style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
<span className={styles.title}>{label}</span>
|
||||
<span className={itemStyles.title}>{label}</span>
|
||||
{localizedDescription && (
|
||||
<span className={styles.description}>{localizedDescription}</span>
|
||||
<span className={itemStyles.description}>{localizedDescription}</span>
|
||||
)}
|
||||
</Flexbox>
|
||||
<div onClick={(e) => e.stopPropagation()}>{renderAction()}</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS } from '@lobechat/const';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles, responsive } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import type { Klavis } from 'klavis';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
@@ -17,7 +17,7 @@ import Empty from '../Empty';
|
||||
import Item from './Item';
|
||||
import { useSkillConnect } from './useSkillConnect';
|
||||
|
||||
const useStyles = createStyles(({ css }) => ({
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
grid: css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -26,7 +26,7 @@ const useStyles = createStyles(({ css }) => ({
|
||||
padding-block-end: 16px;
|
||||
padding-inline: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
${responsive.sm} {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`,
|
||||
@@ -69,7 +69,6 @@ const DetailModalWithConnect = memo<DetailModalWithConnectProps>(({ detailState,
|
||||
DetailModalWithConnect.displayName = 'DetailModalWithConnect';
|
||||
|
||||
export const LobeHubList = memo<LobeHubListProps>(({ keywords }) => {
|
||||
const { styles } = useStyles();
|
||||
const [detailState, setDetailState] = useState<DetailState | null>(null);
|
||||
|
||||
const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
|
||||
@@ -173,10 +172,7 @@ export const LobeHubList = memo<LobeHubListProps>(({ keywords }) => {
|
||||
})}
|
||||
</div>
|
||||
{detailState && (
|
||||
<DetailModalWithConnect
|
||||
detailState={detailState}
|
||||
onClose={() => setDetailState(null)}
|
||||
/>
|
||||
<DetailModalWithConnect detailState={detailState} onClose={() => setDetailState(null)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox, SearchBar } from '@lobehub/ui';
|
||||
import { SearchBar } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -17,26 +17,26 @@ export const Search = memo<SearchProps>(({ activeTab, onLobeHubSearch }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const mcpKeywords = useToolStore((s) => s.mcpSearchKeywords);
|
||||
|
||||
const isCustomTab = activeTab === SkillStoreTab.Custom;
|
||||
|
||||
const keywords = activeTab === SkillStoreTab.Community ? mcpKeywords : '';
|
||||
|
||||
return (
|
||||
<Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
|
||||
<Flexbox flex={1}>
|
||||
<SearchBar
|
||||
allowClear
|
||||
defaultValue={keywords}
|
||||
onSearch={(keywords: string) => {
|
||||
if (activeTab === SkillStoreTab.Community) {
|
||||
useToolStore.setState({ mcpSearchKeywords: keywords, searchLoading: true });
|
||||
} else {
|
||||
onLobeHubSearch(keywords);
|
||||
}
|
||||
}}
|
||||
placeholder={t('skillStore.search')}
|
||||
variant={'borderless'}
|
||||
/>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<SearchBar
|
||||
allowClear
|
||||
defaultValue={keywords}
|
||||
onSearch={(keywords: string) => {
|
||||
if (activeTab === SkillStoreTab.Community) {
|
||||
useToolStore.setState({ mcpSearchKeywords: keywords, searchLoading: true });
|
||||
} else if (isCustomTab) {
|
||||
useToolStore.setState({ customPluginSearchKeywords: keywords });
|
||||
} else {
|
||||
onLobeHubSearch(keywords);
|
||||
}
|
||||
}}
|
||||
placeholder={t('skillStore.search')}
|
||||
variant={'borderless'}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SkillStore = memo<SkillStoreProps>(({ open, setOpen }) => {
|
||||
body: { overflow: 'hidden', padding: 0 },
|
||||
}}
|
||||
title={t('skillStore.title')}
|
||||
width={'min(80%, 800px)'}
|
||||
width={'min(90%, 900px)'}
|
||||
>
|
||||
<Content />
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
|
||||
export const useItemStyles = createStyles(({ css, token }) => ({
|
||||
export const itemStyles = createStaticStyles(({ css }) => ({
|
||||
container: css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -11,7 +11,7 @@ export const useItemStyles = createStyles(({ css, token }) => ({
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 12px;
|
||||
color: ${token.colorTextSecondary};
|
||||
color: ${cssVar.colorTextSecondary};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
@@ -20,7 +20,7 @@ export const useItemStyles = createStyles(({ css, token }) => ({
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${token.colorText};
|
||||
color: ${cssVar.colorText};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
|
||||
@@ -563,6 +563,7 @@ export default {
|
||||
'skillStore.networkError': 'Network error, please try again',
|
||||
'skillStore.search': 'Search skills by name or keyword, press Enter to search…',
|
||||
'skillStore.tabs.community': 'Community',
|
||||
'skillStore.tabs.custom': 'Custom',
|
||||
'skillStore.tabs.lobehub': 'LobeHub',
|
||||
'skillStore.title': 'Skill Store',
|
||||
'startConversation': 'Start Conversation',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type LobeToolCustomPlugin } from '@/types/tool/plugin';
|
||||
|
||||
export interface CustomPluginState {
|
||||
customPluginSearchKeywords?: string;
|
||||
newCustomPlugin: Partial<LobeToolCustomPlugin>;
|
||||
}
|
||||
export const defaultCustomPlugin: Partial<LobeToolCustomPlugin> = {
|
||||
@@ -13,5 +14,6 @@ export const defaultCustomPlugin: Partial<LobeToolCustomPlugin> = {
|
||||
};
|
||||
|
||||
export const initialCustomPluginState: CustomPluginState = {
|
||||
customPluginSearchKeywords: '',
|
||||
newCustomPlugin: defaultCustomPlugin,
|
||||
};
|
||||
|
||||
@@ -731,6 +731,7 @@ export const createMCPPluginStoreSlice: StateCreator<
|
||||
draft.mcpPluginItems = [];
|
||||
draft.currentPage = 1;
|
||||
draft.mcpSearchKeywords = keywords;
|
||||
draft.isMcpListInit = false;
|
||||
}),
|
||||
false,
|
||||
n('resetMCPPluginList'),
|
||||
|
||||
Reference in New Issue
Block a user