mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: modify frontend
This commit is contained in:
@@ -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';
|
||||||
|
|
||||||
@@ -57,6 +67,13 @@ function MarketPageContent({
|
|||||||
|
|
||||||
const validTypes = ['plugin', 'mcp', 'skill'];
|
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');
|
||||||
@@ -72,6 +89,7 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
return 'all';
|
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>>({});
|
||||||
@@ -149,6 +167,40 @@ function MarketPageContent({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
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) => {
|
||||||
@@ -162,32 +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 typeFilterValue = typeFilter === 'all' ? undefined : typeFilter;
|
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,
|
||||||
typeFilterValue,
|
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);
|
||||||
@@ -197,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);
|
||||||
@@ -214,8 +332,11 @@ function MarketPageContent({
|
|||||||
selectedTags,
|
selectedTags,
|
||||||
pageSize,
|
pageSize,
|
||||||
transformToVO,
|
transformToVO,
|
||||||
|
transformMCPToVO,
|
||||||
|
transformSkillToVO,
|
||||||
plugins.length,
|
plugins.length,
|
||||||
getCurrentSort,
|
getCurrentSort,
|
||||||
|
typeFilter,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -460,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')}
|
||||||
@@ -477,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);
|
||||||
}
|
}
|
||||||
@@ -488,138 +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>
|
|
||||||
|
|
||||||
{/* Type 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.filterByType')}:
|
|
||||||
</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={typeFilter}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value) handleTypeFilterChange(value);
|
|
||||||
}}
|
|
||||||
className="justify-start flex-nowrap"
|
|
||||||
>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="all"
|
|
||||||
aria-label="All types"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
{t('market.allTypes')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="plugin"
|
|
||||||
aria-label="Plugin"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
{t('market.typePlugin')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="mcp"
|
|
||||||
aria-label="MCP"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
{t('market.typeMCP')}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
|
||||||
value="skill"
|
|
||||||
aria-label="Skill"
|
|
||||||
className="text-xs sm:text-sm cursor-pointer"
|
|
||||||
>
|
|
||||||
{t('market.typeSkill')}
|
|
||||||
</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>
|
||||||
@@ -630,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">
|
||||||
|
|||||||
@@ -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,52 +70,55 @@ 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}>
|
||||||
{cardVO.type && (
|
<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
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`text-[0.6rem] px-1.5 py-0 h-4 flex-shrink-0 gap-0.5 ${
|
className={`text-[0.6rem] px-1.5 py-0 h-4 flex-shrink-0 gap-0.5 ${
|
||||||
@@ -136,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');
|
||||||
}}
|
}}
|
||||||
@@ -151,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"
|
||||||
@@ -176,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) => (
|
||||||
@@ -198,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 && (
|
||||||
@@ -215,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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,12 +53,12 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
tags_filter,
|
tags_filter,
|
||||||
},
|
},
|
||||||
).then((resp) => ({
|
).then((resp) => ({
|
||||||
plugins: (resp.mcps || []).map((mcp) => ({
|
plugins: (resp?.mcps || []).map((mcp) => ({
|
||||||
...mcp,
|
...mcp,
|
||||||
plugin_id: mcp.mcp_id || mcp.plugin_id,
|
plugin_id: mcp.mcp_id || mcp.plugin_id,
|
||||||
type: 'mcp' as const,
|
type: 'mcp' as const,
|
||||||
})),
|
})),
|
||||||
total: resp.total || 0,
|
total: resp?.total || 0,
|
||||||
}));
|
}));
|
||||||
} else if (type_filter === 'skill') {
|
} else if (type_filter === 'skill') {
|
||||||
return this.post<{ skills: PluginV4[]; total: number }>(
|
return this.post<{ skills: PluginV4[]; total: number }>(
|
||||||
@@ -72,12 +72,12 @@ export class CloudServiceClient extends BaseHttpClient {
|
|||||||
tags_filter,
|
tags_filter,
|
||||||
},
|
},
|
||||||
).then((resp) => ({
|
).then((resp) => ({
|
||||||
plugins: (resp.skills || []).map((skill) => ({
|
plugins: (resp?.skills || []).map((skill) => ({
|
||||||
...skill,
|
...skill,
|
||||||
plugin_id: skill.skill_id || skill.plugin_id,
|
plugin_id: skill.skill_id || skill.plugin_id,
|
||||||
type: 'skill' as const,
|
type: 'skill' as const,
|
||||||
})),
|
})),
|
||||||
total: resp.total || 0,
|
total: resp?.total || 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -628,6 +628,14 @@ const enUS = {
|
|||||||
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',
|
||||||
|
|||||||
@@ -641,6 +641,14 @@ const esES = {
|
|||||||
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',
|
||||||
|
|||||||
@@ -636,6 +636,14 @@ const jaJP = {
|
|||||||
clearAll: 'クリア',
|
clearAll: 'クリア',
|
||||||
noTags: 'タグがありません',
|
noTags: 'タグがありません',
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
allFormats: 'すべての形式',
|
||||||
|
more: 'もっと',
|
||||||
|
advancedTitle: '高度なフィルター',
|
||||||
|
advancedDescription: '拡張子タイプでフィルター',
|
||||||
|
technicalType: '技術タイプ',
|
||||||
|
},
|
||||||
|
allExtensions: 'すべての拡張機能',
|
||||||
viewDetails: '詳細を表示',
|
viewDetails: '詳細を表示',
|
||||||
deprecated: '非推奨',
|
deprecated: '非推奨',
|
||||||
deprecatedTooltip:
|
deprecatedTooltip:
|
||||||
|
|||||||
@@ -638,6 +638,14 @@ const ruRU = {
|
|||||||
deprecated: 'Устаревший',
|
deprecated: 'Устаревший',
|
||||||
deprecatedTooltip:
|
deprecatedTooltip:
|
||||||
'Пожалуйста, установите соответствующий плагин движка знаний.',
|
'Пожалуйста, установите соответствующий плагин движка знаний.',
|
||||||
|
filters: {
|
||||||
|
allFormats: 'Все форматы',
|
||||||
|
more: 'Ещё',
|
||||||
|
advancedTitle: 'Расширенные фильтры',
|
||||||
|
advancedDescription: 'Фильтр по типу расширения',
|
||||||
|
technicalType: 'Технический тип',
|
||||||
|
},
|
||||||
|
allExtensions: 'Все расширения',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'Фильтр по тегам',
|
filterByTags: 'Фильтр по тегам',
|
||||||
selected: 'выбрано',
|
selected: 'выбрано',
|
||||||
|
|||||||
@@ -619,6 +619,14 @@ const thTH = {
|
|||||||
viewDetails: 'ดูรายละเอียด',
|
viewDetails: 'ดูรายละเอียด',
|
||||||
deprecated: 'เลิกใช้แล้ว',
|
deprecated: 'เลิกใช้แล้ว',
|
||||||
deprecatedTooltip: 'กรุณาติดตั้งปลั๊กอินเครื่องมือความรู้ที่เกี่ยวข้อง',
|
deprecatedTooltip: 'กรุณาติดตั้งปลั๊กอินเครื่องมือความรู้ที่เกี่ยวข้อง',
|
||||||
|
filters: {
|
||||||
|
allFormats: 'ทุกรูปแบบ',
|
||||||
|
more: 'เพิ่มเติม',
|
||||||
|
advancedTitle: 'ตัวกรองขั้นสูง',
|
||||||
|
advancedDescription: 'กรองตามประเภทส่วนขยาย',
|
||||||
|
technicalType: 'ประเภทเทคนิค',
|
||||||
|
},
|
||||||
|
allExtensions: 'ส่วนขยายทั้งหมด',
|
||||||
tags: {
|
tags: {
|
||||||
filterByTags: 'กรองตามแท็ก',
|
filterByTags: 'กรองตามแท็ก',
|
||||||
selected: 'เลือกแล้ว',
|
selected: 'เลือกแล้ว',
|
||||||
|
|||||||
@@ -631,6 +631,14 @@ const viVN = {
|
|||||||
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',
|
||||||
|
|||||||
@@ -604,6 +604,14 @@ const zhHans = {
|
|||||||
clearAll: '清空',
|
clearAll: '清空',
|
||||||
noTags: '暂无标签',
|
noTags: '暂无标签',
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
allFormats: '全部格式',
|
||||||
|
more: '更多',
|
||||||
|
advancedTitle: '高级筛选',
|
||||||
|
advancedDescription: '按扩展类型筛选',
|
||||||
|
technicalType: '技术类型',
|
||||||
|
},
|
||||||
|
allExtensions: '全部扩展',
|
||||||
viewDetails: '查看详情',
|
viewDetails: '查看详情',
|
||||||
deprecated: '已弃用',
|
deprecated: '已弃用',
|
||||||
deprecatedTooltip: '请安装对应「知识引擎」插件',
|
deprecatedTooltip: '请安装对应「知识引擎」插件',
|
||||||
|
|||||||
@@ -604,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