Tools UI updates

* Make UI UX fixes to tools and tool configs

* Fix font sizing and dark mode issues for tool labels

* Remove subtitle in tools selection list
This commit is contained in:
Akhilesh Sudhakar 2025-07-24 19:19:28 +05:30 committed by GitHub
parent a298036b4b
commit da58903f67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 289 additions and 171 deletions

View file

@ -14,6 +14,7 @@ import { ToolParamCard } from "@/components/common/tool-param-card";
import { UserIcon, Settings, Settings2 } from "lucide-react";
import { EditableField } from "@/app/lib/components/editable-field";
import Link from "next/link";
import { Tooltip } from "@heroui/react";
// Update textarea styles with improved states
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500";
@ -446,13 +447,15 @@ export function ToolConfig({
{/* Mock Section */}
<SectionCard
icon={<Settings className="w-5 h-5 text-indigo-500" />}
title="Mock responses"
labelWidth="md:w-64"
title={<span className="whitespace-nowrap">Mock responses</span>}
labelWidth="md:w-32"
className="mb-1"
singleColumnFields={true}
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-1">Mock tool responses</label>
<div className="flex items-center gap-2 mb-1">
<Switch
isSelected={tool.mockTool}
onValueChange={(value) => handleUpdate({
@ -462,16 +465,13 @@ export function ToolConfig({
size="sm"
color="primary"
/>
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300">
Mock tool responses
</label>
<span className="text-xs text-gray-500 dark:text-gray-400">
When enabled, this tool will be mocked.
</span>
</div>
<span className="text-xs text-gray-500 dark:text-gray-400 ml-12">
When enabled, this tool will be mocked.
</span>
</div>
{tool.mockTool && (
<div className="flex flex-col gap-2 ml-12">
<div className="flex flex-col gap-1 mt-4">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-1">Mock Response Instructions</label>
<span className="text-xs text-gray-500 dark:text-gray-400 mb-1">Describe the response the mock tool should return. This will be shown in the chat when the tool is called.</span>
<EditableField

View file

@ -18,15 +18,19 @@ type ToolkitType = z.infer<typeof ZToolkit>;
type ToolkitListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>;
type ProjectType = z.infer<typeof Project>;
export function Composio({
projectId,
tools,
onAddTool
}: {
interface ComposioProps {
projectId: string;
tools: z.infer<typeof Workflow.shape.tools>;
onAddTool: (tool: z.infer<typeof WorkflowTool>) => void;
}) {
initialToolkitSlug?: string | null;
}
export function Composio({
projectId,
tools,
onAddTool,
initialToolkitSlug
}: ComposioProps) {
const [toolkits, setToolkits] = useState<ToolkitType[]>([]);
const [projectConfig, setProjectConfig] = useState<ProjectType | null>(null);
const [loading, setLoading] = useState(true);
@ -97,6 +101,17 @@ export function Composio({
loadAllToolkits();
}, [loadAllToolkits]);
// Auto-select toolkit if initialToolkitSlug is provided
useEffect(() => {
if (initialToolkitSlug && toolkits.length > 0) {
const toolkit = toolkits.find(t => t.slug === initialToolkitSlug);
if (toolkit) {
setSelectedToolkit(toolkit);
setIsToolsPanelOpen(true);
}
}
}, [initialToolkitSlug, toolkits]);
const filteredToolkits = toolkits.filter(toolkit => {
const searchLower = searchQuery.toLowerCase();
return (

View file

@ -200,9 +200,6 @@ export function ComposioToolsPanel({
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Select Tools</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Check the tools you want to add to your workflow
</p>
</div>
{hasChanges && (
<Button

View file

@ -121,9 +121,6 @@ export function McpToolsPanel({
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Select Tools</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Check the tools you want to add to your workflow
</p>
</div>
{hasChanges && (
<Button

View file

@ -318,7 +318,7 @@ export function ToolkitAuthModal({
<Modal
isOpen={isOpen}
onOpenChange={onClose}
size="md"
size="lg"
classNames={{
base: "bg-white dark:bg-gray-900",
header: "border-b border-gray-200 dark:border-gray-800",
@ -415,15 +415,15 @@ export function ToolkitAuthModal({
) : (
// Auth options view
<div className="space-y-4">
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-4 mt-2">
Choose how you&apos;d like to authenticate with this toolkit:
</div>
<div className="space-y-3">
<div className="space-y-6">
{/* OAuth2 Composio Managed */}
{toolkit.composio_managed_auth_schemes.includes('OAUTH2') && (
<HeroButton
className="w-full justify-start gap-3 h-auto py-4 px-4"
className="w-full justify-start gap-3 h-auto py-5 px-5 border-2 border-blue-500 bg-blue-50 dark:bg-blue-900/20"
variant="bordered"
onPress={handleComposioOAuth2}
isDisabled={processing}
@ -432,8 +432,11 @@ export function ToolkitAuthModal({
<div className="bg-green-100 dark:bg-green-900/20 p-2 rounded-lg">
<Shield className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div className="text-left">
<div className="font-medium">Connect using OAuth2</div>
<div className="text-left flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="font-medium text-base">Connect using OAuth2</div>
<span className="inline-block px-2 py-0.5 text-xs rounded-full bg-blue-500 text-white font-semibold">Most popular</span>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Secure authentication managed by Composio
</div>
@ -446,7 +449,7 @@ export function ToolkitAuthModal({
{(toolkit.composio_managed_auth_schemes.includes('OAUTH2') ||
toolkit.auth_config_details?.some(config => config.mode === 'OAUTH2')) && (
<HeroButton
className="w-full justify-start gap-3 h-auto py-4 px-4"
className="w-full justify-start gap-3 h-auto py-5 px-5"
variant="bordered"
onPress={() => handleCustomAuth('OAUTH2')}
isDisabled={processing}
@ -456,7 +459,7 @@ export function ToolkitAuthModal({
<Shield className="h-5 w-5 text-orange-600 dark:text-orange-400" />
</div>
<div className="text-left">
<div className="font-medium">Connect using custom OAuth2 app</div>
<div className="font-medium text-base">Connect using custom OAuth2 app</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Use your own OAuth2 configuration
</div>
@ -468,7 +471,7 @@ export function ToolkitAuthModal({
{toolkit.auth_config_details?.filter(config => config.mode !== 'OAUTH2').map(config => (
<HeroButton
key={config.mode}
className="w-full justify-start gap-3 h-auto py-4 px-4"
className="w-full justify-start gap-3 h-auto py-5 px-5"
variant="bordered"
onPress={() => handleCustomAuth(config.mode)}
isDisabled={processing}
@ -478,7 +481,7 @@ export function ToolkitAuthModal({
{getAuthMethodIcon(config.mode)}
</div>
<div className="text-left">
<div className="font-medium">Connect using {getAuthMethodName(config.mode)}</div>
<div className="font-medium text-base">Connect using {getAuthMethodName(config.mode)}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Enter your credentials
</div>

View file

@ -9,17 +9,21 @@ import type { Key } from 'react';
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
import { z } from 'zod';
interface ToolsConfigProps {
projectId: string;
useComposioTools: boolean;
tools: z.infer<typeof Workflow.shape.tools>;
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
initialToolkitSlug?: string | null;
}
export function ToolsConfig({
projectId,
useComposioTools,
tools,
onAddTool,
}: {
projectId: string;
useComposioTools: boolean;
tools: z.infer<typeof Workflow.shape.tools>;
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
}) {
initialToolkitSlug
}: ToolsConfigProps) {
let defaultActiveTab = 'mcp';
if (useComposioTools) {
defaultActiveTab = 'composio';
@ -46,6 +50,7 @@ export function ToolsConfig({
projectId={projectId}
tools={tools}
onAddTool={onAddTool}
initialToolkitSlug={initialToolkitSlug}
/>
</div>
</Tab>

View file

@ -12,6 +12,7 @@ interface ToolsModalProps {
projectId: string;
tools: z.infer<typeof Workflow.shape.tools>;
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
initialToolkitSlug?: string | null;
}
export function ToolsModal({
@ -19,7 +20,8 @@ export function ToolsModal({
onClose,
projectId,
tools,
onAddTool
onAddTool,
initialToolkitSlug
}: ToolsModalProps) {
function handleAddTool(tool: Partial<z.infer<typeof WorkflowTool>>) {
onAddTool(tool);
@ -45,6 +47,7 @@ export function ToolsModal({
projectId={projectId}
tools={tools}
onAddTool={handleAddTool}
initialToolkitSlug={initialToolkitSlug}
/>
</ModalBody>
</ModalContent>

View file

@ -1,9 +1,10 @@
import React from "react";
import { z } from "zod";
import { WorkflowPrompt, WorkflowAgent, WorkflowTool, Workflow } from "../../../lib/types/workflow_types";
import { Project } from "../../../lib/types/project_types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react";
import { useRef, useEffect, useState } from "react";
import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, MoreVertical, Eye } from "lucide-react";
import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, MoreVertical, Eye, Trash2, AlertTriangle, Circle } from "lucide-react";
import { Tooltip } from "@heroui/react";
import { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
@ -105,7 +106,7 @@ const ListItemWithMenu = ({
}) => {
return (
<div className={clsx(
"group flex items-center gap-2 px-2 py-0.5 rounded-md min-h-[24px]",
"group flex items-center gap-2 px-3 py-2 rounded-md min-h-[24px]",
{
"bg-indigo-50 dark:bg-indigo-950/30": isSelected,
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !isSelected
@ -275,6 +276,8 @@ export function EntityList({
}) {
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
const [showToolsModal, setShowToolsModal] = useState(false);
// State to track which toolkit's tools panel to open
const [selectedToolkitSlug, setSelectedToolkitSlug] = useState<string | null>(null);
const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => {
onAddAgent({
@ -582,6 +585,8 @@ export function EntityList({
projectId={projectId}
workflow={workflow}
onProjectToolsUpdated={onProjectToolsUpdated}
setSelectedToolkitSlug={setSelectedToolkitSlug}
setShowToolsModal={setShowToolsModal}
/>
))}
@ -602,22 +607,28 @@ export function EntityList({
{customTools.length > 0 && (
<div className="mt-2">
{customTools.map((tool, index) => (
<ListItemWithMenu
<div
key={`custom-tool-${index}`}
name={tool.name}
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
className={clsx(
"flex items-center gap-2 px-3 py-2 rounded cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800",
selectedEntity?.type === "tool" && selectedEntity.name === tool.name && "bg-indigo-50 dark:bg-indigo-950/30"
)}
onClick={() => handleToolSelection(tool.name)}
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
icon={<Boxes className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />}
isMocked={tool.mockTool}
menuContent={
<EntityDropdown
name={tool.name}
onDelete={onDeleteTool}
isLocked={tool.isLibrary}
/>
}
/>
>
<Boxes className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />
<span className="flex-1 text-xs text-zinc-900 dark:text-zinc-100 whitespace-normal break-words">{tool.name}</span>
{tool.mockTool && (
<span className="ml-2 px-1 py-0 rounded bg-purple-50 text-purple-400 dark:bg-purple-900/40 dark:text-purple-200 text-[11px] font-normal align-middle">Mocked</span>
)}
<Tooltip content="Remove tool" size="sm" delay={500}>
<button
className="ml-1 p-1 pr-2 rounded hover:bg-red-100 dark:hover:bg-red-900 flex items-center"
onClick={e => { e.stopPropagation(); onDeleteTool(tool.name); }}
>
<Trash2 className="w-3 h-3 text-red-500" />
</button>
</Tooltip>
</div>
))}
</div>
)}
@ -722,10 +733,14 @@ export function EntityList({
/>
<ToolsModal
isOpen={showToolsModal}
onClose={() => setShowToolsModal(false)}
onClose={() => {
setShowToolsModal(false);
setSelectedToolkitSlug(null);
}}
projectId={projectId}
tools={tools}
onAddTool={onAddTool}
initialToolkitSlug={selectedToolkitSlug}
/>
</div>
);
@ -830,31 +845,48 @@ const ComposioCard = ({
projectId,
workflow,
onProjectToolsUpdated,
}: ComposioCardProps) => {
setSelectedToolkitSlug,
setShowToolsModal,
}: ComposioCardProps & { setSelectedToolkitSlug: (slug: string) => void, setShowToolsModal: (open: boolean) => void }) => {
const [isExpanded, setIsExpanded] = useState(false);
const [showAuthModal, setShowAuthModal] = useState(false);
const [showDisconnectModal, setShowDisconnectModal] = useState(false);
const [showRemoveToolkitModal, setShowRemoveToolkitModal] = useState(false);
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
const [isProcessingRemove, setIsProcessingRemove] = useState(false);
// Check if the toolkit requires authentication
const hasToolkitWithAuth = card.tools.some(tool => tool.composioData && !tool.composioData.noAuth);
// Check if toolkit is connected
const isToolkitConnected = !hasToolkitWithAuth || projectConfig?.composioConnectedAccounts?.[card.slug]?.status === 'ACTIVE';
const handleConnect = () => {
setShowAuthModal(true);
};
const handleDisconnect = () => {
setShowDisconnectModal(true);
// Remove all tools from this toolkit
const handleRemoveToolkit = async () => {
setIsProcessingRemove(true);
// Disconnect if needed
if (hasToolkitWithAuth && isToolkitConnected) {
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[card.slug]?.id;
try {
if (connectedAccountId) {
await deleteConnectedAccount(projectId, card.slug, connectedAccountId);
}
} catch (err) {
// ignore error, continue to remove tools
}
}
// Remove all tools from this toolkit
card.tools.forEach(tool => {
onDeleteTool(tool.name);
});
setIsProcessingRemove(false);
setShowRemoveToolkitModal(false);
onProjectToolsUpdated?.();
};
const handleConnect = () => setShowAuthModal(true);
const handleDisconnect = () => setShowDisconnectModal(true);
const handleConfirmDisconnect = async () => {
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[card.slug]?.id;
setIsProcessingAuth(true);
try {
if (connectedAccountId) {
@ -868,13 +900,105 @@ const ComposioCard = ({
setShowDisconnectModal(false);
}
};
const handleAuthComplete = () => {
setShowAuthModal(false);
onProjectToolsUpdated?.();
};
// Status dot
const statusDot = (
<Tooltip content={isToolkitConnected ? "Connected" : "Disconnected"} size="sm" delay={500}>
<Circle className={clsx(
"w-3 h-3",
isToolkitConnected ? "text-green-500" : "text-red-500"
)} fill="currentColor" />
</Tooltip>
);
let statusPill = null;
if (!isToolkitConnected && hasToolkitWithAuth) {
statusPill = (
<Tooltip content="Toolkit needs to be connected" size="sm" delay={500}>
<button
className="flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-full border border-yellow-300 bg-yellow-50 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-200 dark:border-yellow-700 transition-colors cursor-pointer"
onClick={handleConnect}
>
<AlertTriangle className="w-3 h-3 text-yellow-500" />
<span>Connect</span>
</button>
</Tooltip>
);
} else if (isToolkitConnected && hasToolkitWithAuth) {
statusPill = (
<span className="flex items-baseline gap-2 px-1.5 py-0 text-[11px] rounded-full border border-green-200 bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-200 dark:border-green-700">
<span className="flex items-center"><Circle className="w-2 h-2" fill="currentColor" /></span>
<span className="mt-[1px]">Connected</span>
</span>
);
}
// Always show the 3-dots menu for all toolkits
let toolkitMenu = null;
toolkitMenu = (
<div>
<Dropdown>
<DropdownTrigger>
<button className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors">
<MoreVertical className="w-4 h-4 text-gray-500" />
</button>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
switch (key) {
case 'disconnect':
handleDisconnect && handleDisconnect();
break;
case 'remove-toolkit':
setShowRemoveToolkitModal(true);
break;
case 'more-tools':
setSelectedToolkitSlug(card.slug);
setShowToolsModal(true);
break;
}
}}
disabledKeys={[
...(isProcessingAuth ? ['disconnect'] : []),
...(isProcessingRemove ? ['remove-toolkit'] : []),
]}
>
<DropdownItem
key="more-tools"
startContent={<PlusIcon className="h-3 w-3" />}
>
More tools
</DropdownItem>
{hasToolkitWithAuth && isToolkitConnected ? (
<DropdownItem
key="disconnect"
startContent={isProcessingAuth ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600"></div>
) : (
<UnlinkIcon className="h-3 w-3" />
)}
>
{isProcessingAuth ? 'Disconnecting...' : 'Disconnect'}
</DropdownItem>
) : null}
<DropdownItem
key="remove-toolkit"
startContent={isProcessingRemove ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-red-600"></div>
) : (
<Trash2 className="h-3 w-3" />
)}
>
{isProcessingRemove ? 'Removing...' : 'Remove Toolkit'}
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
return (
<>
@ -882,7 +1006,7 @@ const ComposioCard = ({
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px]"
className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px] py-1"
>
{/* Chevron - only show on hover or when has tools */}
<div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
@ -894,7 +1018,6 @@ const ComposioCard = ({
<ChevronRight className="w-3 h-3 text-gray-500" />
))}
</div>
<div className="flex items-center gap-2">
{card.logo ? (
<div className="relative w-4 h-4">
@ -907,111 +1030,72 @@ const ComposioCard = ({
) : (
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
)}
<span className="text-sm">{card.name}</span>
<span className="text-xs">{card.name}</span>
{statusPill && <span className="ml-2">{statusPill}</span>}
</div>
</button>
{/* Status Badge - only show orange when requires auth and not connected */}
{hasToolkitWithAuth && !isToolkitConnected && (
<Tooltip
content="Disconnected"
size="sm"
delay={500}
>
<div className="w-5 h-5 rounded-full bg-orange-500 flex items-center justify-center text-xs font-medium text-white">
</div>
</Tooltip>
)}
{/* Actions Dropdown - only show when requires auth */}
{hasToolkitWithAuth && (
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<Dropdown>
<DropdownTrigger>
<button className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors">
<MoreVertical className="h-3 w-3 text-gray-500" />
</button>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
switch (key) {
case 'connect':
handleConnect();
break;
case 'disconnect':
handleDisconnect();
break;
}
}}
disabledKeys={[
...(isProcessingAuth ? ['connect', 'disconnect'] : []),
...(hasToolkitWithAuth && isToolkitConnected ? [] : ['disconnect']),
...(hasToolkitWithAuth && !isToolkitConnected ? [] : ['connect'])
]}
>
<DropdownItem
key="disconnect"
startContent={
isProcessingAuth ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600"></div>
) : (
<UnlinkIcon className="h-3 w-3" />
)
}
>
{isProcessingAuth ? 'Disconnecting...' : 'Disconnect'}
</DropdownItem>
<DropdownItem
key="connect"
startContent={
isProcessingAuth ? (
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600"></div>
) : (
<LinkIcon className="h-3 w-3" />
)
}
>
{isProcessingAuth ? 'Connecting...' : 'Connect'}
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
)}
<div className="ml-2">{toolkitMenu}</div>
</div>
{isExpanded && (
<div className="ml-6 mt-0.5 space-y-0.5 border-l border-gray-200 dark:border-gray-700 pl-3">
{card.tools.map((tool, index) => (
<div key={`composio-tool-${index}`} className="group/tool">
<ListItemWithMenu
name={tool.name}
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
onClick={() => onSelectTool(tool.name)}
disabled={tool.isLibrary}
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
icon={
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600"></div>
}
isMocked={tool.mockTool}
menuContent={
<div className="opacity-0 group-hover/tool:opacity-100 transition-opacity">
<EntityDropdown
name={tool.name}
onDelete={onDeleteTool}
isLocked={tool.isComposio}
{isExpanded && (
<div className="ml-7 mt-0.5 space-y-0.5 border-l border-gray-200 dark:border-gray-700 pl-3">
{card.tools.map((tool, index) => (
<div
key={`composio-tool-${index}`}
className={clsx(
"group/tool flex items-center gap-2 px-3 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded",
selectedEntity?.type === "tool" && selectedEntity.name === tool.name && "bg-indigo-50 dark:bg-indigo-950/30"
)}
>
{/* Toolkit icon or fallback */}
{card.logo ? (
<div className="w-4 h-4 flex items-center justify-center">
<PictureImg
src={card.logo}
alt={`${card.name} logo`}
className="w-full h-full object-contain rounded"
/>
</div>
}
/>
</div>
))}
</div>
)}
) : (
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
)}
<button
className={clsx(
"flex-1 flex items-center gap-2 text-sm text-left bg-transparent border-none p-0 m-0",
tool.isLibrary ? "text-zinc-400 dark:text-zinc-600" : "text-zinc-900 dark:text-zinc-100"
)}
onClick={() => onSelectTool(tool.name)}
disabled={tool.isLibrary}
style={{ minWidth: 0 }}
>
<span className="whitespace-normal break-words text-xs">{tool.name}</span>
</button>
{tool.mockTool && (
<span className="ml-2 px-1 py-0 rounded bg-purple-50 text-purple-400 dark:bg-purple-900/40 dark:text-purple-200 text-[11px] font-normal align-middle">Mocked</span>
)}
<Tooltip content="Remove tool" size="sm" delay={500}>
<button
className="ml-1 p-1 pr-2 rounded hover:bg-red-100 dark:hover:bg-red-900 flex items-center"
onClick={() => onDeleteTool(tool.name)}
>
<Trash2 className="w-3 h-3 text-red-500" />
</button>
</Tooltip>
</div>
))}
{/* More tools option */}
<button
className="flex items-center gap-2 px-3 py-2 mt-1 text-xs text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-950/30 rounded transition-colors"
onClick={() => {
setSelectedToolkitSlug(card.slug);
setShowToolsModal(true);
}}
>
<PlusIcon className="w-4 h-4" />
<span>More tools</span>
</button>
</div>
)}
</div>
{/* Auth Modal */}
{hasToolkitWithAuth && (
<ToolkitAuthModal
@ -1023,7 +1107,6 @@ const ComposioCard = ({
onComplete={handleAuthComplete}
/>
)}
{/* Disconnect Confirmation Modal */}
<ProjectWideChangeConfirmationModal
isOpen={showDisconnectModal}
@ -1034,6 +1117,16 @@ const ComposioCard = ({
confirmButtonText="Disconnect"
isLoading={isProcessingAuth}
/>
{/* Remove Toolkit Confirmation Modal */}
<ProjectWideChangeConfirmationModal
isOpen={showRemoveToolkitModal}
onClose={() => setShowRemoveToolkitModal(false)}
onConfirm={handleRemoveToolkit}
title={`Remove ${card.name} Toolkit`}
confirmationQuestion={`Are you sure you want to remove the ${card.name} toolkit and all its tools? This will disconnect and delete all tools from this toolkit.`}
confirmButtonText="Remove Toolkit"
isLoading={isProcessingRemove}
/>
</>
);
};

View file

@ -9,6 +9,11 @@ interface SectionCardProps {
className?: string;
style?: React.CSSProperties;
chevronSize?: string;
/**
* If true, all fields are single column. If string[], only those fields are single column (by label).
* If not provided, all fields use the default two-column layout.
*/
singleColumnFields?: string[] | boolean;
}
export function SectionCard({ icon, title, children, labelWidth = 'md:w-32', className = '', style, chevronSize = 'w-4 h-4' }: SectionCardProps) {