Compare commits

...

7 Commits

Author SHA1 Message Date
Junyan Qin
99e3abec72 chore: bump version 4.5.4 2025-11-20 23:19:37 +08:00
Junyan Qin
fc2efdf994 chore: bump langbot-plugin 0.1.12 2025-11-20 21:51:44 +08:00
Junyan Qin
6ed672d996 perf: tips msg for tool call 2025-11-20 21:45:22 +08:00
Junyan Qin
2bf593fa6b feat: pass session and query_id to tool call 2025-11-20 21:17:47 +08:00
Junyan Qin
3182214663 fix: linter errors 2025-11-20 19:48:34 +08:00
Junyan Qin
20614b20b7 feat: add component filter to marketplace page 2025-11-20 19:46:33 +08:00
Junyan Qin
da323817f7 feat: add plugin components displaying in marketplace page 2025-11-20 18:50:00 +08:00
27 changed files with 294 additions and 94 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "langbot"
version = "4.5.3"
version = "4.5.4"
description = "Easy-to-use global IM bot platform designed for LLM era"
readme = "README.md"
license-files = ["LICENSE"]
@@ -63,7 +63,7 @@ dependencies = [
"langchain-text-splitters>=0.0.1",
"chromadb>=0.4.24",
"qdrant-client (>=1.15.1,<2.0.0)",
"langbot-plugin==0.1.11",
"langbot-plugin==0.1.12",
"asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0",
"tboxsdk>=0.0.10",

View File

@@ -1,3 +1,3 @@
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
__version__ = '4.5.3'
__version__ = '4.5.4'

View File

@@ -55,17 +55,6 @@ class SystemRouterGroup(group.RouterGroup):
return self.success(data=exec(py_code, {'ap': ap}))
@self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
if not constants.debug_mode:
return self.http_status(403, 403, 'Forbidden')
data = await quart.request.json
return self.success(
data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters'])
)
@self.route(
'/debug/plugin/action',
methods=['POST'],

View File

@@ -96,7 +96,7 @@ class ResponseWrapper(stage.PipelineStage):
if result.tool_calls is not None and len(result.tool_calls) > 0: # 有函数调用
function_names = [tc.function.name for tc in result.tool_calls]
reply_text = f'调用函数 {".".join(function_names)}...'
reply_text = f'Call {".".join(function_names)}...'
query.resp_message_chain.append(
platform_message.MessageChain([platform_message.Plain(text=reply_text)])

View File

@@ -8,6 +8,7 @@ import os
import sys
import httpx
from async_lru import alru_cache
from langbot_plugin.api.entities.builtin.pipeline.query import provider_session
from ..core import app
from . import handler
@@ -321,13 +322,20 @@ class PluginRuntimeConnector:
return tools
async def call_tool(
self, tool_name: str, parameters: dict[str, Any], bound_plugins: list[str] | None = None
self,
tool_name: str,
parameters: dict[str, Any],
session: provider_session.Session,
query_id: int,
bound_plugins: list[str] | None = None,
) -> dict[str, Any]:
if not self.is_enable_plugin:
return {'error': 'Tool not found: plugin system is disabled'}
# Pass include_plugins to runtime for validation
return await self.handler.call_tool(tool_name, parameters, include_plugins=bound_plugins)
return await self.handler.call_tool(
tool_name, parameters, session.model_dump(serialize_as_any=True), query_id, include_plugins=bound_plugins
)
async def list_commands(self, bound_plugins: list[str] | None = None) -> list[ComponentManifest]:
if not self.is_enable_plugin:

View File

@@ -620,7 +620,12 @@ class RuntimeConnectionHandler(handler.Handler):
)
async def call_tool(
self, tool_name: str, parameters: dict[str, Any], include_plugins: list[str] | None = None
self,
tool_name: str,
parameters: dict[str, Any],
session: dict[str, Any],
query_id: int,
include_plugins: list[str] | None = None,
) -> dict[str, Any]:
"""Call tool"""
result = await self.call_action(
@@ -628,6 +633,8 @@ class RuntimeConnectionHandler(handler.Handler):
{
'tool_name': tool_name,
'tool_parameters': parameters,
'session': session,
'query_id': query_id,
'include_plugins': include_plugins,
},
timeout=60,

View File

@@ -196,7 +196,7 @@ class LocalAgentRunner(runner.RequestRunner):
parameters = json.loads(func.arguments)
func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters)
func_ret = await self.ap.tool_mgr.execute_func_call(func.name, parameters, query=query)
if is_stream:
msg = provider_message.MessageChunk(
role='tool',

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
import abc
import typing
from langbot_plugin.api.entities.events import pipeline_query
from ...core import app
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
@@ -45,7 +47,7 @@ class ToolLoader(abc.ABC):
pass
@abc.abstractmethod
async def invoke_tool(self, name: str, parameters: dict) -> typing.Any:
async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
"""执行工具调用"""
pass

View File

@@ -4,6 +4,7 @@ import enum
import typing
from contextlib import AsyncExitStack
import traceback
from langbot_plugin.api.entities.events import pipeline_query
import sqlalchemy
import asyncio
@@ -329,7 +330,7 @@ class MCPLoader(loader.ToolLoader):
return True
return False
async def invoke_tool(self, name: str, parameters: dict) -> typing.Any:
async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
"""执行工具调用"""
for session in self.sessions.values():
for function in session.get_tools():

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
import typing
import traceback
from langbot_plugin.api.entities.events import pipeline_query
from .. import loader
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
@@ -43,9 +45,11 @@ class PluginToolLoader(loader.ToolLoader):
return tool
return None
async def invoke_tool(self, name: str, parameters: dict) -> typing.Any:
async def invoke_tool(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
try:
return await self.ap.plugin_connector.call_tool(name, parameters)
return await self.ap.plugin_connector.call_tool(
name, parameters, session=query.session, query_id=query.query_id
)
except Exception as e:
self.ap.logger.error(f'执行函数 {name} 时发生错误: {e}')
traceback.print_exc()

View File

@@ -7,6 +7,7 @@ from langbot.pkg.utils import importutil
from langbot.pkg.provider.tools import loaders
from langbot.pkg.provider.tools.loaders import mcp as mcp_loader, plugin as plugin_loader
import langbot_plugin.api.entities.builtin.resource.tool as resource_tool
from langbot_plugin.api.entities.events import pipeline_query
importutil.import_modules_in_pkg(loaders)
@@ -91,13 +92,13 @@ class ToolManager:
return tools
async def execute_func_call(self, name: str, parameters: dict) -> typing.Any:
async def execute_func_call(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any:
"""执行函数调用"""
if await self.plugin_tool_loader.has_tool(name):
return await self.plugin_tool_loader.invoke_tool(name, parameters)
return await self.plugin_tool_loader.invoke_tool(name, parameters, query)
elif await self.mcp_tool_loader.has_tool(name):
return await self.mcp_tool_loader.invoke_tool(name, parameters)
return await self.mcp_tool_loader.invoke_tool(name, parameters, query)
else:
raise ValueError(f'未找到工具: {name}')

View File

@@ -234,7 +234,19 @@ export default function PipelineExtension({
</div>
<div className="flex gap-1 mt-1">
<PluginComponentList
components={plugin.components}
components={(() => {
const componentKindCount: Record<string, number> =
{};
for (const component of plugin.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={true}
showTitle={false}
useBadge={true}
@@ -401,7 +413,19 @@ export default function PipelineExtension({
</div>
<div className="flex gap-1 mt-1">
<PluginComponentList
components={plugin.components}
components={(() => {
const componentKindCount: Record<string, number> =
{};
for (const component of plugin.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={true}
showTitle={false}
useBadge={true}

View File

@@ -1,4 +1,3 @@
import { PluginComponent } from '@/app/infra/entities/plugin';
import { TFunction } from 'i18next';
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
@@ -9,31 +8,22 @@ export default function PluginComponentList({
showTitle,
useBadge,
t,
responsive = false,
}: {
components: PluginComponent[];
components: Record<string, number>;
showComponentName: boolean;
showTitle: boolean;
useBadge: boolean;
t: TFunction;
responsive?: boolean;
}) {
const componentKindCount: Record<string, number> = {};
for (const component of components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
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" />,
};
const componentKindList = Object.keys(componentKindCount);
const componentKindList = Object.keys(components || {});
return (
<>
@@ -44,11 +34,21 @@ export default function PluginComponentList({
return (
<>
{useBadge && (
<Badge variant="outline">
<Badge
key={kind}
variant="outline"
className="flex items-center gap-1"
>
{kindIconMap[kind]}
{showComponentName &&
t('plugins.componentName.' + kind) + ' '}
{componentKindCount[kind]}
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
{responsive ? (
<span className="hidden md:inline">
{t('plugins.componentName.' + kind)}
</span>
) : (
showComponentName && t('plugins.componentName.' + kind)
)}
<span className="ml-1">{components[kind]}</span>
</Badge>
)}
@@ -58,9 +58,15 @@ export default function PluginComponentList({
className="flex flex-row items-center justify-start gap-[0.2rem]"
>
{kindIconMap[kind]}
{showComponentName &&
t('plugins.componentName.' + kind) + ' '}
{componentKindCount[kind]}
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
{responsive ? (
<span className="hidden md:inline">
{t('plugins.componentName.' + kind)}
</span>
) : (
showComponentName && t('plugins.componentName.' + kind)
)}
<span className="ml-1">{components[kind]}</span>
</div>
)}
</>

View File

@@ -128,7 +128,18 @@ export default function PluginCardComponent({
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
<PluginComponentList
components={cardVO.components}
components={(() => {
const componentKindCount: Record<string, number> = {};
for (const component of cardVO.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={false}
showTitle={true}
useBadge={false}

View File

@@ -160,7 +160,18 @@ export default function PluginForm({
<div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]">
<PluginComponentList
components={pluginInfo.components}
components={(() => {
const componentKindCount: Record<string, number> = {};
for (const component of pluginInfo.components) {
const kind = component.manifest.manifest.kind;
if (componentKindCount[kind]) {
componentKindCount[kind]++;
} else {
componentKindCount[kind] = 1;
}
}
return componentKindCount;
})()}
showComponentName={true}
showTitle={false}
useBadge={true}

View File

@@ -10,7 +10,8 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Search, Loader2 } from 'lucide-react';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { Search, Loader2, Wrench, AudioWaveform, Hash } from 'lucide-react';
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog';
@@ -38,6 +39,7 @@ function MarketPageContent({
const searchParams = useSearchParams();
const [searchQuery, setSearchQuery] = useState('');
const [componentFilter, setComponentFilter] = useState<string>('all');
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
@@ -111,6 +113,7 @@ function MarketPageContent({
),
githubURL: plugin.repository,
version: plugin.latest_version,
components: plugin.components,
});
}, []);
@@ -124,25 +127,20 @@ function MarketPageContent({
}
try {
let response;
const { sortBy, sortOrder } = getCurrentSort();
const filterValue =
componentFilter === 'all' ? undefined : componentFilter;
if (isSearch && searchQuery.trim()) {
response = await getCloudServiceClientSync().searchMarketplacePlugins(
searchQuery.trim(),
// Always use searchMarketplacePlugins to support component filtering
const response =
await getCloudServiceClientSync().searchMarketplacePlugins(
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
page,
pageSize,
sortBy,
sortOrder,
filterValue,
);
} else {
response = await getCloudServiceClientSync().getMarketplacePlugins(
page,
pageSize,
sortBy,
sortOrder,
);
}
const data: ApiRespMarketplacePlugins = response;
const newPlugins = data.plugins.map(transformToVO);
@@ -167,7 +165,14 @@ function MarketPageContent({
setIsLoadingMore(false);
}
},
[searchQuery, pageSize, transformToVO, plugins.length, getCurrentSort],
[
searchQuery,
componentFilter,
pageSize,
transformToVO,
plugins.length,
getCurrentSort,
],
);
// 初始加载
@@ -212,10 +217,18 @@ function MarketPageContent({
// fetchPlugins will be called by useEffect when sortOption changes
}, []);
// 当排序选项变化时重新加载数据
// 组件筛选变化处理
const handleComponentFilterChange = useCallback((value: string) => {
setComponentFilter(value);
setCurrentPage(1);
setPlugins([]);
// fetchPlugins will be called by useEffect when componentFilter changes
}, []);
// 当排序选项或组件筛选变化时重新加载数据
useEffect(() => {
fetchPlugins(1, !!searchQuery.trim(), true);
}, [sortOption]);
}, [sortOption, componentFilter]);
// 处理URL参数检查是否需要打开插件详情对话框
useEffect(() => {
@@ -342,9 +355,59 @@ function MarketPageContent({
</div>
</div>
{/* Sort dropdown */}
<div className="flex items-center justify-center">
<div className="w-full max-w-2xl flex items-center gap-2 sm:gap-3">
{/* 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">
<span className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
{t('market.filterByComponent')}:
</span>
<ToggleGroup
type="single"
spacing={2}
size="sm"
value={componentFilter}
onValueChange={(value) => {
if (value) handleComponentFilterChange(value);
}}
className="justify-start"
>
<ToggleGroupItem
value="all"
aria-label="All components"
className="text-xs sm:text-sm"
>
{t('market.allComponents')}
</ToggleGroupItem>
<ToggleGroupItem
value="Tool"
aria-label="Tool"
className="text-xs sm:text-sm"
>
<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"
>
<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"
>
<AudioWaveform className="h-4 w-4 mr-1" />
{t('plugins.componentName.EventListener')}
</ToggleGroupItem>
</ToggleGroup>
</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>

View File

@@ -12,6 +12,7 @@ import { toast } from 'sonner';
import { PluginV4 } from '@/app/infra/entities/plugin';
import { getCloudServiceClientSync } from '@/app/infra/http';
import { extractI18nObject } from '@/i18n/I18nProvider';
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
interface PluginDetailDialogProps {
open: boolean;
@@ -104,6 +105,15 @@ export default function PluginDetailDialog({
<Download className="w-4 h-4" />
{plugin!.install_count.toLocaleString()} {t('market.downloads')}
</Badge>
{plugin!.components && Object.keys(plugin!.components).length > 0 && (
<PluginComponentList
components={plugin!.components}
showComponentName={true}
showTitle={false}
useBadge={true}
t={t}
/>
)}
{plugin!.repository && (
<button
onClick={(e) => {

View File

@@ -1,4 +1,7 @@
import { PluginMarketCardVO } from './PluginMarketCardVO';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
export default function PluginMarketCardComponent({
cardVO,
@@ -7,18 +10,32 @@ export default function PluginMarketCardComponent({
cardVO: PluginMarketCardVO;
onPluginClick?: (author: string, pluginName: string) => void;
}) {
const { t } = useTranslation();
function handleCardClick() {
if (onPluginClick) {
onPluginClick(cardVO.author, cardVO.pluginName);
}
}
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" />,
};
const componentKindNameMap: Record<string, string> = {
Tool: t('plugins.componentName.Tool'),
EventListener: t('plugins.componentName.EventListener'),
Command: t('plugins.componentName.Command'),
};
return (
<div
className="w-[100%] h-auto min-h-[8rem] sm:h-[9rem] 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]"
className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] 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]"
onClick={handleCardClick}
>
<div className="w-full h-full flex flex-col justify-between gap-2">
<div className="w-full h-full flex flex-col justify-between gap-3">
{/* 上部分:插件信息 */}
<div className="flex flex-row items-start justify-start gap-2 sm:gap-[1.2rem] min-h-0">
<img
@@ -60,23 +77,45 @@ export default function PluginMarketCardComponent({
</div>
</div>
{/* 下部分:下载量 */}
<div className="w-full flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0">
<svg
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
{cardVO.installCount.toLocaleString()}
{/* 下部分:下载量和组件列表 */}
<div className="w-full flex flex-row items-center justify-between gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0">
<div className="flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem]">
<svg
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
{cardVO.installCount.toLocaleString()}
</div>
</div>
{/* 组件列表 */}
{cardVO.components && Object.keys(cardVO.components).length > 0 && (
<div className="flex flex-row items-center gap-1">
{Object.entries(cardVO.components).map(([kind, count]) => (
<Badge
key={kind}
variant="outline"
className="flex items-center gap-1"
>
{kindIconMap[kind]}
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
<span className="hidden md:inline">
{componentKindNameMap[kind]}
</span>
<span className="ml-1">{count}</span>
</Badge>
))}
</div>
)}
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@ export interface IPluginMarketCardVO {
iconURL: string;
githubURL: string;
version: string;
components?: Record<string, number>;
}
export class PluginMarketCardVO implements IPluginMarketCardVO {
@@ -20,6 +21,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
githubURL: string;
installCount: number;
version: string;
components?: Record<string, number>;
constructor(prop: IPluginMarketCardVO) {
this.description = prop.description;
@@ -31,5 +33,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
this.installCount = prop.installCount;
this.pluginId = prop.pluginId;
this.version = prop.version;
this.components = prop.components;
}
}

View File

@@ -40,6 +40,7 @@ export interface PluginV4 {
tags: string[];
install_count: number;
latest_version: string;
components: Record<string, number>;
status: PluginV4Status;
created_at: string;
updated_at: string;

View File

@@ -34,6 +34,7 @@ export class CloudServiceClient extends BaseHttpClient {
page_size: number,
sort_by?: string,
sort_order?: string,
component_filter?: string,
): Promise<ApiRespMarketplacePlugins> {
return this.post<ApiRespMarketplacePlugins>(
'/api/v1/marketplace/plugins/search',
@@ -43,6 +44,7 @@ export class CloudServiceClient extends BaseHttpClient {
page_size,
sort_by,
sort_order,
component_filter,
},
);
}

View File

@@ -8,32 +8,40 @@ import { cn } from '@/lib/utils';
import { toggleVariants } from '@/components/ui/toggle';
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
VariantProps<typeof toggleVariants> & {
spacing?: number;
}
>({
size: 'default',
variant: 'default',
spacing: 0,
});
function ToggleGroup({
className,
variant,
size,
spacing = 0,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
VariantProps<typeof toggleVariants> & {
spacing?: number;
}) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={{ '--gap': spacing } as React.CSSProperties}
className={cn(
'group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs',
'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs',
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
@@ -55,12 +63,14 @@ function ToggleGroupItem({
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l',
'w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10',
'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l',
className,
)}
{...props}

View File

@@ -7,7 +7,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:data-[state=on]:bg-slate-700 dark:data-[state=on]:text-white [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {

View File

@@ -351,6 +351,8 @@ const enUS = {
markAsRead: 'Mark as Read',
markAsReadSuccess: 'Marked as read',
markAsReadFailed: 'Mark as read failed',
filterByComponent: 'Component',
allComponents: 'All Components',
},
mcp: {
title: 'MCP',

View File

@@ -353,6 +353,8 @@ const jaJP = {
markAsRead: '既読',
markAsReadSuccess: '既読に設定しました',
markAsReadFailed: '既読に設定に失敗しました',
filterByComponent: 'コンポーネント',
allComponents: '全部コンポーネント',
},
mcp: {
title: 'MCP',

View File

@@ -335,6 +335,8 @@ const zhHans = {
markAsRead: '已读',
markAsReadSuccess: '已标记为已读',
markAsReadFailed: '标记为已读失败',
filterByComponent: '组件',
allComponents: '全部组件',
},
mcp: {
title: 'MCP',

View File

@@ -333,6 +333,8 @@ const zhHant = {
markAsRead: '已讀',
markAsReadSuccess: '已標記為已讀',
markAsReadFailed: '標記為已讀失敗',
filterByComponent: '組件',
allComponents: '全部組件',
},
mcp: {
title: 'MCP',