mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-22 04:16:07 +00:00
feat(plugin-market): rename component filter to "插件组件" with hint tooltip + persist filters
- Rename the in-app plugin market component filter label to "插件组件" / "Plugin Component" - Add an Info icon tooltip explaining what plugin components are (Tool / Command / EventListener, etc.) - Persist filter selections (type / component / tags / sort) in localStorage so they survive reloads; restored on mount (URL type param still wins) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,13 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
X,
|
X,
|
||||||
|
Info,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
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';
|
||||||
import { RecommendationLists } from './RecommendationLists';
|
import { RecommendationLists } from './RecommendationLists';
|
||||||
@@ -49,6 +55,23 @@ interface SortOption {
|
|||||||
sortOrder: string;
|
sortOrder: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the market filter conditions (type / component / tags / sort) across
|
||||||
|
// visits via localStorage.
|
||||||
|
const MARKET_FILTERS_KEY = 'langbot_market_filters';
|
||||||
|
interface MarketFilters {
|
||||||
|
typeFilter?: string;
|
||||||
|
componentFilter?: string;
|
||||||
|
selectedTags?: string[];
|
||||||
|
sortOption?: string;
|
||||||
|
}
|
||||||
|
function loadMarketFilters(): MarketFilters {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(MARKET_FILTERS_KEY) || '{}');
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 内部组件,用于处理搜索参数
|
// 内部组件,用于处理搜索参数
|
||||||
function MarketPageContent({
|
function MarketPageContent({
|
||||||
installPlugin,
|
installPlugin,
|
||||||
@@ -70,17 +93,22 @@ function MarketPageContent({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [componentFilter, setComponentFilter] = useState('all');
|
const [componentFilter, setComponentFilter] = useState<string>(
|
||||||
|
() => loadMarketFilters().componentFilter ?? 'all',
|
||||||
|
);
|
||||||
const [typeFilter, setTypeFilter] = useState<string>(() => {
|
const [typeFilter, setTypeFilter] = useState<string>(() => {
|
||||||
const type = searchParams.get('type');
|
const type = searchParams.get('type');
|
||||||
if (type && validTypes.includes(type)) {
|
if (type && validTypes.includes(type)) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
return 'all';
|
const saved = loadMarketFilters().typeFilter;
|
||||||
|
return saved && validTypes.includes(saved) ? saved : 'all';
|
||||||
});
|
});
|
||||||
const activeAdvancedFilters =
|
const activeAdvancedFilters =
|
||||||
(typeFilter === 'all' ? 0 : 1) + (componentFilter === 'all' ? 0 : 1);
|
(typeFilter === 'all' ? 0 : 1) + (componentFilter === 'all' ? 0 : 1);
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>(
|
||||||
|
() => loadMarketFilters().selectedTags ?? [],
|
||||||
|
);
|
||||||
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
|
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
|
||||||
const [tagNames, setTagNames] = useState<Record<string, string>>({});
|
const [tagNames, setTagNames] = useState<Record<string, string>>({});
|
||||||
const [recommendationLists, setRecommendationLists] = useState<
|
const [recommendationLists, setRecommendationLists] = useState<
|
||||||
@@ -92,7 +120,26 @@ function MarketPageContent({
|
|||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [sortOption, setSortOption] = useState('install_count_desc');
|
const [sortOption, setSortOption] = useState<string>(
|
||||||
|
() => loadMarketFilters().sortOption ?? 'install_count_desc',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Persist filter conditions so they survive navigation / reload.
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
MARKET_FILTERS_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
typeFilter,
|
||||||
|
componentFilter,
|
||||||
|
selectedTags,
|
||||||
|
sortOption,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore storage errors
|
||||||
|
}
|
||||||
|
}, [typeFilter, componentFilter, selectedTags, sortOption]);
|
||||||
|
|
||||||
const pageSize = 12; // 每页12个
|
const pageSize = 12; // 每页12个
|
||||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -642,8 +689,22 @@ function MarketPageContent({
|
|||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="flex items-center gap-1 text-xs font-medium text-muted-foreground">
|
||||||
{t('market.filterByComponent')}
|
{t('market.filterByComponent')}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex text-muted-foreground/70 hover:text-foreground"
|
||||||
|
aria-label={t('market.filterByComponentHint')}
|
||||||
|
>
|
||||||
|
<Info className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-64">
|
||||||
|
{t('market.filterByComponentHint')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
|
|||||||
@@ -670,7 +670,9 @@ const enUS = {
|
|||||||
markAsRead: 'Mark as Read',
|
markAsRead: 'Mark as Read',
|
||||||
markAsReadSuccess: 'Marked as read',
|
markAsReadSuccess: 'Marked as read',
|
||||||
markAsReadFailed: 'Mark as read failed',
|
markAsReadFailed: 'Mark as read failed',
|
||||||
filterByComponent: 'Component',
|
filterByComponent: 'Plugin Component',
|
||||||
|
filterByComponentHint:
|
||||||
|
'Plugin components are the capability types a plugin provides — Tool, Command, EventListener, etc. Filter by component to show only plugins offering that capability.',
|
||||||
allComponents: 'All Components',
|
allComponents: 'All Components',
|
||||||
componentName: {
|
componentName: {
|
||||||
Tool: 'Tool',
|
Tool: 'Tool',
|
||||||
|
|||||||
@@ -643,7 +643,9 @@ const zhHans = {
|
|||||||
markAsRead: '已读',
|
markAsRead: '已读',
|
||||||
markAsReadSuccess: '已标记为已读',
|
markAsReadSuccess: '已标记为已读',
|
||||||
markAsReadFailed: '标记为已读失败',
|
markAsReadFailed: '标记为已读失败',
|
||||||
filterByComponent: '组件',
|
filterByComponent: '插件组件',
|
||||||
|
filterByComponentHint:
|
||||||
|
'插件组件是插件提供的能力类型,如工具(Tool)、命令(Command)、事件监听器(EventListener)等。按组件筛选可只看提供对应能力的插件。',
|
||||||
allComponents: '全部组件',
|
allComponents: '全部组件',
|
||||||
componentName: {
|
componentName: {
|
||||||
Tool: '工具',
|
Tool: '工具',
|
||||||
|
|||||||
Reference in New Issue
Block a user