mirror of https://github.com/kortix-ai/suna.git
fix: improve integration tool interface
This commit is contained in:
parent
f713c51daf
commit
39cd190311
|
@ -89,31 +89,31 @@ const getStepIndex = (step: Step): number => {
|
|||
|
||||
const StepIndicator = ({ currentStep, mode }: { currentStep: Step; mode: 'full' | 'profile-only' }) => {
|
||||
const currentIndex = getStepIndex(currentStep);
|
||||
const visibleSteps = mode === 'profile-only'
|
||||
const visibleSteps = mode === 'profile-only'
|
||||
? stepConfigs.filter(step => step.id !== Step.ToolsSelection && step.id !== Step.ProfileSelect)
|
||||
: stepConfigs;
|
||||
|
||||
|
||||
const visibleCurrentIndex = visibleSteps.findIndex(step => step.id === currentStep);
|
||||
|
||||
return (
|
||||
<div className="px-8 py-6">
|
||||
<div className="flex items-center justify-between relative">
|
||||
<div className="absolute left-0 right-0 top-[14px] h-[2px] bg-muted-foreground/20 -z-10" />
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="absolute left-0 top-[14px] h-[2px] bg-primary -z-10"
|
||||
initial={{ width: 0 }}
|
||||
animate={{
|
||||
width: `${(visibleCurrentIndex / (visibleSteps.length - 1)) * 100}%`
|
||||
animate={{
|
||||
width: `${(visibleCurrentIndex / (visibleSteps.length - 1)) * 100}%`
|
||||
}}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
|
||||
{visibleSteps.map((step, index) => {
|
||||
const stepIndex = getStepIndex(step.id);
|
||||
const isCompleted = stepIndex < currentIndex;
|
||||
const isCurrent = step.id === currentStep;
|
||||
const isUpcoming = stepIndex > currentIndex;
|
||||
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={step.id}
|
||||
|
@ -195,7 +195,7 @@ const InitiationFieldInput = ({ field, value, onChange, error }: {
|
|||
{field.required && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
|
||||
{isBooleanField ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
|
@ -220,7 +220,7 @@ const InitiationFieldInput = ({ field, value, onChange, error }: {
|
|||
step={isNumberField ? "any" : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{field.description && !isBooleanField && (
|
||||
<div className="flex items-start gap-2 text-xs text-muted-foreground">
|
||||
<Info className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
||||
|
@ -247,13 +247,13 @@ const ToolPreviewCard = ({ tool, searchTerm }: {
|
|||
if (!term) return text;
|
||||
const regex = new RegExp(`(${term})`, 'gi');
|
||||
const parts = text.split(regex);
|
||||
return parts.map((part, index) =>
|
||||
regex.test(part) ?
|
||||
<mark key={index} className="bg-yellow-200 dark:bg-yellow-900 px-0.5">{part}</mark> :
|
||||
return parts.map((part, index) =>
|
||||
regex.test(part) ?
|
||||
<mark key={index} className="bg-yellow-200 dark:bg-yellow-900 px-0.5">{part}</mark> :
|
||||
part
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Generate icon based on tool name/category
|
||||
const getToolIcon = (toolName: string) => {
|
||||
const name = toolName.toLowerCase();
|
||||
|
@ -282,9 +282,9 @@ const ToolPreviewCard = ({ tool, searchTerm }: {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className="border rounded-md p-2 hover:bg-muted/30 transition-colors group cursor-pointer"
|
||||
title={tool.description} // Show full description on hover
|
||||
>
|
||||
|
@ -322,7 +322,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
const [showToolsManager, setShowToolsManager] = useState(false);
|
||||
const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
|
||||
const [selectedConnectionType, setSelectedConnectionType] = useState<'existing' | 'new' | null>(null);
|
||||
|
||||
|
||||
const [initiationFields, setInitiationFields] = useState<Record<string, string>>({});
|
||||
const [initiationFieldsErrors, setInitiationFieldsErrors] = useState<Record<string, string>>({});
|
||||
|
||||
|
@ -330,13 +330,13 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
|
||||
const { mutate: createProfile, isPending: isCreating } = useCreateComposioProfile();
|
||||
const { data: profiles, isLoading: isLoadingProfiles } = useComposioProfiles();
|
||||
|
||||
|
||||
const { data: toolkitDetails, isLoading: isLoadingToolkitDetails } = useComposioToolkitDetails(
|
||||
app.slug,
|
||||
{ enabled: open && currentStep === Step.ProfileCreate }
|
||||
);
|
||||
|
||||
const existingProfiles = profiles?.filter(p =>
|
||||
const existingProfiles = profiles?.filter(p =>
|
||||
p.toolkit_slug === app.slug && p.is_connected
|
||||
) || [];
|
||||
|
||||
|
@ -344,13 +344,13 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
|
||||
const [toolsPreviewSearchTerm, setToolsPreviewSearchTerm] = useState('');
|
||||
const { data: toolsResponse, isLoading: isLoadingToolsPreview } = useComposioTools(
|
||||
app.slug,
|
||||
{
|
||||
app.slug,
|
||||
{
|
||||
enabled: open && currentStep === Step.ProfileSelect,
|
||||
limit: 50
|
||||
limit: 50
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const availableToolsPreview = toolsResponse?.tools || [];
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -382,38 +382,38 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
const validateInitiationFields = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
const initiationRequirements = toolkitDetails?.toolkit.connected_account_initiation_fields;
|
||||
|
||||
|
||||
if (initiationRequirements?.required) {
|
||||
for (const field of initiationRequirements.required) {
|
||||
if (field.required) {
|
||||
const value = initiationFields[field.name];
|
||||
const isEmpty = !value || value.trim() === '';
|
||||
|
||||
|
||||
if (field.type.toLowerCase() === 'boolean') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if ((field.type.toLowerCase() === 'number' || field.type.toLowerCase() === 'double') && value) {
|
||||
if (isNaN(Number(value))) {
|
||||
newErrors[field.name] = `${field.displayName} must be a valid number`;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isEmpty) {
|
||||
newErrors[field.name] = `${field.displayName} is required`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setInitiationFieldsErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSaveTools = async () => {
|
||||
if (!selectedProfile || !agentId) return;
|
||||
|
||||
|
||||
const mcpConfigResponse = await composioApi.getMcpConfigForProfile(selectedProfile.profile_id);
|
||||
const response = await backendApi.put(`/agents/${agentId}/custom-mcp-tools`, {
|
||||
custom_mcps: [{
|
||||
|
@ -458,12 +458,12 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
toast.error('Profile name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!validateInitiationFields()) {
|
||||
toast.error('Please fill in all required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
createProfile({
|
||||
toolkit_slug: app.slug,
|
||||
profile_name: profileName,
|
||||
|
@ -556,8 +556,8 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const filteredToolsPreview = availableToolsPreview.filter(tool =>
|
||||
!toolsPreviewSearchTerm ||
|
||||
const filteredToolsPreview = availableToolsPreview.filter(tool =>
|
||||
!toolsPreviewSearchTerm ||
|
||||
tool.name.toLowerCase().includes(toolsPreviewSearchTerm.toLowerCase()) ||
|
||||
tool.description.toLowerCase().includes(toolsPreviewSearchTerm.toLowerCase()) ||
|
||||
tool.tags?.some(tag => tag.toLowerCase().includes(toolsPreviewSearchTerm.toLowerCase()))
|
||||
|
@ -586,11 +586,11 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={cn(
|
||||
"overflow-hidden gap-0",
|
||||
currentStep === Step.ToolsSelection ? "max-w-2xl h-[85vh] p-0 flex flex-col" :
|
||||
currentStep === Step.ProfileSelect ? "max-w-2xl p-0" : "max-w-lg p-0"
|
||||
currentStep === Step.ToolsSelection ? "max-w-2xl h-[85vh] p-0 flex flex-col" :
|
||||
currentStep === Step.ProfileSelect ? "max-w-2xl p-0" : "max-w-lg p-0"
|
||||
)}>
|
||||
<StepIndicator currentStep={currentStep} mode={mode} />
|
||||
|
||||
|
||||
{currentStep !== Step.ToolsSelection ? (
|
||||
<>
|
||||
<DialogHeader className="px-8 pb-2">
|
||||
|
@ -647,10 +647,10 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
{showToolsManager ? 'Hide' : 'View'} Tools
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid gap-3">
|
||||
{existingProfiles.length > 0 && (
|
||||
<Card
|
||||
<Card
|
||||
className={cn(
|
||||
"cursor-pointer p-0 transition-all",
|
||||
selectedConnectionType === 'existing' ? "border-primary bg-primary/5" : "border-border hover:border-border/80"
|
||||
|
@ -693,22 +693,22 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<SelectValue placeholder="Select a profile..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{existingProfiles.map((profile) => (
|
||||
<SelectItem key={profile.profile_id} value={profile.profile_id}>
|
||||
<div className="flex items-center gap-3">
|
||||
{app.logo ? (
|
||||
<img src={app.logo} alt={app.name} className="w-5 h-5 rounded-lg object-contain bg-muted p-0.5 border flex-shrink-0" />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-lg bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center text-primary text-xs font-semibold flex-shrink-0">
|
||||
{app.name.charAt(0)}
|
||||
{existingProfiles.map((profile) => (
|
||||
<SelectItem key={profile.profile_id} value={profile.profile_id}>
|
||||
<div className="flex items-center gap-3">
|
||||
{app.logo ? (
|
||||
<img src={app.logo} alt={app.name} className="w-5 h-5 rounded-lg object-contain bg-muted p-0.5 border flex-shrink-0" />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-lg bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center text-primary text-xs font-semibold flex-shrink-0">
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">{profile.profile_name}</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">{profile.profile_name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
@ -718,7 +718,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
|
||||
<Card className={cn(
|
||||
"cursor-pointer p-0 transition-all",
|
||||
selectedConnectionType === 'new' ? "border-primary bg-primary/5" : "border-border hover:border-border/80"
|
||||
|
@ -748,51 +748,51 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{showToolsManager && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ScrollArea className="h-[200px] border rounded-md bg-muted/10">
|
||||
<div className="p-2">
|
||||
{isLoadingToolsPreview ? (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div key={i} className="border rounded-md p-2 animate-pulse bg-background/50">
|
||||
<div className="h-2 bg-muted rounded w-3/4 mb-1"></div>
|
||||
<div className="h-2 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredToolsPreview.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{filteredToolsPreview.map((tool) => (
|
||||
<ToolPreviewCard
|
||||
key={tool.slug}
|
||||
tool={tool}
|
||||
searchTerm={toolsPreviewSearchTerm}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<Search className="h-4 w-4 mx-auto mb-2" />
|
||||
<p className="text-xs">
|
||||
{toolsPreviewSearchTerm ? 'No matches' : 'No tools available'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{showToolsManager && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ScrollArea className="h-[200px] border rounded-md bg-muted/10">
|
||||
<div className="p-2">
|
||||
{isLoadingToolsPreview ? (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div key={i} className="border rounded-md p-2 animate-pulse bg-background/50">
|
||||
<div className="h-2 bg-muted rounded w-3/4 mb-1"></div>
|
||||
<div className="h-2 bg-muted rounded w-1/2"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : filteredToolsPreview.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{filteredToolsPreview.map((tool) => (
|
||||
<ToolPreviewCard
|
||||
key={tool.slug}
|
||||
tool={tool}
|
||||
searchTerm={toolsPreviewSearchTerm}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<Search className="h-4 w-4 mx-auto mb-2" />
|
||||
<p className="text-xs">
|
||||
{toolsPreviewSearchTerm ? 'No matches' : 'No tools available'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -800,10 +800,10 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<div className="px-6 py-4 border-t border-border/50 bg-muted/20 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{selectedConnectionType === 'new' ? 'Ready to create new connection' :
|
||||
selectedConnectionType === 'existing' && selectedProfileId ? 'Profile selected' :
|
||||
selectedConnectionType === 'existing' ? 'Select a profile to continue' :
|
||||
'Choose how you want to connect'}
|
||||
{selectedConnectionType === 'new' ? 'Ready to create new connection' :
|
||||
selectedConnectionType === 'existing' && selectedProfileId ? 'Profile selected' :
|
||||
selectedConnectionType === 'existing' ? 'Select a profile to continue' :
|
||||
'Choose how you want to connect'}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
|
@ -1021,9 +1021,9 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
<DialogHeader className="px-8 border-border/50 flex-shrink-0 bg-muted/10">
|
||||
<div className="flex items-center gap-4">
|
||||
{app.logo ? (
|
||||
<img
|
||||
src={app.logo}
|
||||
alt={app.name}
|
||||
<img
|
||||
src={app.logo}
|
||||
alt={app.name}
|
||||
className="w-14 h-14 rounded-xl object-contain bg-muted p-2 border"
|
||||
/>
|
||||
) : (
|
||||
|
@ -1058,6 +1058,7 @@ export const ComposioConnector: React.FC<ComposioConnectorProps> = ({
|
|||
profileId={selectedProfile.profile_id}
|
||||
agentId={agentId}
|
||||
toolkitName={selectedProfile.toolkit_name || app.name}
|
||||
toolkitSlug={selectedProfile.toolkit_slug || app.slug}
|
||||
selectedTools={selectedTools}
|
||||
onToolsChange={setSelectedTools}
|
||||
onSave={handleSaveTools}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { useComposioProfiles } from '@/hooks/react-query/composio/use-composio-profiles';
|
||||
|
@ -46,9 +46,40 @@ export const ComposioToolsManager: React.FC<ComposioToolsManagerProps> = ({
|
|||
enabled: !!currentProfile?.toolkit_slug
|
||||
});
|
||||
|
||||
// Load current tools when dialog opens
|
||||
useEffect(() => {
|
||||
const loadCurrentTools = async () => {
|
||||
if (!open || !agentId || !profileId) return;
|
||||
|
||||
try {
|
||||
const response = await backendApi.get(`/agents/${agentId}`);
|
||||
if (response.success && response.data) {
|
||||
const agent = response.data;
|
||||
const composioMcps = agent.custom_mcps?.filter((mcp: any) =>
|
||||
mcp.type === 'composio' && mcp.config?.profile_id === profileId
|
||||
) || [];
|
||||
|
||||
const enabledTools = composioMcps.flatMap((mcp: any) => mcp.enabledTools || []);
|
||||
console.log('Loading current tools for editing:', enabledTools);
|
||||
setSelectedTools(enabledTools);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load current tools:', error);
|
||||
setSelectedTools([]);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
loadCurrentTools();
|
||||
} else {
|
||||
// Reset when dialog closes
|
||||
setSelectedTools([]);
|
||||
}
|
||||
}, [open, agentId, profileId]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!currentProfile) return;
|
||||
|
||||
|
||||
const mcpConfigResponse = await composioApi.getMcpConfigForProfile(currentProfile.profile_id);
|
||||
const response = await backendApi.put(`/agents/${agentId}/custom-mcp-tools`, {
|
||||
custom_mcps: [{
|
||||
|
@ -71,9 +102,9 @@ export const ComposioToolsManager: React.FC<ComposioToolsManagerProps> = ({
|
|||
<DialogHeader className="p-6 pb-4 border-b flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
{iconData?.icon_url || appLogo ? (
|
||||
<img
|
||||
src={iconData?.icon_url || appLogo}
|
||||
alt={currentProfile?.toolkit_name}
|
||||
<img
|
||||
src={iconData?.icon_url || appLogo}
|
||||
alt={currentProfile?.toolkit_name}
|
||||
className="w-10 h-10 rounded-lg border object-contain bg-muted p-1"
|
||||
/>
|
||||
) : (
|
||||
|
@ -91,11 +122,12 @@ export const ComposioToolsManager: React.FC<ComposioToolsManagerProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<ComposioToolsSelector
|
||||
profileId={currentProfile.profile_id}
|
||||
agentId={agentId}
|
||||
toolkitName={currentProfile.toolkit_name}
|
||||
toolkitSlug={currentProfile.toolkit_slug}
|
||||
selectedTools={selectedTools}
|
||||
onToolsChange={setSelectedTools}
|
||||
onSave={handleSave}
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Search, Save, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Search, Save, AlertCircle, Loader2, Filter, X, ChevronDown } from 'lucide-react';
|
||||
import { backendApi } from '@/lib/api-client';
|
||||
import { composioApi } from '@/hooks/react-query/composio/utils';
|
||||
import { useComposioTools } from '@/hooks/react-query/composio/use-composio';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: any;
|
||||
}
|
||||
import type { ComposioTool } from '@/hooks/react-query/composio/utils';
|
||||
|
||||
interface ComposioToolsSelectorProps {
|
||||
profileId: string;
|
||||
profileId?: string;
|
||||
agentId?: string;
|
||||
toolkitName: string;
|
||||
toolkitSlug: string;
|
||||
selectedTools: string[];
|
||||
onToolsChange: (tools: string[]) => void;
|
||||
onSave?: () => Promise<void>;
|
||||
|
@ -33,7 +37,7 @@ interface ComposioToolsSelectorProps {
|
|||
}
|
||||
|
||||
const ToolCard = ({ tool, isSelected, onToggle, searchTerm }: {
|
||||
tool: Tool;
|
||||
tool: ComposioTool;
|
||||
isSelected: boolean;
|
||||
onToggle: () => void;
|
||||
searchTerm: string;
|
||||
|
@ -42,7 +46,7 @@ const ToolCard = ({ tool, isSelected, onToggle, searchTerm }: {
|
|||
if (!term) return text;
|
||||
const regex = new RegExp(`(${term})`, 'gi');
|
||||
const parts = text.split(regex);
|
||||
return parts.map((part, i) =>
|
||||
return parts.map((part, i) =>
|
||||
regex.test(part) ? <mark key={i} className="bg-yellow-200 dark:bg-yellow-900/50">{part}</mark> : part
|
||||
);
|
||||
};
|
||||
|
@ -55,30 +59,39 @@ const ToolCard = ({ tool, isSelected, onToggle, searchTerm }: {
|
|||
<CardContent className="p-4" onClick={onToggle}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-medium text-sm truncate">
|
||||
{highlightText(tool.name, searchTerm)}
|
||||
</h3>
|
||||
{tool.tags?.includes('important') && (
|
||||
<Badge variant="secondary" className="text-xs bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">
|
||||
Important
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
|
||||
{highlightText(tool.description || 'No description available', searchTerm)}
|
||||
</p>
|
||||
|
||||
{tool.parameters?.properties && (
|
||||
<div className="mt-2">
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{tool.input_parameters?.properties && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{Object.keys(tool.parameters.properties).length} parameters
|
||||
{Object.keys(tool.input_parameters.properties).length} params
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{tool.tags?.filter(tag => tag !== 'important').slice(0, 2).map(tag => (
|
||||
<Badge key={tag} variant="outline" className="text-xs text-muted-foreground">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex-shrink-0 ml-2">
|
||||
<Switch
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={onToggle}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -106,6 +119,7 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
|
|||
profileId,
|
||||
agentId,
|
||||
toolkitName,
|
||||
toolkitSlug,
|
||||
selectedTools,
|
||||
onToolsChange,
|
||||
onSave,
|
||||
|
@ -115,103 +129,223 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
|
|||
searchPlaceholder = "Search tools..."
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [availableTools, setAvailableTools] = useState<Tool[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedTagFilters, setSelectedTagFilters] = useState<Set<string>>(new Set());
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
const filteredTools = useMemo(() => {
|
||||
if (!searchTerm) return availableTools;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return availableTools.filter(tool =>
|
||||
tool.name.toLowerCase().includes(term) ||
|
||||
(tool.description && tool.description.toLowerCase().includes(term))
|
||||
);
|
||||
}, [availableTools, searchTerm]);
|
||||
const { data: toolsResponse, isLoading, error } = useComposioTools(toolkitSlug, { enabled: !!toolkitSlug });
|
||||
|
||||
useEffect(() => {
|
||||
if (profileId) {
|
||||
console.log('ComposioToolsSelector: Loading tools for profile', profileId);
|
||||
loadTools();
|
||||
if (agentId) {
|
||||
loadCurrentAgentTools();
|
||||
}
|
||||
}
|
||||
}, [profileId, agentId]);
|
||||
// DEDUPE: Remove duplicate tools by name, prioritizing ones with "important" tag
|
||||
const availableTools = useMemo(() => {
|
||||
const rawTools = toolsResponse?.tools || [];
|
||||
const toolMap = new Map();
|
||||
|
||||
const loadTools = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log('ComposioToolsSelector: Discovering tools for profile', profileId);
|
||||
const response = await composioApi.discoverTools(profileId);
|
||||
console.log('ComposioToolsSelector: Tools response', response);
|
||||
if (response.success && response.tools) {
|
||||
setAvailableTools(response.tools);
|
||||
console.log('ComposioToolsSelector: Loaded', response.tools.length, 'tools');
|
||||
// First pass: add all tools
|
||||
rawTools.forEach(tool => {
|
||||
const existing = toolMap.get(tool.name);
|
||||
if (!existing) {
|
||||
toolMap.set(tool.name, tool);
|
||||
} else {
|
||||
setError('Failed to load available tools');
|
||||
console.error('ComposioToolsSelector: Failed to load tools', response);
|
||||
// If we have a duplicate, prefer the one with "important" tag
|
||||
const hasImportant = tool.tags?.includes('important');
|
||||
const existingHasImportant = existing.tags?.includes('important');
|
||||
|
||||
if (hasImportant && !existingHasImportant) {
|
||||
toolMap.set(tool.name, tool);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to load tools');
|
||||
console.error('ComposioToolsSelector: Error loading tools', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return Array.from(toolMap.values());
|
||||
}, [toolsResponse?.tools]);
|
||||
|
||||
const loadCurrentAgentTools = async () => {
|
||||
if (!agentId) return;
|
||||
|
||||
if (!agentId || !profileId) {
|
||||
console.log('Missing agentId or profileId:', { agentId, profileId });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading tools for agent:', { agentId, profileId });
|
||||
|
||||
try {
|
||||
console.log('ComposioToolsSelector: Loading current agent tools for agent', agentId, 'profile', profileId);
|
||||
const response = await backendApi.get(`/agents/${agentId}`);
|
||||
console.log('Agent response:', response);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const agent = response.data;
|
||||
console.log('ComposioToolsSelector: Agent data', agent);
|
||||
const composioMcps = agent.custom_mcps?.filter((mcp: any) =>
|
||||
const composioMcps = agent.custom_mcps?.filter((mcp: any) =>
|
||||
mcp.type === 'composio' && mcp.config?.profile_id === profileId
|
||||
) || [];
|
||||
|
||||
|
||||
console.log('Found composio MCPs:', composioMcps);
|
||||
|
||||
const enabledTools = composioMcps.flatMap((mcp: any) => mcp.enabledTools || []);
|
||||
console.log('ComposioToolsSelector: Found enabled tools', enabledTools);
|
||||
onToolsChange(enabledTools);
|
||||
console.log('Enabled tools from agent:', enabledTools);
|
||||
|
||||
// If no existing tools found, auto-select important tools
|
||||
if (enabledTools.length === 0 && availableTools.length > 0) {
|
||||
const importantTools = availableTools.filter(tool => tool.tags?.includes('important'));
|
||||
const importantToolNames = importantTools.map(tool => tool.name);
|
||||
|
||||
if (importantToolNames.length > 0) {
|
||||
console.log('No existing tools, selecting important tools:', importantToolNames);
|
||||
onToolsChange(importantToolNames);
|
||||
} else {
|
||||
console.log('No existing tools and no important tools, selecting none');
|
||||
onToolsChange([]);
|
||||
}
|
||||
} else {
|
||||
console.log('Setting tools from agent:', enabledTools);
|
||||
onToolsChange(enabledTools);
|
||||
}
|
||||
|
||||
initializedRef.current = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('ComposioToolsSelector: Failed to load current agent tools:', err);
|
||||
console.error('Failed to load current agent tools:', err);
|
||||
// If API call fails, fall back to important tools
|
||||
if (availableTools.length > 0) {
|
||||
const importantTools = availableTools.filter(tool => tool.tags?.includes('important'));
|
||||
const importantToolNames = importantTools.map(tool => tool.name);
|
||||
if (importantToolNames.length > 0) {
|
||||
onToolsChange(importantToolNames);
|
||||
} else {
|
||||
onToolsChange([]);
|
||||
}
|
||||
}
|
||||
initializedRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
// INSTANT: Show important tools as selected the moment they appear
|
||||
const getEffectiveSelectedTools = useMemo(() => {
|
||||
// Always use explicit selections if they exist
|
||||
if (selectedTools.length > 0) {
|
||||
return selectedTools;
|
||||
}
|
||||
|
||||
// Only auto-select important tools for NEW agents (no agentId) when no tools are selected
|
||||
if (!agentId && availableTools.length > 0) {
|
||||
const importantTools = availableTools.filter(tool => tool.tags?.includes('important'));
|
||||
const importantToolNames = importantTools.map(tool => tool.name);
|
||||
|
||||
console.log('Auto-selecting important tools for new agent:', {
|
||||
totalTools: availableTools.length,
|
||||
importantTools: importantTools.length,
|
||||
importantToolNames,
|
||||
});
|
||||
|
||||
return importantToolNames;
|
||||
}
|
||||
|
||||
// For existing agents, return empty if no tools are explicitly selected
|
||||
// (they should be loaded via loadCurrentAgentTools)
|
||||
return selectedTools;
|
||||
}, [selectedTools, availableTools, agentId]);
|
||||
|
||||
// Get all unique tags
|
||||
const allTags = useMemo(() => {
|
||||
const tags = new Set<string>();
|
||||
availableTools.forEach(tool => {
|
||||
tool.tags?.forEach(tag => tags.add(tag));
|
||||
});
|
||||
return Array.from(tags).sort((a, b) => {
|
||||
// Put "important" first, then alphabetical
|
||||
if (a === 'important') return -1;
|
||||
if (b === 'important') return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}, [availableTools]);
|
||||
|
||||
// Sync the effective selection back to parent when tools load
|
||||
useEffect(() => {
|
||||
console.log('useEffect triggered:', {
|
||||
initialized: initializedRef.current,
|
||||
availableToolsCount: availableTools.length,
|
||||
agentId,
|
||||
profileId,
|
||||
selectedToolsCount: selectedTools.length
|
||||
});
|
||||
|
||||
if (!initializedRef.current && availableTools.length > 0) {
|
||||
initializedRef.current = true;
|
||||
|
||||
if (agentId && profileId) {
|
||||
// For existing agents, load their tools
|
||||
console.log('Loading tools for existing agent');
|
||||
loadCurrentAgentTools();
|
||||
} else if (selectedTools.length === 0) {
|
||||
// For new agents, sync the important tools selection
|
||||
console.log('Setting up new agent with important tools');
|
||||
const importantToolNames = availableTools
|
||||
.filter(tool => tool.tags?.includes('important'))
|
||||
.map(tool => tool.name);
|
||||
|
||||
if (importantToolNames.length > 0) {
|
||||
onToolsChange(importantToolNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [availableTools, agentId, profileId]); // Added agentId and profileId as dependencies
|
||||
|
||||
// Filter tools based on selected tag filters and search
|
||||
const filteredTools = useMemo(() => {
|
||||
let tools = availableTools;
|
||||
|
||||
// Filter by tags if any are selected
|
||||
if (selectedTagFilters.size > 0) {
|
||||
tools = tools.filter(tool =>
|
||||
tool.tags?.some(tag => selectedTagFilters.has(tag)) ||
|
||||
(selectedTagFilters.has('untagged') && (!tool.tags || tool.tags.length === 0))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
tools = tools.filter(tool =>
|
||||
tool.name.toLowerCase().includes(term) ||
|
||||
tool.description.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}, [availableTools, selectedTagFilters, searchTerm]);
|
||||
|
||||
const handleToolToggle = (toolName: string) => {
|
||||
const newTools = selectedTools.includes(toolName)
|
||||
? selectedTools.filter(t => t !== toolName)
|
||||
: [...selectedTools, toolName];
|
||||
const effectiveSelected = getEffectiveSelectedTools;
|
||||
const newTools = effectiveSelected.includes(toolName)
|
||||
? effectiveSelected.filter(t => t !== toolName)
|
||||
: [...effectiveSelected, toolName];
|
||||
onToolsChange(newTools);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const allToolNames = filteredTools.map(tool => tool.name);
|
||||
const hasAll = allToolNames.every(name => selectedTools.includes(name));
|
||||
|
||||
if (hasAll) {
|
||||
const newTools = selectedTools.filter(name => !allToolNames.includes(name));
|
||||
onToolsChange(newTools);
|
||||
onToolsChange(allToolNames);
|
||||
};
|
||||
|
||||
const handleSelectNone = () => {
|
||||
onToolsChange([]);
|
||||
};
|
||||
|
||||
const handleTagFilterToggle = (tag: string) => {
|
||||
const newFilters = new Set(selectedTagFilters);
|
||||
|
||||
if (newFilters.has(tag)) {
|
||||
// Remove tag filter - just affects visibility
|
||||
newFilters.delete(tag);
|
||||
} else {
|
||||
const newTools = [...selectedTools];
|
||||
allToolNames.forEach(name => {
|
||||
if (!newTools.includes(name)) {
|
||||
newTools.push(name);
|
||||
}
|
||||
});
|
||||
onToolsChange(newTools);
|
||||
// Add tag filter - just affects visibility
|
||||
newFilters.add(tag);
|
||||
}
|
||||
|
||||
setSelectedTagFilters(newFilters);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!agentId || !onSave) return;
|
||||
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await onSave();
|
||||
|
@ -224,14 +358,13 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
|
|||
};
|
||||
|
||||
const selectedCount = selectedTools.length;
|
||||
const filteredSelectedCount = filteredTools.filter(tool => selectedTools.includes(tool.name)).length;
|
||||
const allFilteredSelected = filteredTools.length > 0 && filteredSelectedCount === filteredTools.length;
|
||||
const totalTools = availableTools.length;
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col h-full", className)}>
|
||||
{/* Search and Controls Bar */}
|
||||
<div className="px-6 py-3 border-b bg-muted/20 flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
|
@ -241,30 +374,122 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
|
|||
className="pl-10 h-9 bg-background border-0 focus-visible:ring-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="text-muted-foreground whitespace-nowrap">
|
||||
{filteredTools.length} {searchTerm && `of ${availableTools.length}`} tools
|
||||
</span>
|
||||
|
||||
{selectedCount > 0 && (
|
||||
<Badge variant="secondary" className="bg-primary/10 text-primary border-0">
|
||||
{selectedCount}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{filteredTools.length > 0 && (
|
||||
|
||||
{/* Tag Filter Dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSelectAll}
|
||||
className="h-8 text-xs"
|
||||
className={cn(
|
||||
"h-9 gap-2 shadow-none",
|
||||
selectedTagFilters.size > 0 && "border-primary/50 bg-primary/5"
|
||||
)}
|
||||
>
|
||||
{allFilteredSelected ? 'Deselect' : 'Select'} All
|
||||
<Filter className="h-4 w-4" />
|
||||
<span>Filter
|
||||
<span className="text-muted-foreground"> ({selectedTagFilters.size || 0})</span>
|
||||
</span>
|
||||
|
||||
<ChevronDown className="h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-80"
|
||||
align="start"
|
||||
side="bottom"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="flex items-center justify-between">
|
||||
<span>Filter by tags</span>
|
||||
{selectedTagFilters.size > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedTagFilters(new Set())}
|
||||
className="h-6 px-2 text-xs text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="h-3 w-3 mr-1" />
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="max-h-56 overflow-y-auto">
|
||||
{allTags.map(tag => {
|
||||
const isSelected = selectedTagFilters.has(tag);
|
||||
const toolsWithTag = availableTools.filter(tool => tool.tags?.includes(tag));
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={tag}
|
||||
className="flex items-center space-x-3 cursor-pointer focus:bg-accent"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleTagFilterToggle(tag);
|
||||
}}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={() => handleTagFilterToggle(tag)}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={cn(
|
||||
"text-sm font-medium truncate",
|
||||
tag === 'important' && "text-orange-600"
|
||||
)}>
|
||||
{tag}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs ml-2 shrink-0">
|
||||
{toolsWithTag.length}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Compact Filter + Quick Actions */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-muted-foreground whitespace-nowrap text-sm">
|
||||
Showing {filteredTools.length} of {availableTools.length} tools
|
||||
</span>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleSelectAll}
|
||||
disabled={isLoading || filteredTools.length === 0}
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
Select All
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleSelectNone}
|
||||
disabled={isLoading}
|
||||
className="h-8 text-xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Tools List */}
|
||||
|
@ -273,23 +498,23 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
|
|||
{error && (
|
||||
<Alert className="mb-6">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
<AlertDescription>{error.message || 'Failed to load tools'}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<ToolSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : filteredTools.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-3">
|
||||
{filteredTools.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.name}
|
||||
key={tool.slug}
|
||||
tool={tool}
|
||||
isSelected={selectedTools.includes(tool.name)}
|
||||
isSelected={getEffectiveSelectedTools.includes(tool.name)}
|
||||
onToggle={() => handleToolToggle(tool.name)}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
|
@ -300,9 +525,24 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
|
|||
<div className="w-12 h-12 rounded-xl bg-muted/50 flex items-center justify-center mx-auto mb-4">
|
||||
<Search className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{searchTerm ? `No tools found matching "${searchTerm}"` : 'No tools available'}
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{searchTerm
|
||||
? `No tools found matching "${searchTerm}"`
|
||||
: selectedTagFilters.size > 0
|
||||
? 'No tools match the selected filters'
|
||||
: 'No tools available'
|
||||
}
|
||||
</p>
|
||||
{selectedTagFilters.size > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedTagFilters(new Set())}
|
||||
className="mt-2"
|
||||
>
|
||||
Clear all filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -313,8 +553,8 @@ export const ComposioToolsSelector: React.FC<ComposioToolsSelectorProps> = ({
|
|||
<div className="p-6 pt-4 border-t bg-muted/20 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{selectedCount > 0 ? (
|
||||
`${selectedCount} tool${selectedCount === 1 ? '' : 's'} will be added to your agent`
|
||||
{getEffectiveSelectedTools.length > 0 ? (
|
||||
`${getEffectiveSelectedTools.length} tool${getEffectiveSelectedTools.length === 1 ? '' : 's'} will be added to your agent`
|
||||
) : (
|
||||
'No tools selected'
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue