mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
improve composio tools ux
This commit is contained in:
parent
2ee4f37464
commit
3063c9fea9
3 changed files with 268 additions and 273 deletions
|
|
@ -158,6 +158,16 @@ export function Composio() {
|
||||||
toolkit.meta.description.toLowerCase().includes(searchLower) ||
|
toolkit.meta.description.toLowerCase().includes(searchLower) ||
|
||||||
toolkit.slug.toLowerCase().includes(searchLower)
|
toolkit.slug.toLowerCase().includes(searchLower)
|
||||||
);
|
);
|
||||||
|
}).sort((a, b) => {
|
||||||
|
// Sort by actual connection status first (only connected tools, not no-auth)
|
||||||
|
const aConnected = !a.no_auth && projectConfig?.composioConnectedAccounts?.[a.slug]?.status === 'ACTIVE';
|
||||||
|
const bConnected = !b.no_auth && projectConfig?.composioConnectedAccounts?.[b.slug]?.status === 'ACTIVE';
|
||||||
|
|
||||||
|
if (aConnected && !bConnected) return -1;
|
||||||
|
if (!aConnected && bConnected) return 1;
|
||||||
|
|
||||||
|
// If both have same connection status, maintain original order (don't sort alphabetically)
|
||||||
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -191,14 +201,6 @@ export function Composio() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex-1 flex items-center gap-4">
|
<div className="flex-1 flex items-center gap-4">
|
||||||
|
|
@ -276,6 +278,8 @@ export function Composio() {
|
||||||
onClose={handleCloseToolsPanel}
|
onClose={handleCloseToolsPanel}
|
||||||
projectConfig={projectConfig}
|
projectConfig={projectConfig}
|
||||||
onUpdateToolsSelection={handleUpdateToolsSelection}
|
onUpdateToolsSelection={handleUpdateToolsSelection}
|
||||||
|
onProjectConfigUpdate={handleProjectConfigUpdate}
|
||||||
|
onRemoveToolkitTools={handleRemoveToolkitTools}
|
||||||
isSaving={savingTools}
|
isSaving={savingTools}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { PictureImg } from '@/components/ui/picture-img';
|
import { PictureImg } from '@/components/ui/picture-img';
|
||||||
import { Checkbox } from '@heroui/react';
|
import { Button, Checkbox } from '@heroui/react';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react';
|
||||||
import { listTools } from '@/app/actions/composio_actions';
|
import { listTools, deleteConnectedAccount } from '@/app/actions/composio_actions';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
||||||
import { SlidePanel } from '@/components/ui/slide-panel';
|
import { SlidePanel } from '@/components/ui/slide-panel';
|
||||||
import { Project } from '@/app/lib/types/project_types';
|
import { Project } from '@/app/lib/types/project_types';
|
||||||
|
import { ToolkitAuthModal } from './ToolkitAuthModal';
|
||||||
|
|
||||||
type ToolType = z.infer<typeof ZTool>;
|
type ToolType = z.infer<typeof ZTool>;
|
||||||
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
|
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
|
||||||
|
|
@ -29,6 +29,8 @@ interface ComposioToolsPanelProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
projectConfig: ProjectType | null;
|
projectConfig: ProjectType | null;
|
||||||
onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void;
|
onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void;
|
||||||
|
onProjectConfigUpdate: () => void;
|
||||||
|
onRemoveToolkitTools: (toolkitSlug: string) => void;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +40,8 @@ export function ComposioToolsPanel({
|
||||||
onClose,
|
onClose,
|
||||||
projectConfig,
|
projectConfig,
|
||||||
onUpdateToolsSelection,
|
onUpdateToolsSelection,
|
||||||
|
onProjectConfigUpdate,
|
||||||
|
onRemoveToolkitTools,
|
||||||
isSaving
|
isSaving
|
||||||
}: ComposioToolsPanelProps) {
|
}: ComposioToolsPanelProps) {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -51,6 +55,8 @@ export function ComposioToolsPanel({
|
||||||
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
||||||
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||||
|
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
|
||||||
|
|
||||||
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
|
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -117,6 +123,34 @@ export function ComposioToolsPanel({
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
}, [onUpdateToolsSelection, selectedTools, tools]);
|
}, [onUpdateToolsSelection, selectedTools, tools]);
|
||||||
|
|
||||||
|
const handleConnect = useCallback(() => {
|
||||||
|
setShowAuthModal(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDisconnect = useCallback(async () => {
|
||||||
|
if (!toolkit) return;
|
||||||
|
|
||||||
|
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
|
||||||
|
|
||||||
|
setIsProcessingAuth(true);
|
||||||
|
try {
|
||||||
|
if (connectedAccountId) {
|
||||||
|
await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId);
|
||||||
|
onProjectConfigUpdate();
|
||||||
|
onRemoveToolkitTools(toolkit.slug);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Disconnect failed:', err);
|
||||||
|
} finally {
|
||||||
|
setIsProcessingAuth(false);
|
||||||
|
}
|
||||||
|
}, [projectId, toolkit, projectConfig, onProjectConfigUpdate, onRemoveToolkitTools]);
|
||||||
|
|
||||||
|
const handleAuthComplete = useCallback(() => {
|
||||||
|
setShowAuthModal(false);
|
||||||
|
onProjectConfigUpdate();
|
||||||
|
}, [onProjectConfigUpdate]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setTools([]);
|
setTools([]);
|
||||||
setSelectedTools(new Set());
|
setSelectedTools(new Set());
|
||||||
|
|
@ -151,119 +185,170 @@ export function ComposioToolsPanel({
|
||||||
const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SlidePanel
|
<>
|
||||||
isOpen={isOpen}
|
<SlidePanel
|
||||||
onClose={handleClose}
|
isOpen={isOpen}
|
||||||
title={
|
onClose={handleClose}
|
||||||
<div className="flex items-center gap-3">
|
title={
|
||||||
{toolkit.meta.logo && (
|
<div className="flex items-center gap-3">
|
||||||
<PictureImg
|
{toolkit.meta.logo && (
|
||||||
src={toolkit.meta.logo}
|
<PictureImg
|
||||||
alt={`${toolkit.name} logo`}
|
src={toolkit.meta.logo}
|
||||||
width={24}
|
alt={`${toolkit.name} logo`}
|
||||||
height={24}
|
width={24}
|
||||||
className="rounded-md object-cover"
|
height={24}
|
||||||
/>
|
className="rounded-md object-cover"
|
||||||
)}
|
/>
|
||||||
<span>{toolkit.name}</span>
|
)}
|
||||||
</div>
|
<span>{toolkit.name}</span>
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Available Tools</h4>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{!isToolkitConnected && !toolkit.no_auth && (
|
|
||||||
<div className="text-sm text-orange-600 dark:text-orange-400 px-3 py-1 rounded-full bg-orange-50 dark:bg-orange-900/20">
|
|
||||||
Toolkit not connected
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasChanges && (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSaveTools}
|
|
||||||
disabled={isSaving || !isToolkitConnected}
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<>
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-b-transparent border-white mr-2" />
|
|
||||||
Saving...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Save Changes'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
{/* Scrollable Tools List */}
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 overflow-y-auto">
|
{/* Connection Status Banner */}
|
||||||
{toolsLoading ? (
|
{!toolkit.no_auth && (
|
||||||
<div className="text-center py-8">
|
<div className={`mb-6 p-4 rounded-lg border-2 ${
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
|
isToolkitConnected
|
||||||
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
|
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'
|
||||||
</div>
|
: 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800'
|
||||||
) : (
|
}`}>
|
||||||
<div className="space-y-4">
|
<div className="flex items-center justify-between">
|
||||||
{tools.map((tool) => (
|
<div className="flex items-center gap-3">
|
||||||
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${
|
<div className={`w-3 h-3 rounded-full ${
|
||||||
isToolkitConnected
|
isToolkitConnected ? 'bg-emerald-500' : 'bg-orange-500'
|
||||||
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600'
|
}`}></div>
|
||||||
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60'
|
<div>
|
||||||
}`}>
|
<h3 className={`font-semibold text-sm ${
|
||||||
<div className="flex items-start gap-3">
|
isToolkitConnected
|
||||||
<Checkbox
|
? 'text-emerald-800 dark:text-emerald-200'
|
||||||
isSelected={selectedTools.has(tool.slug)}
|
: 'text-orange-800 dark:text-orange-200'
|
||||||
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
|
}`}>
|
||||||
size="sm"
|
{isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'}
|
||||||
isDisabled={!isToolkitConnected}
|
</h3>
|
||||||
/>
|
<p className={`text-xs mt-0.5 ${
|
||||||
<div className="flex-1">
|
isToolkitConnected
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
|
? 'text-emerald-700 dark:text-emerald-300'
|
||||||
{tool.name}
|
: 'text-orange-700 dark:text-orange-300'
|
||||||
</h4>
|
}`}>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
{isToolkitConnected
|
||||||
{tool.description}
|
? 'You can select and use tools from this toolkit'
|
||||||
</p>
|
: 'Connect your account to access and use tools'
|
||||||
</div>
|
}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
onPress={isToolkitConnected ? handleDisconnect : handleConnect}
|
||||||
|
disabled={isProcessingAuth}
|
||||||
|
color={isToolkitConnected ? "danger" : "primary"}
|
||||||
|
isLoading={isProcessingAuth}
|
||||||
|
startContent={isToolkitConnected ? <UnlinkIcon className="h-4 w-4" /> : <LinkIcon className="h-4 w-4" />}
|
||||||
|
>
|
||||||
|
{isToolkitConnected ? 'Disconnect' : 'Connect Now'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Fixed Pagination Controls */}
|
{/* Header */}
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Available Tools</h4>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="secondary"
|
{hasChanges && (
|
||||||
size="sm"
|
<Button
|
||||||
onClick={handlePreviousPage}
|
variant="solid"
|
||||||
disabled={cursorHistory.length === 0 || toolsLoading}
|
size="sm"
|
||||||
>
|
color="primary"
|
||||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
onPress={handleSaveTools}
|
||||||
Previous
|
disabled={isSaving || !isToolkitConnected}
|
||||||
</Button>
|
isLoading={isSaving}
|
||||||
<Button
|
>
|
||||||
variant="secondary"
|
Save Changes
|
||||||
size="sm"
|
</Button>
|
||||||
onClick={handleNextPage}
|
)}
|
||||||
disabled={!nextCursor || toolsLoading}
|
</div>
|
||||||
>
|
</div>
|
||||||
Next
|
</div>
|
||||||
<ChevronRight className="h-4 w-4 ml-1" />
|
|
||||||
</Button>
|
{/* Scrollable Tools List */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{toolsLoading ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${
|
||||||
|
isToolkitConnected
|
||||||
|
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600'
|
||||||
|
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
isSelected={selectedTools.has(tool.slug)}
|
||||||
|
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
|
||||||
|
size="sm"
|
||||||
|
isDisabled={!isToolkitConnected}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
|
||||||
|
{tool.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fixed Pagination Controls */}
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="sm"
|
||||||
|
onClick={handlePreviousPage}
|
||||||
|
disabled={cursorHistory.length === 0 || toolsLoading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleNextPage}
|
||||||
|
disabled={!nextCursor || toolsLoading}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SlidePanel>
|
||||||
</SlidePanel>
|
|
||||||
|
{/* Auth Modal */}
|
||||||
|
{toolkit && (
|
||||||
|
<ToolkitAuthModal
|
||||||
|
key={toolkit.slug}
|
||||||
|
isOpen={showAuthModal}
|
||||||
|
onClose={() => setShowAuthModal(false)}
|
||||||
|
toolkitSlug={toolkit.slug}
|
||||||
|
projectId={projectId}
|
||||||
|
onComplete={handleAuthComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
|
||||||
import { PictureImg } from '@/components/ui/picture-img';
|
import { PictureImg } from '@/components/ui/picture-img';
|
||||||
import { Wrench } from 'lucide-react';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Spinner } from '@heroui/react';
|
|
||||||
import { deleteConnectedAccount } from '@/app/actions/composio_actions';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ZToolkit } from '@/app/lib/composio/composio';
|
import { ZToolkit } from '@/app/lib/composio/composio';
|
||||||
import { Project } from '@/app/lib/types/project_types';
|
import { Project } from '@/app/lib/types/project_types';
|
||||||
import { ToolkitAuthModal } from './ToolkitAuthModal';
|
import { Chip } from '@heroui/react';
|
||||||
|
import { LinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
type ToolkitType = z.infer<typeof ZToolkit>;
|
type ToolkitType = z.infer<typeof ZToolkit>;
|
||||||
type ProjectType = z.infer<typeof Project>;
|
type ProjectType = z.infer<typeof Project>;
|
||||||
|
|
||||||
const toolkitCardStyles = {
|
const toolkitCardStyles = {
|
||||||
base: clsx(
|
base: clsx(
|
||||||
"group p-6 rounded-xl transition-all duration-200",
|
"group p-6 rounded-xl transition-all duration-200 cursor-pointer",
|
||||||
"bg-white dark:bg-gray-900 shadow-sm dark:shadow-none",
|
"bg-white dark:bg-gray-900",
|
||||||
"border-2 border-gray-200/80 dark:border-gray-700/80",
|
"border border-gray-200 dark:border-gray-700",
|
||||||
"hover:shadow-md dark:hover:shadow-none",
|
"shadow-md dark:shadow-gray-900/20",
|
||||||
"hover:border-blue-200 dark:hover:border-blue-900",
|
"hover:shadow-lg dark:hover:shadow-gray-900/30",
|
||||||
"min-h-[280px] flex flex-col"
|
"hover:border-blue-300 dark:hover:border-blue-600",
|
||||||
|
"hover:bg-gray-50/50 dark:hover:bg-gray-800/50",
|
||||||
|
"hover:-translate-y-1",
|
||||||
|
"min-h-[200px] flex flex-col"
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,45 +47,9 @@ export function ToolkitCard({
|
||||||
onProjectConfigUpdate,
|
onProjectConfigUpdate,
|
||||||
onRemoveToolkitTools
|
onRemoveToolkitTools
|
||||||
}: ToolkitCardProps) {
|
}: ToolkitCardProps) {
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const handleCardClick = useCallback(() => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
onManageTools();
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
}, [onManageTools]);
|
||||||
|
|
||||||
const handleToggleConnection = useCallback(async () => {
|
|
||||||
const newState = !isConnected;
|
|
||||||
|
|
||||||
// Clear any previous error when starting a new operation
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
if (newState) {
|
|
||||||
// Show authentication modal
|
|
||||||
setShowAuthModal(true);
|
|
||||||
} else {
|
|
||||||
// Disconnect - remove the connected account
|
|
||||||
setIsProcessing(true);
|
|
||||||
try {
|
|
||||||
if (connectedAccountId) {
|
|
||||||
await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId);
|
|
||||||
onProjectConfigUpdate();
|
|
||||||
onRemoveToolkitTools(toolkit.slug);
|
|
||||||
} else {
|
|
||||||
// Fallback: just refresh the project config
|
|
||||||
onProjectConfigUpdate();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Disconnect failed:', err);
|
|
||||||
const errorMessage = err.message || 'Failed to disconnect toolkit';
|
|
||||||
setError(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [projectId, toolkit.slug, isConnected, connectedAccountId, onProjectConfigUpdate, onRemoveToolkitTools]);
|
|
||||||
|
|
||||||
const handleAuthComplete = useCallback(() => {
|
|
||||||
// Update project config when authentication completes
|
|
||||||
onProjectConfigUpdate();
|
|
||||||
}, [onProjectConfigUpdate]);
|
|
||||||
|
|
||||||
// Calculate selected tools count for this toolkit
|
// Calculate selected tools count for this toolkit
|
||||||
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
|
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
|
||||||
|
|
@ -94,127 +57,70 @@ export function ToolkitCard({
|
||||||
).length || 0;
|
).length || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={toolkitCardStyles.base}>
|
<div className={toolkitCardStyles.base} onClick={handleCardClick}>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-start justify-between mb-4">
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-start gap-3 mb-4">
|
||||||
{toolkit.meta.logo && (
|
{toolkit.meta.logo && (
|
||||||
<PictureImg
|
<PictureImg
|
||||||
src={toolkit.meta.logo}
|
src={toolkit.meta.logo}
|
||||||
alt={`${toolkit.name} logo`}
|
alt={`${toolkit.name} logo`}
|
||||||
className="w-8 h-8 rounded-md object-cover"
|
className="w-8 h-8 rounded-md object-cover flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100">
|
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100 truncate">
|
||||||
{toolkit.name}
|
{toolkit.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
<Chip
|
||||||
bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
|
color="secondary"
|
||||||
{toolkit.meta.tools_count} tools
|
variant="faded"
|
||||||
</span>
|
size="sm"
|
||||||
{selectedToolsCount > 0 && (
|
>
|
||||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
{selectedToolsCount > 0
|
||||||
bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300">
|
? `${toolkit.meta.tools_count} tools, ${selectedToolsCount} selected`
|
||||||
{selectedToolsCount} selected
|
: `${toolkit.meta.tools_count} tools`
|
||||||
</span>
|
}
|
||||||
)}
|
</Chip>
|
||||||
{toolkit.no_auth && (
|
|
||||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
|
||||||
bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300">
|
|
||||||
No Auth
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{toolkit.no_auth ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={true}
|
|
||||||
onCheckedChange={() => {}} // No-op for no-auth toolkits
|
|
||||||
disabled={true}
|
|
||||||
className={clsx(
|
|
||||||
"data-[state=checked]:bg-emerald-500 dark:data-[state=checked]:bg-emerald-600",
|
|
||||||
"data-[state=unchecked]:bg-emerald-500 dark:data-[state=unchecked]:bg-emerald-600",
|
|
||||||
"opacity-50 cursor-not-allowed",
|
|
||||||
"scale-75"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
|
|
||||||
Always Available
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Switch
|
|
||||||
checked={isConnected}
|
|
||||||
onCheckedChange={handleToggleConnection}
|
|
||||||
disabled={isProcessing}
|
|
||||||
className={clsx(
|
|
||||||
"data-[state=checked]:bg-blue-500 dark:data-[state=checked]:bg-blue-600",
|
|
||||||
"data-[state=unchecked]:bg-gray-200 dark:data-[state=unchecked]:bg-gray-700",
|
|
||||||
isProcessing && "opacity-50 cursor-not-allowed",
|
|
||||||
"scale-75"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 line-clamp-3">
|
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-3">
|
||||||
{toolkit.meta.description}
|
{toolkit.meta.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto">
|
{/* Footer */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
|
||||||
ID: {toolkit.slug}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isProcessing && (
|
{isConnected && !toolkit.no_auth && (
|
||||||
<div className="flex items-center gap-1 text-xs py-1 px-2 rounded-full text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20">
|
<Chip
|
||||||
<Spinner size="sm" />
|
color='success'
|
||||||
<span>Processing...</span>
|
variant='flat'
|
||||||
</div>
|
size="sm"
|
||||||
|
startContent={<LinkIcon className="w-3 h-3 mr-1" />}
|
||||||
|
>
|
||||||
|
Connected
|
||||||
|
</Chip>
|
||||||
)}
|
)}
|
||||||
{(isConnected || toolkit.no_auth) && !isProcessing && (
|
{toolkit.no_auth && (
|
||||||
<div className="text-xs py-1 px-2 rounded-full text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-900/20">
|
<Chip
|
||||||
{toolkit.no_auth ? 'Available' : 'Connected'}
|
color='success'
|
||||||
</div>
|
variant='flat'
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Ready
|
||||||
|
</Chip>
|
||||||
)}
|
)}
|
||||||
{error && (
|
|
||||||
<div className="text-xs py-1 px-2 rounded-full text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20">
|
|
||||||
Error: {error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={onManageTools}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center">
|
|
||||||
<Wrench className="h-3.5 w-3.5" />
|
|
||||||
<span className="ml-1.5">Tools</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolkitAuthModal
|
|
||||||
key={toolkit.slug}
|
|
||||||
isOpen={showAuthModal}
|
|
||||||
onClose={() => setShowAuthModal(false)}
|
|
||||||
toolkitSlug={toolkit.slug}
|
|
||||||
projectId={projectId}
|
|
||||||
onComplete={handleAuthComplete}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue