mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
Compare commits
3 Commits
feat/workf
...
feat/add-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b3deec080 | ||
|
|
58ec377413 | ||
|
|
7c50aabe65 |
@@ -14,6 +14,7 @@ export interface IPluginCardVO {
|
|||||||
components: PluginComponent[];
|
components: PluginComponent[];
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
hasUpdate?: boolean;
|
hasUpdate?: boolean;
|
||||||
|
type?: 'plugin' | 'mcp' | 'skill';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PluginCardVO implements IPluginCardVO {
|
export class PluginCardVO implements IPluginCardVO {
|
||||||
@@ -30,6 +31,7 @@ export class PluginCardVO implements IPluginCardVO {
|
|||||||
status: string;
|
status: string;
|
||||||
components: PluginComponent[];
|
components: PluginComponent[];
|
||||||
hasUpdate?: boolean;
|
hasUpdate?: boolean;
|
||||||
|
type?: 'plugin' | 'mcp' | 'skill';
|
||||||
|
|
||||||
constructor(prop: IPluginCardVO) {
|
constructor(prop: IPluginCardVO) {
|
||||||
this.author = prop.author;
|
this.author = prop.author;
|
||||||
@@ -45,5 +47,6 @@ export class PluginCardVO implements IPluginCardVO {
|
|||||||
this.install_source = prop.install_source;
|
this.install_source = prop.install_source;
|
||||||
this.install_info = prop.install_info;
|
this.install_info = prop.install_info;
|
||||||
this.hasUpdate = prop.hasUpdate;
|
this.hasUpdate = prop.hasUpdate;
|
||||||
|
this.type = prop.type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
|
|
||||||
// 转换并比较版本号
|
// 转换并比较版本号
|
||||||
const pluginCards = installedPlugins.map((plugin) => {
|
const pluginCards = installedPlugins.map((plugin) => {
|
||||||
|
const marketplaceKey = `${plugin.manifest.manifest.metadata.author}/${plugin.manifest.manifest.metadata.name}`;
|
||||||
|
const marketplacePlugin = marketplacePluginMap.get(marketplaceKey);
|
||||||
const cardVO = new PluginCardVO({
|
const cardVO = new PluginCardVO({
|
||||||
author: plugin.manifest.manifest.metadata.author ?? '',
|
author: plugin.manifest.manifest.metadata.author ?? '',
|
||||||
label: extractI18nObject(plugin.manifest.manifest.metadata.label),
|
label: extractI18nObject(plugin.manifest.manifest.metadata.label),
|
||||||
@@ -106,13 +108,12 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
priority: plugin.priority,
|
priority: plugin.priority,
|
||||||
install_source: plugin.install_source,
|
install_source: plugin.install_source,
|
||||||
install_info: plugin.install_info,
|
install_info: plugin.install_info,
|
||||||
|
type: marketplacePlugin?.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查是否来自市场且有更新
|
// 检查是否来自市场且有更新
|
||||||
if (cardVO.install_source === 'marketplace') {
|
if (cardVO.install_source === 'marketplace' && marketplacePlugin) {
|
||||||
const marketplaceKey = `${cardVO.author}/${cardVO.name}`;
|
if (marketplacePlugin.latest_version) {
|
||||||
const marketplacePlugin = marketplacePluginMap.get(marketplaceKey);
|
|
||||||
if (marketplacePlugin && marketplacePlugin.latest_version) {
|
|
||||||
cardVO.hasUpdate = isNewerVersion(
|
cardVO.hasUpdate = isNewerVersion(
|
||||||
marketplacePlugin.latest_version,
|
marketplacePlugin.latest_version,
|
||||||
cardVO.version,
|
cardVO.version,
|
||||||
|
|||||||
@@ -60,6 +60,24 @@ export default function PluginCardComponent({
|
|||||||
>
|
>
|
||||||
v{cardVO.version}
|
v{cardVO.version}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{cardVO.type && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`text-[0.7rem] flex-shrink-0 ${
|
||||||
|
cardVO.type === 'mcp'
|
||||||
|
? 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'
|
||||||
|
: cardVO.type === 'skill'
|
||||||
|
? 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'
|
||||||
|
: 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cardVO.type === 'mcp'
|
||||||
|
? 'MCP'
|
||||||
|
: cardVO.type === 'skill'
|
||||||
|
? t('common.skill')
|
||||||
|
: t('market.typePlugin')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{cardVO.debug && (
|
{cardVO.debug && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { TFunction } from 'i18next';
|
||||||
|
import { Wrench, AudioWaveform, Hash, Book, FileText } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
export default function PluginComponentList({
|
||||||
|
components,
|
||||||
|
showComponentName,
|
||||||
|
showTitle,
|
||||||
|
useBadge,
|
||||||
|
t,
|
||||||
|
responsive = false,
|
||||||
|
}: {
|
||||||
|
components: Record<string, number>;
|
||||||
|
showComponentName: boolean;
|
||||||
|
showTitle: boolean;
|
||||||
|
useBadge: boolean;
|
||||||
|
t: TFunction;
|
||||||
|
responsive?: boolean;
|
||||||
|
}) {
|
||||||
|
const kindIconMap: Record<string, React.ReactNode> = {
|
||||||
|
Tool: <Wrench className="w-5 h-5" />,
|
||||||
|
EventListener: <AudioWaveform className="w-5 h-5" />,
|
||||||
|
Command: <Hash className="w-5 h-5" />,
|
||||||
|
KnowledgeEngine: <Book className="w-5 h-5" />,
|
||||||
|
Parser: <FileText className="w-5 h-5" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentKindList = Object.keys(components || {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showTitle && <div>{t('market.componentsList')}</div>}
|
||||||
|
{componentKindList.length > 0 && (
|
||||||
|
<>
|
||||||
|
{componentKindList.map((kind) => {
|
||||||
|
return (
|
||||||
|
<Fragment key={kind}>
|
||||||
|
{useBadge && (
|
||||||
|
<Badge variant="outline" className="flex items-center gap-1">
|
||||||
|
{kindIconMap[kind]}
|
||||||
|
{responsive ? (
|
||||||
|
<span className="hidden md:inline">
|
||||||
|
{t('market.componentName.' + kind)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
showComponentName && t('market.componentName.' + kind)
|
||||||
|
)}
|
||||||
|
<span className="ml-1">{components[kind]}</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!useBadge && (
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-start gap-[0.2rem]"
|
||||||
|
>
|
||||||
|
{kindIconMap[kind]}
|
||||||
|
{responsive ? (
|
||||||
|
<span className="hidden md:inline">
|
||||||
|
{t('market.componentName.' + kind)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
showComponentName && t('market.componentName.' + kind)
|
||||||
|
)}
|
||||||
|
<span className="ml-1">{components[kind]}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{componentKindList.length === 0 && <div>{t('market.noComponents')}</div>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,14 +8,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import {
|
||||||
|
ToggleGroup,
|
||||||
|
ToggleGroupItem,
|
||||||
|
} from '@/components/ui/toggle-group';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Wrench,
|
Wrench,
|
||||||
AudioWaveform,
|
AudioWaveform,
|
||||||
Hash,
|
|
||||||
Book,
|
Book,
|
||||||
FileText,
|
SlidersHorizontal,
|
||||||
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
|
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
|
||||||
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
||||||
@@ -26,6 +35,7 @@ import { extractI18nObject } from '@/i18n/I18nProvider';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
|
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
|
||||||
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { TagsFilter } from './TagsFilter';
|
import { TagsFilter } from './TagsFilter';
|
||||||
import { PluginTag } from '@/app/infra/http/CloudServiceClient';
|
import { PluginTag } from '@/app/infra/http/CloudServiceClient';
|
||||||
|
|
||||||
@@ -55,6 +65,15 @@ function MarketPageContent({
|
|||||||
'Parser',
|
'Parser',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const validTypes = ['plugin', 'mcp', 'skill'];
|
||||||
|
|
||||||
|
const extensionTypeOptions = [
|
||||||
|
{ value: 'all', label: t('market.filters.allFormats'), icon: null },
|
||||||
|
{ value: 'plugin', label: t('market.typePlugin'), icon: Wrench },
|
||||||
|
{ value: 'mcp', label: t('market.typeMCP'), icon: AudioWaveform },
|
||||||
|
{ value: 'skill', label: t('market.typeSkill'), icon: Book },
|
||||||
|
];
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [componentFilter, setComponentFilter] = useState<string>(() => {
|
const [componentFilter, setComponentFilter] = useState<string>(() => {
|
||||||
const category = searchParams.get('category');
|
const category = searchParams.get('category');
|
||||||
@@ -63,6 +82,14 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
return 'all';
|
return 'all';
|
||||||
});
|
});
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>(() => {
|
||||||
|
const type = searchParams.get('type');
|
||||||
|
if (type && validTypes.includes(type)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
return 'all';
|
||||||
|
});
|
||||||
|
const activeAdvancedFilters = typeFilter === 'all' ? 0 : 1;
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
|
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
|
||||||
const [tagNames, setTagNames] = useState<Record<string, string>>({});
|
const [tagNames, setTagNames] = useState<Record<string, string>>({});
|
||||||
@@ -136,9 +163,44 @@ function MarketPageContent({
|
|||||||
version: plugin.latest_version,
|
version: plugin.latest_version,
|
||||||
components: plugin.components,
|
components: plugin.components,
|
||||||
tags: plugin.tags || [],
|
tags: plugin.tags || [],
|
||||||
|
type: plugin.type,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const transformMCPToVO = useCallback((mcp: any): PluginMarketCardVO => {
|
||||||
|
return new PluginMarketCardVO({
|
||||||
|
pluginId: mcp.author + ' / ' + mcp.name,
|
||||||
|
author: mcp.author,
|
||||||
|
pluginName: mcp.name,
|
||||||
|
label: extractI18nObject(mcp.label),
|
||||||
|
description: extractI18nObject(mcp.description) || t('market.noDescription'),
|
||||||
|
installCount: mcp.install_count || 0,
|
||||||
|
iconURL: mcp.icon || getCloudServiceClientSync().getPluginIconURL(mcp.author, mcp.name),
|
||||||
|
githubURL: mcp.repository,
|
||||||
|
version: mcp.latest_version,
|
||||||
|
components: mcp.components || {},
|
||||||
|
tags: mcp.tags || [],
|
||||||
|
type: 'mcp',
|
||||||
|
});
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const transformSkillToVO = useCallback((skill: any): PluginMarketCardVO => {
|
||||||
|
return new PluginMarketCardVO({
|
||||||
|
pluginId: skill.author + ' / ' + skill.name,
|
||||||
|
author: skill.author,
|
||||||
|
pluginName: skill.name,
|
||||||
|
label: extractI18nObject(skill.label),
|
||||||
|
description: extractI18nObject(skill.description) || t('market.noDescription'),
|
||||||
|
installCount: skill.install_count || 0,
|
||||||
|
iconURL: skill.icon || getCloudServiceClientSync().getPluginIconURL(skill.author, skill.name),
|
||||||
|
githubURL: skill.repository,
|
||||||
|
version: skill.latest_version,
|
||||||
|
components: skill.components || {},
|
||||||
|
tags: skill.tags || [],
|
||||||
|
type: 'skill',
|
||||||
|
});
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
// 获取插件列表
|
// 获取插件列表
|
||||||
const fetchPlugins = useCallback(
|
const fetchPlugins = useCallback(
|
||||||
async (page: number, isSearch: boolean = false, reset: boolean = false) => {
|
async (page: number, isSearch: boolean = false, reset: boolean = false) => {
|
||||||
@@ -152,30 +214,98 @@ function MarketPageContent({
|
|||||||
const { sortBy, sortOrder } = getCurrentSort();
|
const { sortBy, sortOrder } = getCurrentSort();
|
||||||
const filterValue =
|
const filterValue =
|
||||||
componentFilter === 'all' ? undefined : componentFilter;
|
componentFilter === 'all' ? undefined : componentFilter;
|
||||||
|
const query = isSearch && searchQuery.trim() ? searchQuery.trim() : '';
|
||||||
|
|
||||||
// Always use searchMarketplacePlugins to support component filtering and tags filtering
|
let newPlugins: PluginMarketCardVO[] = [];
|
||||||
const response =
|
let total = 0;
|
||||||
await getCloudServiceClientSync().searchMarketplacePlugins(
|
|
||||||
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
|
if (typeFilter === 'all') {
|
||||||
|
let pluginsResult: PluginMarketCardVO[] = [];
|
||||||
|
let mcpsResult: PluginMarketCardVO[] = [];
|
||||||
|
let skillsResult: PluginMarketCardVO[] = [];
|
||||||
|
let pluginsTotal = 0;
|
||||||
|
let mcpsTotal = 0;
|
||||||
|
let skillsTotal = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pluginsResponse = await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
filterValue,
|
||||||
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
|
'plugin',
|
||||||
|
);
|
||||||
|
pluginsResult = pluginsResponse.plugins
|
||||||
|
.filter((plugin) => {
|
||||||
|
const keys = Object.keys(plugin.components || {});
|
||||||
|
return !(keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever'));
|
||||||
|
})
|
||||||
|
.map(transformToVO);
|
||||||
|
pluginsTotal = pluginsResponse.total || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch plugins:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mcpsResponse = await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
filterValue,
|
||||||
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
|
'mcp',
|
||||||
|
);
|
||||||
|
mcpsResult = (mcpsResponse.plugins || []).map(transformMCPToVO);
|
||||||
|
mcpsTotal = mcpsResponse.total || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch mcps:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const skillsResponse = await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
filterValue,
|
||||||
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
|
'skill',
|
||||||
|
);
|
||||||
|
skillsResult = (skillsResponse.plugins || []).map(transformSkillToVO);
|
||||||
|
skillsTotal = skillsResponse.total || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch skills:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
newPlugins = [...pluginsResult, ...mcpsResult, ...skillsResult];
|
||||||
|
total = pluginsTotal + mcpsTotal + skillsTotal;
|
||||||
|
} else {
|
||||||
|
const response = await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
|
query,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
filterValue,
|
filterValue,
|
||||||
selectedTags.length > 0 ? selectedTags : undefined,
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
|
typeFilter === 'all' ? undefined : typeFilter,
|
||||||
);
|
);
|
||||||
|
|
||||||
const data: ApiRespMarketplacePlugins = response;
|
const data: ApiRespMarketplacePlugins = response;
|
||||||
const newPlugins = data.plugins
|
newPlugins = data.plugins
|
||||||
.filter((plugin) => {
|
.filter((plugin) => {
|
||||||
// Hide plugins that only contain deprecated KnowledgeRetriever components
|
const keys = Object.keys(plugin.components || {});
|
||||||
const keys = Object.keys(plugin.components || {});
|
return !(keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever'));
|
||||||
return !(
|
})
|
||||||
keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever')
|
.map(transformToVO);
|
||||||
);
|
total = data.total;
|
||||||
})
|
}
|
||||||
.map(transformToVO);
|
|
||||||
const total = data.total;
|
|
||||||
|
|
||||||
if (reset || page === 1) {
|
if (reset || page === 1) {
|
||||||
setPlugins(newPlugins);
|
setPlugins(newPlugins);
|
||||||
@@ -185,8 +315,8 @@ function MarketPageContent({
|
|||||||
|
|
||||||
setTotal(total);
|
setTotal(total);
|
||||||
setHasMore(
|
setHasMore(
|
||||||
data.plugins.length === pageSize &&
|
newPlugins.length > 0 &&
|
||||||
plugins.length + newPlugins.length < total,
|
(reset || page === 1 ? newPlugins.length : plugins.length + newPlugins.length) < total,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch plugins:', error);
|
console.error('Failed to fetch plugins:', error);
|
||||||
@@ -202,8 +332,11 @@ function MarketPageContent({
|
|||||||
selectedTags,
|
selectedTags,
|
||||||
pageSize,
|
pageSize,
|
||||||
transformToVO,
|
transformToVO,
|
||||||
|
transformMCPToVO,
|
||||||
|
transformSkillToVO,
|
||||||
plugins.length,
|
plugins.length,
|
||||||
getCurrentSort,
|
getCurrentSort,
|
||||||
|
typeFilter,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -313,10 +446,29 @@ function MarketPageContent({
|
|||||||
// fetchPlugins will be called by useEffect when componentFilter changes
|
// fetchPlugins will be called by useEffect when componentFilter changes
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle type filter change
|
||||||
|
const handleTypeFilterChange = useCallback((value: string) => {
|
||||||
|
setTypeFilter(value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setPlugins([]);
|
||||||
|
|
||||||
|
// Update URL query param to keep it in sync
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (value === 'all') {
|
||||||
|
params.delete('type');
|
||||||
|
} else {
|
||||||
|
params.set('type', value);
|
||||||
|
}
|
||||||
|
const newUrl = params.toString()
|
||||||
|
? `${window.location.pathname}?${params.toString()}`
|
||||||
|
: window.location.pathname;
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 当排序选项或组件筛选变化时重新加载数据
|
// 当排序选项或组件筛选变化时重新加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPlugins(1, !!searchQuery.trim(), true);
|
fetchPlugins(1, !!searchQuery.trim(), true);
|
||||||
}, [sortOption, componentFilter]);
|
}, [sortOption, componentFilter, typeFilter]);
|
||||||
|
|
||||||
// Tags 筛选变化时重新搜索
|
// Tags 筛选变化时重新搜索
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -429,9 +581,9 @@ function MarketPageContent({
|
|||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Fixed header with search and sort controls */}
|
{/* Fixed header with search and sort controls */}
|
||||||
<div className="flex-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
|
<div className="flex-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
|
||||||
{/* Search box and Tags filter */}
|
{/* Search box */}
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
<div className="flex flex-col lg:flex-row items-stretch lg:items-center justify-center gap-3">
|
||||||
<div className="relative w-full max-w-2xl">
|
<div className="relative w-full lg:max-w-xl">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('market.searchPlaceholder')}
|
placeholder={t('market.searchPlaceholder')}
|
||||||
@@ -446,7 +598,6 @@ function MarketPageContent({
|
|||||||
}}
|
}}
|
||||||
onKeyPress={(e) => {
|
onKeyPress={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
// Immediately search, clear debounce timer
|
|
||||||
if (searchTimeoutRef.current) {
|
if (searchTimeoutRef.current) {
|
||||||
clearTimeout(searchTimeoutRef.current);
|
clearTimeout(searchTimeoutRef.current);
|
||||||
}
|
}
|
||||||
@@ -457,90 +608,9 @@ function MarketPageContent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tags filter */}
|
<div className="flex w-full items-center justify-end gap-2 lg:w-auto">
|
||||||
<TagsFilter
|
|
||||||
availableTags={availableTags}
|
|
||||||
selectedTags={selectedTags}
|
|
||||||
onTagsChange={handleTagsChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Component filter and sort */}
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-3 sm:px-4">
|
|
||||||
{/* Component filter */}
|
|
||||||
<div className="flex flex-col sm:flex-row items-center gap-2 min-w-0 max-w-full">
|
|
||||||
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
{t('market.filterByComponent')}:
|
|
||||||
</span>
|
|
||||||
<div className="overflow-x-auto max-w-full [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
|
||||||
<ToggleGroup
|
|
||||||
type="single"
|
|
||||||
spacing={2}
|
|
||||||
size="sm"
|
|
||||||
value={componentFilter}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value) handleComponentFilterChange(value);
|
|
||||||
}}
|
|
||||||
className="justify-start flex-nowrap"
|
|
||||||
>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="all"
|
|
||||||
aria-label="All components"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
{t('market.allComponents')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="Tool"
|
|
||||||
aria-label="Tool"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
<Wrench className="h-4 w-4 mr-1" />
|
|
||||||
{t('plugins.componentName.Tool')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="Command"
|
|
||||||
aria-label="Command"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
<Hash className="h-4 w-4 mr-1" />
|
|
||||||
{t('plugins.componentName.Command')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="EventListener"
|
|
||||||
aria-label="EventListener"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
<AudioWaveform className="h-4 w-4 mr-1" />
|
|
||||||
{t('plugins.componentName.EventListener')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="KnowledgeEngine"
|
|
||||||
aria-label="KnowledgeEngine"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
<Book className="h-4 w-4 mr-1" />
|
|
||||||
{t('plugins.componentName.KnowledgeEngine')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="Parser"
|
|
||||||
aria-label="Parser"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
<FileText className="h-4 w-4 mr-1" />
|
|
||||||
{t('plugins.componentName.Parser')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
</ToggleGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sort dropdown */}
|
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
|
||||||
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
{t('market.sortBy')}:
|
|
||||||
</span>
|
|
||||||
<Select value={sortOption} onValueChange={handleSortChange}>
|
<Select value={sortOption} onValueChange={handleSortChange}>
|
||||||
<SelectTrigger className="w-40 sm:w-48 text-xs sm:text-sm">
|
<SelectTrigger className="w-[128px] sm:w-40 text-xs sm:text-sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -551,9 +621,96 @@ function MarketPageContent({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" className="relative">
|
||||||
|
<SlidersHorizontal className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">{t('market.filters.more')}</span>
|
||||||
|
{activeAdvancedFilters > 0 && (
|
||||||
|
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] leading-none text-primary-foreground">
|
||||||
|
{activeAdvancedFilters}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-[320px] space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">{t('market.filters.advancedTitle')}</div>
|
||||||
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{t('market.filters.advancedDescription')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
|
{t('market.filters.technicalType')}
|
||||||
|
</div>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
spacing={2}
|
||||||
|
size="sm"
|
||||||
|
value={typeFilter}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value) handleTypeFilterChange(value);
|
||||||
|
}}
|
||||||
|
className="flex flex-wrap justify-start gap-2"
|
||||||
|
>
|
||||||
|
{extensionTypeOptions.map((option) => {
|
||||||
|
const Icon = option.icon;
|
||||||
|
return (
|
||||||
|
<ToggleGroupItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
aria-label={option.label}
|
||||||
|
className="cursor-pointer text-xs"
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="mr-1 h-3.5 w-3.5" />}
|
||||||
|
{option.label}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Quick tag filter buttons */}
|
||||||
|
<div className="mx-auto flex w-full max-w-4xl items-center gap-2 overflow-x-auto pb-1 sm:flex-wrap sm:justify-center sm:overflow-visible">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={selectedTags.length === 0 ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-8 shrink-0"
|
||||||
|
onClick={() => handleTagsChange([])}
|
||||||
|
>
|
||||||
|
{t('market.allExtensions')}
|
||||||
|
</Button>
|
||||||
|
{availableTags.map((tag) => {
|
||||||
|
const selected = selectedTags.includes(tag.tag);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={tag.tag}
|
||||||
|
type="button"
|
||||||
|
variant={selected ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-8 shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
const newTags = selected
|
||||||
|
? selectedTags.filter((t) => t !== tag.tag)
|
||||||
|
: [...selectedTags, tag.tag];
|
||||||
|
handleTagsChange(newTags);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tagNames[tag.tag] || tag.tag}
|
||||||
|
{selected && <X className="h-3.5 w-3.5" />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search results stats */}
|
{/* Search results stats */}
|
||||||
{total > 0 && (
|
{total > 0 && (
|
||||||
<div className="text-center text-muted-foreground text-sm">
|
<div className="text-center text-muted-foreground text-sm">
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function pluginToVO(
|
|||||||
version: plugin.latest_version,
|
version: plugin.latest_version,
|
||||||
components: plugin.components,
|
components: plugin.components,
|
||||||
tags: plugin.tags || [],
|
tags: plugin.tags || [],
|
||||||
|
type: plugin.type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { PluginMarketCardVO } from './PluginMarketCardVO';
|
import { PluginMarketCardVO } from './PluginMarketCardVO';
|
||||||
|
import { useRef, useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import PluginComponentList from '../PluginComponentList';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Info, Package } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Wrench,
|
Tooltip,
|
||||||
AudioWaveform,
|
TooltipContent,
|
||||||
Hash,
|
TooltipProvider,
|
||||||
Download,
|
TooltipTrigger,
|
||||||
ExternalLink,
|
} from '@/components/ui/tooltip';
|
||||||
Book,
|
|
||||||
FileText,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
export default function PluginMarketCardComponent({
|
export default function PluginMarketCardComponent({
|
||||||
cardVO,
|
cardVO,
|
||||||
@@ -23,11 +21,24 @@ export default function PluginMarketCardComponent({
|
|||||||
tagNames?: Record<string, string>;
|
tagNames?: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const [visibleTags, setVisibleTags] = useState(2);
|
const [visibleTags, setVisibleTags] = useState(2);
|
||||||
|
const [iconFailed, setIconFailed] = useState(!cardVO.iconURL);
|
||||||
|
|
||||||
|
const pluginDetailUrl = `https://space.langbot.app/market/${cardVO.author}/${cardVO.pluginName}`;
|
||||||
|
|
||||||
|
const isDeprecated = (() => {
|
||||||
|
if (!cardVO.components) return false;
|
||||||
|
const keys = Object.keys(cardVO.components);
|
||||||
|
return keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever');
|
||||||
|
})();
|
||||||
|
|
||||||
|
const showTypeBadge = cardVO.type;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIconFailed(!cardVO.iconURL);
|
||||||
|
}, [cardVO.iconURL]);
|
||||||
|
|
||||||
// Measure how many tags fit in the bottom row
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tags = cardVO.tags;
|
const tags = cardVO.tags;
|
||||||
if (!bottomRef.current || !tags || tags.length === 0) return;
|
if (!bottomRef.current || !tags || tags.length === 0) return;
|
||||||
@@ -43,10 +54,7 @@ export default function PluginMarketCardComponent({
|
|||||||
}
|
}
|
||||||
const tagWidth = 80;
|
const tagWidth = 80;
|
||||||
const plusBadgeWidth = 40;
|
const plusBadgeWidth = 40;
|
||||||
const maxTags = Math.max(
|
const maxTags = Math.max(0, Math.floor((availableForTags - plusBadgeWidth) / tagWidth));
|
||||||
0,
|
|
||||||
Math.floor((availableForTags - plusBadgeWidth) / tagWidth),
|
|
||||||
);
|
|
||||||
if (maxTags >= tags.length) {
|
if (maxTags >= tags.length) {
|
||||||
setVisibleTags(tags.length);
|
setVisibleTags(tags.length);
|
||||||
} else {
|
} else {
|
||||||
@@ -62,51 +70,72 @@ export default function PluginMarketCardComponent({
|
|||||||
|
|
||||||
const remainingTags = cardVO.tags ? cardVO.tags.length - visibleTags : 0;
|
const remainingTags = cardVO.tags ? cardVO.tags.length - visibleTags : 0;
|
||||||
|
|
||||||
function handleInstallClick(e: React.MouseEvent) {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onInstall) {
|
|
||||||
onInstall(cardVO.author, cardVO.pluginName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleViewDetailsClick(e: React.MouseEvent) {
|
|
||||||
e.stopPropagation();
|
|
||||||
const detailUrl = `https://space.langbot.app/market/${cardVO.author}/${cardVO.pluginName}`;
|
|
||||||
window.open(detailUrl, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
const kindIconMap: Record<string, React.ReactNode> = {
|
|
||||||
Tool: <Wrench className="w-4 h-4" />,
|
|
||||||
EventListener: <AudioWaveform className="w-4 h-4" />,
|
|
||||||
Command: <Hash className="w-4 h-4" />,
|
|
||||||
KnowledgeEngine: <Book className="w-4 h-4" />,
|
|
||||||
Parser: <FileText className="w-4 h-4" />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<a
|
||||||
className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] bg-white rounded-[10px] border border-[#e4e4e7] dark:border-[#27272a] p-3 sm:p-[1rem] hover:border-[#a1a1aa] dark:hover:border-[#3f3f46] transition-all duration-200 dark:bg-[#1f1f22] relative"
|
href={pluginDetailUrl}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
target="_blank"
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
rel="noopener noreferrer"
|
||||||
|
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] cursor-pointer hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.15)] transition-shadow duration-200 dark:bg-[#1f1f22] dark:shadow-[0px_0px_4px_0_rgba(255,255,255,0.1)] dark:hover:shadow-[0px_2px_8px_0_rgba(255,255,255,0.15)] block"
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex flex-col justify-between gap-3">
|
<div className="w-full h-full flex flex-col justify-between">
|
||||||
{/* 上部分:插件信息 */}
|
<div className="flex flex-row items-start justify-start gap-2 sm:gap-[1.2rem] min-h-0 flex-1 overflow-hidden">
|
||||||
<div className="flex flex-row items-start justify-start gap-2 sm:gap-[1.2rem] min-h-0">
|
{iconFailed ? (
|
||||||
<img
|
<div className="w-12 h-12 sm:w-16 sm:h-16 flex-shrink-0 rounded-[8%] border bg-muted text-muted-foreground flex items-center justify-center">
|
||||||
src={cardVO.iconURL}
|
<Package className="w-6 h-6 sm:w-8 sm:h-8" />
|
||||||
alt="plugin icon"
|
</div>
|
||||||
className="w-12 h-12 sm:w-16 sm:h-16 flex-shrink-0 rounded-[8%]"
|
) : (
|
||||||
/>
|
<img
|
||||||
|
src={cardVO.iconURL}
|
||||||
|
alt="plugin icon"
|
||||||
|
className="w-12 h-12 sm:w-16 sm:h-16 flex-shrink-0 rounded-[8%] object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="low"
|
||||||
|
onError={() => setIconFailed(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col items-start justify-start gap-[0.4rem] sm:gap-[0.6rem] min-w-0 overflow-hidden">
|
<div className="flex-1 flex flex-col items-start justify-start gap-[0.4rem] sm:gap-[0.6rem] min-w-0 overflow-hidden">
|
||||||
<div className="flex flex-col items-start justify-start w-full min-w-0">
|
<div className="flex flex-col items-start justify-start w-full min-w-0">
|
||||||
<div className="text-[0.65rem] sm:text-[0.7rem] text-[#666] dark:text-[#999] truncate w-full">
|
<div className="text-[0.65rem] sm:text-[0.7rem] text-[#666] dark:text-[#999] truncate w-full">{cardVO.pluginId}</div>
|
||||||
{cardVO.pluginId}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 w-full min-w-0">
|
<div className="flex items-center gap-1.5 w-full min-w-0">
|
||||||
<div className="text-base sm:text-[1.2rem] text-black dark:text-[#f0f0f0] truncate">
|
<div className="text-base sm:text-[1.2rem] text-black dark:text-[#f0f0f0] truncate">{cardVO.label}</div>
|
||||||
{cardVO.label}
|
{isDeprecated && (
|
||||||
</div>
|
<TooltipProvider delayDuration={200}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild onClick={(e) => e.preventDefault()}>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[0.6rem] px-1.5 py-0 h-4 flex-shrink-0 border-red-400 text-red-500 dark:border-red-500 dark:text-red-400 gap-0.5 cursor-help"
|
||||||
|
>
|
||||||
|
{t('market.deprecated')}
|
||||||
|
<Info className="w-2.5 h-2.5" />
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-[240px] text-xs">
|
||||||
|
{t('market.deprecatedTooltip')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
{showTypeBadge && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`text-[0.6rem] px-1.5 py-0 h-4 flex-shrink-0 gap-0.5 ${
|
||||||
|
cardVO.type === 'mcp'
|
||||||
|
? 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'
|
||||||
|
: cardVO.type === 'skill'
|
||||||
|
? 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'
|
||||||
|
: 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cardVO.type === 'mcp'
|
||||||
|
? 'MCP'
|
||||||
|
: cardVO.type === 'skill'
|
||||||
|
? t('common.skill')
|
||||||
|
: t('market.typePlugin')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -118,11 +147,12 @@ export default function PluginMarketCardComponent({
|
|||||||
<div className="flex flex-row items-start justify-center gap-[0.4rem] flex-shrink-0">
|
<div className="flex flex-row items-start justify-center gap-[0.4rem] flex-shrink-0">
|
||||||
{cardVO.githubURL && (
|
{cardVO.githubURL && (
|
||||||
<svg
|
<svg
|
||||||
className="w-5 h-5 sm:w-[1.4rem] sm:h-[1.4rem] text-black cursor-pointer hover:text-gray-600 dark:text-[#f0f0f0] flex-shrink-0"
|
className="w-5 h-5 sm:w-[1.4rem] sm:h-[1.4rem] text-black cursor-pointer hover:text-gray-600 dark:text-[#f0f0f0] dark:hover:text-[#c0c0c0] flex-shrink-0"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
window.open(cardVO.githubURL, '_blank');
|
window.open(cardVO.githubURL, '_blank');
|
||||||
}}
|
}}
|
||||||
@@ -133,13 +163,8 @@ export default function PluginMarketCardComponent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 下部分:下载量、标签和组件列表 */}
|
<div ref={bottomRef} className="w-full flex flex-row items-center justify-between gap-2 px-0 sm:px-[0.4rem] flex-shrink-0 overflow-hidden">
|
||||||
<div
|
|
||||||
ref={bottomRef}
|
|
||||||
className="w-full flex flex-row items-center justify-between gap-2 px-0 sm:px-[0.4rem] flex-shrink-0 overflow-hidden"
|
|
||||||
>
|
|
||||||
<div className="flex flex-row items-center justify-start gap-2 min-w-0 overflow-hidden">
|
<div className="flex flex-row items-center justify-start gap-2 min-w-0 overflow-hidden">
|
||||||
{/* 下载数量 */}
|
|
||||||
<div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem] flex-shrink-0">
|
<div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem] flex-shrink-0">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] dark:text-[#5b8def] flex-shrink-0"
|
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] dark:text-[#5b8def] flex-shrink-0"
|
||||||
@@ -158,7 +183,6 @@ export default function PluginMarketCardComponent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tags - adaptive */}
|
|
||||||
{cardVO.tags && cardVO.tags.length > 0 && visibleTags > 0 && (
|
{cardVO.tags && cardVO.tags.length > 0 && visibleTags > 0 && (
|
||||||
<div className="flex flex-row items-center gap-1.5 overflow-hidden flex-shrink min-w-0">
|
<div className="flex flex-row items-center gap-1.5 overflow-hidden flex-shrink min-w-0">
|
||||||
{cardVO.tags.slice(0, visibleTags).map((tag) => (
|
{cardVO.tags.slice(0, visibleTags).map((tag) => (
|
||||||
@@ -180,9 +204,7 @@ export default function PluginMarketCardComponent({
|
|||||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
|
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
|
||||||
<line x1="7" y1="7" x2="7.01" y2="7" />
|
<line x1="7" y1="7" x2="7.01" y2="7" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="truncate max-w-[5rem]">
|
<span className="truncate max-w-[5rem]">{tagNames[tag] || tag}</span>
|
||||||
{tagNames[tag] || tag}
|
|
||||||
</span>
|
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{remainingTags > 0 && (
|
{remainingTags > 0 && (
|
||||||
@@ -197,52 +219,20 @@ export default function PluginMarketCardComponent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 组件列表 */}
|
|
||||||
{cardVO.components && Object.keys(cardVO.components).length > 0 && (
|
{cardVO.components && Object.keys(cardVO.components).length > 0 && (
|
||||||
<div className="flex flex-row items-center gap-1">
|
<div className="flex flex-row items-center gap-1 flex-shrink-0">
|
||||||
{Object.entries(cardVO.components).map(([kind, count]) => (
|
<PluginComponentList
|
||||||
<Badge
|
components={cardVO.components}
|
||||||
key={kind}
|
showComponentName={false}
|
||||||
variant="outline"
|
showTitle={false}
|
||||||
className="flex items-center gap-1"
|
useBadge={true}
|
||||||
>
|
t={t}
|
||||||
{kindIconMap[kind]}
|
responsive={false}
|
||||||
<span className="ml-1">{count}</span>
|
/>
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
{/* Hover overlay with action buttons */}
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 bg-gray-100/55 dark:bg-black/35 rounded-[10px] flex items-center justify-center gap-3 transition-all duration-200 ${
|
|
||||||
isHovered ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={handleInstallClick}
|
|
||||||
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
|
|
||||||
isHovered ? 'translate-y-0 opacity-100' : 'translate-y-1 opacity-0'
|
|
||||||
}`}
|
|
||||||
style={{ transitionDelay: isHovered ? '10ms' : '0ms' }}
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
{t('market.install')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleViewDetailsClick}
|
|
||||||
variant="outline"
|
|
||||||
className={`bg-white hover:bg-gray-100 text-gray-900 dark:bg-white dark:hover:bg-gray-100 dark:text-gray-900 px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
|
|
||||||
isHovered ? 'translate-y-0 opacity-100' : 'translate-y-1 opacity-0'
|
|
||||||
}`}
|
|
||||||
style={{ transitionDelay: isHovered ? '20ms' : '0ms' }}
|
|
||||||
>
|
|
||||||
<ExternalLink className="w-4 h-4" />
|
|
||||||
{t('market.viewDetails')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ export interface IPluginMarketCardVO {
|
|||||||
version: string;
|
version: string;
|
||||||
components?: Record<string, number>;
|
components?: Record<string, number>;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
type?: 'plugin' | 'mcp' | 'skill';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||||
@@ -24,6 +25,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
|||||||
version: string;
|
version: string;
|
||||||
components?: Record<string, number>;
|
components?: Record<string, number>;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
type?: 'plugin' | 'mcp' | 'skill';
|
||||||
|
|
||||||
constructor(prop: IPluginMarketCardVO) {
|
constructor(prop: IPluginMarketCardVO) {
|
||||||
this.description = prop.description;
|
this.description = prop.description;
|
||||||
@@ -37,5 +39,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
|||||||
this.version = prop.version;
|
this.version = prop.version;
|
||||||
this.components = prop.components;
|
this.components = prop.components;
|
||||||
this.tags = prop.tags;
|
this.tags = prop.tags;
|
||||||
|
this.type = prop.type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface PluginV4 {
|
|||||||
latest_version: string;
|
latest_version: string;
|
||||||
components: Record<string, number>;
|
components: Record<string, number>;
|
||||||
status: PluginV4Status;
|
status: PluginV4Status;
|
||||||
|
type?: 'plugin' | 'mcp' | 'skill';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,49 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
sort_order?: string,
|
sort_order?: string,
|
||||||
component_filter?: string,
|
component_filter?: string,
|
||||||
tags_filter?: string[],
|
tags_filter?: string[],
|
||||||
|
type_filter?: string,
|
||||||
): Promise<ApiRespMarketplacePlugins> {
|
): Promise<ApiRespMarketplacePlugins> {
|
||||||
|
// Use different endpoints based on type_filter
|
||||||
|
if (type_filter === 'mcp') {
|
||||||
|
return this.post<{ mcps: PluginV4[]; total: number }>(
|
||||||
|
'/api/v1/marketplace/mcps/search',
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
sort_by,
|
||||||
|
sort_order,
|
||||||
|
tags_filter,
|
||||||
|
},
|
||||||
|
).then((resp) => ({
|
||||||
|
plugins: (resp?.mcps || []).map((mcp) => ({
|
||||||
|
...mcp,
|
||||||
|
plugin_id: mcp.mcp_id || mcp.plugin_id,
|
||||||
|
type: 'mcp' as const,
|
||||||
|
})),
|
||||||
|
total: resp?.total || 0,
|
||||||
|
}));
|
||||||
|
} else if (type_filter === 'skill') {
|
||||||
|
return this.post<{ skills: PluginV4[]; total: number }>(
|
||||||
|
'/api/v1/marketplace/skills/search',
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
sort_by,
|
||||||
|
sort_order,
|
||||||
|
tags_filter,
|
||||||
|
},
|
||||||
|
).then((resp) => ({
|
||||||
|
plugins: (resp?.skills || []).map((skill) => ({
|
||||||
|
...skill,
|
||||||
|
plugin_id: skill.skill_id || skill.plugin_id,
|
||||||
|
type: 'skill' as const,
|
||||||
|
})),
|
||||||
|
total: resp?.total || 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return this.post<ApiRespMarketplacePlugins>(
|
return this.post<ApiRespMarketplacePlugins>(
|
||||||
'/api/v1/marketplace/plugins/search',
|
'/api/v1/marketplace/plugins/search',
|
||||||
{
|
{
|
||||||
@@ -49,6 +91,7 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
sort_order,
|
sort_order,
|
||||||
component_filter,
|
component_filter,
|
||||||
tags_filter,
|
tags_filter,
|
||||||
|
type_filter,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const enUS = {
|
|||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
|
skill: 'Skill',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
submit: 'Submit',
|
submit: 'Submit',
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
@@ -617,11 +618,24 @@ const enUS = {
|
|||||||
markAsReadFailed: 'Mark as read failed',
|
markAsReadFailed: 'Mark as read failed',
|
||||||
filterByComponent: 'Component',
|
filterByComponent: 'Component',
|
||||||
allComponents: 'All Components',
|
allComponents: 'All Components',
|
||||||
|
filterByType: 'Type',
|
||||||
|
allTypes: 'All Types',
|
||||||
|
typePlugin: 'Plugin',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: 'Skill',
|
||||||
requestPlugin: 'Request Plugin',
|
requestPlugin: 'Request Plugin',
|
||||||
viewDetails: 'View Details',
|
viewDetails: 'View Details',
|
||||||
deprecated: 'Deprecated',
|
deprecated: 'Deprecated',
|
||||||
deprecatedTooltip:
|
deprecatedTooltip:
|
||||||
'Please install the corresponding Knowledge Engine plugin.',
|
'Please install the corresponding Knowledge Engine plugin.',
|
||||||
|
filters: {
|
||||||
|
allFormats: 'All Formats',
|
||||||
|
more: 'More',
|
||||||
|
advancedTitle: 'Advanced Filters',
|
||||||
|
advancedDescription: 'Filter by extension type',
|
||||||
|
technicalType: 'Technical Type',
|
||||||
|
},
|
||||||
|
allExtensions: 'All Extensions',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'Filter by Tags',
|
filterByTags: 'Filter by Tags',
|
||||||
selected: 'selected',
|
selected: 'selected',
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const esES = {
|
|||||||
delete: 'Eliminar',
|
delete: 'Eliminar',
|
||||||
add: 'Añadir',
|
add: 'Añadir',
|
||||||
select: 'Seleccionar',
|
select: 'Seleccionar',
|
||||||
|
skill: 'Habilidad',
|
||||||
cancel: 'Cancelar',
|
cancel: 'Cancelar',
|
||||||
submit: 'Enviar',
|
submit: 'Enviar',
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
@@ -630,11 +631,24 @@ const esES = {
|
|||||||
markAsReadFailed: 'Error al marcar como leído',
|
markAsReadFailed: 'Error al marcar como leído',
|
||||||
filterByComponent: 'Componente',
|
filterByComponent: 'Componente',
|
||||||
allComponents: 'Todos los componentes',
|
allComponents: 'Todos los componentes',
|
||||||
|
filterByType: 'Tipo',
|
||||||
|
allTypes: 'Todos los tipos',
|
||||||
|
typePlugin: 'Plugin',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: 'Habilidad',
|
||||||
requestPlugin: 'Solicitar plugin',
|
requestPlugin: 'Solicitar plugin',
|
||||||
viewDetails: 'Ver detalles',
|
viewDetails: 'Ver detalles',
|
||||||
deprecated: 'Obsoleto',
|
deprecated: 'Obsoleto',
|
||||||
deprecatedTooltip:
|
deprecatedTooltip:
|
||||||
'Por favor, instala el plugin de motor de conocimiento correspondiente.',
|
'Por favor, instala el plugin de motor de conocimiento correspondiente.',
|
||||||
|
filters: {
|
||||||
|
allFormats: 'Todos los formatos',
|
||||||
|
more: 'Más',
|
||||||
|
advancedTitle: 'Filtros avanzados',
|
||||||
|
advancedDescription: 'Filtrar por tipo de extensión',
|
||||||
|
technicalType: 'Tipo técnico',
|
||||||
|
},
|
||||||
|
allExtensions: 'Todas las extensiones',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'Filtrar por etiquetas',
|
filterByTags: 'Filtrar por etiquetas',
|
||||||
selected: 'seleccionadas',
|
selected: 'seleccionadas',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const jaJP = {
|
const jaJP = {
|
||||||
sidebar: {
|
sidebar: {
|
||||||
home: 'ホーム',
|
home: 'ホーム',
|
||||||
extensions: '拡張機能',
|
extensions: '拡張機能',
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
delete: '削除',
|
delete: '削除',
|
||||||
add: '追加',
|
add: '追加',
|
||||||
select: '選択してください',
|
select: '選択してください',
|
||||||
|
skill: 'スキル',
|
||||||
cancel: 'キャンセル',
|
cancel: 'キャンセル',
|
||||||
submit: '送信',
|
submit: '送信',
|
||||||
error: 'エラー',
|
error: 'エラー',
|
||||||
@@ -622,6 +623,11 @@
|
|||||||
markAsReadFailed: '既読に設定に失敗しました',
|
markAsReadFailed: '既読に設定に失敗しました',
|
||||||
filterByComponent: 'コンポーネント',
|
filterByComponent: 'コンポーネント',
|
||||||
allComponents: '全部コンポーネント',
|
allComponents: '全部コンポーネント',
|
||||||
|
filterByType: 'タイプ',
|
||||||
|
allTypes: '全部',
|
||||||
|
typePlugin: 'プラグイン',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: 'スキル',
|
||||||
requestPlugin: 'プラグインをリクエスト',
|
requestPlugin: 'プラグインをリクエスト',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'タグで絞り込み',
|
filterByTags: 'タグで絞り込み',
|
||||||
@@ -630,6 +636,14 @@
|
|||||||
clearAll: 'クリア',
|
clearAll: 'クリア',
|
||||||
noTags: 'タグがありません',
|
noTags: 'タグがありません',
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
allFormats: 'すべての形式',
|
||||||
|
more: 'もっと',
|
||||||
|
advancedTitle: '高度なフィルター',
|
||||||
|
advancedDescription: '拡張子タイプでフィルター',
|
||||||
|
technicalType: '技術タイプ',
|
||||||
|
},
|
||||||
|
allExtensions: 'すべての拡張機能',
|
||||||
viewDetails: '詳細を表示',
|
viewDetails: '詳細を表示',
|
||||||
deprecated: '非推奨',
|
deprecated: '非推奨',
|
||||||
deprecatedTooltip:
|
deprecatedTooltip:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const ruRU = {
|
|||||||
delete: 'Удалить',
|
delete: 'Удалить',
|
||||||
add: 'Добавить',
|
add: 'Добавить',
|
||||||
select: 'Выбрать',
|
select: 'Выбрать',
|
||||||
|
skill: 'Навык',
|
||||||
cancel: 'Отмена',
|
cancel: 'Отмена',
|
||||||
submit: 'Отправить',
|
submit: 'Отправить',
|
||||||
error: 'Ошибка',
|
error: 'Ошибка',
|
||||||
@@ -627,11 +628,24 @@ const ruRU = {
|
|||||||
markAsReadFailed: 'Не удалось отметить как прочитанное',
|
markAsReadFailed: 'Не удалось отметить как прочитанное',
|
||||||
filterByComponent: 'Компонент',
|
filterByComponent: 'Компонент',
|
||||||
allComponents: 'Все компоненты',
|
allComponents: 'Все компоненты',
|
||||||
|
filterByType: 'Тип',
|
||||||
|
allTypes: 'Все типы',
|
||||||
|
typePlugin: 'Плагин',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: 'Навык',
|
||||||
requestPlugin: 'Запросить плагин',
|
requestPlugin: 'Запросить плагин',
|
||||||
viewDetails: 'Подробнее',
|
viewDetails: 'Подробнее',
|
||||||
deprecated: 'Устаревший',
|
deprecated: 'Устаревший',
|
||||||
deprecatedTooltip:
|
deprecatedTooltip:
|
||||||
'Пожалуйста, установите соответствующий плагин движка знаний.',
|
'Пожалуйста, установите соответствующий плагин движка знаний.',
|
||||||
|
filters: {
|
||||||
|
allFormats: 'Все форматы',
|
||||||
|
more: 'Ещё',
|
||||||
|
advancedTitle: 'Расширенные фильтры',
|
||||||
|
advancedDescription: 'Фильтр по типу расширения',
|
||||||
|
technicalType: 'Технический тип',
|
||||||
|
},
|
||||||
|
allExtensions: 'Все расширения',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'Фильтр по тегам',
|
filterByTags: 'Фильтр по тегам',
|
||||||
selected: 'выбрано',
|
selected: 'выбрано',
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const thTH = {
|
|||||||
delete: 'ลบ',
|
delete: 'ลบ',
|
||||||
add: 'เพิ่ม',
|
add: 'เพิ่ม',
|
||||||
select: 'เลือก',
|
select: 'เลือก',
|
||||||
|
skill: 'สกิล',
|
||||||
cancel: 'ยกเลิก',
|
cancel: 'ยกเลิก',
|
||||||
submit: 'ส่ง',
|
submit: 'ส่ง',
|
||||||
error: 'ข้อผิดพลาด',
|
error: 'ข้อผิดพลาด',
|
||||||
@@ -609,10 +610,23 @@ const thTH = {
|
|||||||
markAsReadFailed: 'ทำเครื่องหมายว่าอ่านแล้วล้มเหลว',
|
markAsReadFailed: 'ทำเครื่องหมายว่าอ่านแล้วล้มเหลว',
|
||||||
filterByComponent: 'ส่วนประกอบ',
|
filterByComponent: 'ส่วนประกอบ',
|
||||||
allComponents: 'ส่วนประกอบทั้งหมด',
|
allComponents: 'ส่วนประกอบทั้งหมด',
|
||||||
|
filterByType: 'ประเภท',
|
||||||
|
allTypes: 'ทุกประเภท',
|
||||||
|
typePlugin: 'ปลั๊กอิน',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: 'สกิล',
|
||||||
requestPlugin: 'ขอปลั๊กอิน',
|
requestPlugin: 'ขอปลั๊กอิน',
|
||||||
viewDetails: 'ดูรายละเอียด',
|
viewDetails: 'ดูรายละเอียด',
|
||||||
deprecated: 'เลิกใช้แล้ว',
|
deprecated: 'เลิกใช้แล้ว',
|
||||||
deprecatedTooltip: 'กรุณาติดตั้งปลั๊กอินเครื่องมือความรู้ที่เกี่ยวข้อง',
|
deprecatedTooltip: 'กรุณาติดตั้งปลั๊กอินเครื่องมือความรู้ที่เกี่ยวข้อง',
|
||||||
|
filters: {
|
||||||
|
allFormats: 'ทุกรูปแบบ',
|
||||||
|
more: 'เพิ่มเติม',
|
||||||
|
advancedTitle: 'ตัวกรองขั้นสูง',
|
||||||
|
advancedDescription: 'กรองตามประเภทส่วนขยาย',
|
||||||
|
technicalType: 'ประเภทเทคนิค',
|
||||||
|
},
|
||||||
|
allExtensions: 'ส่วนขยายทั้งหมด',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'กรองตามแท็ก',
|
filterByTags: 'กรองตามแท็ก',
|
||||||
selected: 'เลือกแล้ว',
|
selected: 'เลือกแล้ว',
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const viVN = {
|
|||||||
delete: 'Xóa',
|
delete: 'Xóa',
|
||||||
add: 'Thêm',
|
add: 'Thêm',
|
||||||
select: 'Chọn',
|
select: 'Chọn',
|
||||||
|
skill: 'Kỹ năng',
|
||||||
cancel: 'Hủy',
|
cancel: 'Hủy',
|
||||||
submit: 'Gửi',
|
submit: 'Gửi',
|
||||||
error: 'Lỗi',
|
error: 'Lỗi',
|
||||||
@@ -621,10 +622,23 @@ const viVN = {
|
|||||||
markAsReadFailed: 'Đánh dấu đã đọc thất bại',
|
markAsReadFailed: 'Đánh dấu đã đọc thất bại',
|
||||||
filterByComponent: 'Thành phần',
|
filterByComponent: 'Thành phần',
|
||||||
allComponents: 'Tất cả thành phần',
|
allComponents: 'Tất cả thành phần',
|
||||||
|
filterByType: 'Loại',
|
||||||
|
allTypes: 'Tất cả loại',
|
||||||
|
typePlugin: 'Plugin',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: 'Kỹ năng',
|
||||||
requestPlugin: 'Yêu cầu Plugin',
|
requestPlugin: 'Yêu cầu Plugin',
|
||||||
viewDetails: 'Xem chi tiết',
|
viewDetails: 'Xem chi tiết',
|
||||||
deprecated: 'Không còn hỗ trợ',
|
deprecated: 'Không còn hỗ trợ',
|
||||||
deprecatedTooltip: 'Vui lòng cài đặt plugin Công cụ tri thức tương ứng.',
|
deprecatedTooltip: 'Vui lòng cài đặt plugin Công cụ tri thức tương ứng.',
|
||||||
|
filters: {
|
||||||
|
allFormats: 'Tất cả định dạng',
|
||||||
|
more: 'Thêm',
|
||||||
|
advancedTitle: 'Bộ lọc nâng cao',
|
||||||
|
advancedDescription: 'Lọc theo loại phần mở rộng',
|
||||||
|
technicalType: 'Loại kỹ thuật',
|
||||||
|
},
|
||||||
|
allExtensions: 'Tất cả phần mở rộng',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'Lọc theo thẻ',
|
filterByTags: 'Lọc theo thẻ',
|
||||||
selected: 'đã chọn',
|
selected: 'đã chọn',
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const zhHans = {
|
|||||||
delete: '删除',
|
delete: '删除',
|
||||||
add: '添加',
|
add: '添加',
|
||||||
select: '请选择',
|
select: '请选择',
|
||||||
|
skill: '技能',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
error: '错误',
|
error: '错误',
|
||||||
@@ -590,6 +591,11 @@ const zhHans = {
|
|||||||
markAsReadFailed: '标记为已读失败',
|
markAsReadFailed: '标记为已读失败',
|
||||||
filterByComponent: '组件',
|
filterByComponent: '组件',
|
||||||
allComponents: '全部组件',
|
allComponents: '全部组件',
|
||||||
|
filterByType: '类型',
|
||||||
|
allTypes: '全部类型',
|
||||||
|
typePlugin: '插件',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: '技能',
|
||||||
requestPlugin: '请求插件',
|
requestPlugin: '请求插件',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: '按标签筛选',
|
filterByTags: '按标签筛选',
|
||||||
@@ -598,6 +604,14 @@ const zhHans = {
|
|||||||
clearAll: '清空',
|
clearAll: '清空',
|
||||||
noTags: '暂无标签',
|
noTags: '暂无标签',
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
allFormats: '全部格式',
|
||||||
|
more: '更多',
|
||||||
|
advancedTitle: '高级筛选',
|
||||||
|
advancedDescription: '按扩展类型筛选',
|
||||||
|
technicalType: '技术类型',
|
||||||
|
},
|
||||||
|
allExtensions: '全部扩展',
|
||||||
viewDetails: '查看详情',
|
viewDetails: '查看详情',
|
||||||
deprecated: '已弃用',
|
deprecated: '已弃用',
|
||||||
deprecatedTooltip: '请安装对应「知识引擎」插件',
|
deprecatedTooltip: '请安装对应「知识引擎」插件',
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const zhHant = {
|
|||||||
delete: '刪除',
|
delete: '刪除',
|
||||||
add: '新增',
|
add: '新增',
|
||||||
select: '請選擇',
|
select: '請選擇',
|
||||||
|
skill: '技能',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
error: '錯誤',
|
error: '錯誤',
|
||||||
@@ -590,6 +591,11 @@ const zhHant = {
|
|||||||
markAsReadFailed: '標記為已讀失敗',
|
markAsReadFailed: '標記為已讀失敗',
|
||||||
filterByComponent: '組件',
|
filterByComponent: '組件',
|
||||||
allComponents: '全部組件',
|
allComponents: '全部組件',
|
||||||
|
filterByType: '類型',
|
||||||
|
allTypes: '全部類型',
|
||||||
|
typePlugin: '插件',
|
||||||
|
typeMCP: 'MCP',
|
||||||
|
typeSkill: '技能',
|
||||||
requestPlugin: '請求插件',
|
requestPlugin: '請求插件',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: '按標籤篩選',
|
filterByTags: '按標籤篩選',
|
||||||
@@ -598,6 +604,14 @@ const zhHant = {
|
|||||||
clearAll: '清空',
|
clearAll: '清空',
|
||||||
noTags: '暫無標籤',
|
noTags: '暫無標籤',
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
allFormats: '全部格式',
|
||||||
|
more: '更多',
|
||||||
|
advancedTitle: '高級篩選',
|
||||||
|
advancedDescription: '按擴展類型篩選',
|
||||||
|
technicalType: '技術類型',
|
||||||
|
},
|
||||||
|
allExtensions: '全部擴展',
|
||||||
viewDetails: '查看詳情',
|
viewDetails: '查看詳情',
|
||||||
deprecated: '已棄用',
|
deprecated: '已棄用',
|
||||||
deprecatedTooltip: '請安裝對應「知識引擎」插件',
|
deprecatedTooltip: '請安裝對應「知識引擎」插件',
|
||||||
|
|||||||
Reference in New Issue
Block a user