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.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) {
|
||||
|
|
@ -191,14 +201,6 @@ export function Composio() {
|
|||
|
||||
return (
|
||||
<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 items-center justify-between gap-4">
|
||||
<div className="flex-1 flex items-center gap-4">
|
||||
|
|
@ -276,6 +278,8 @@ export function Composio() {
|
|||
onClose={handleCloseToolsPanel}
|
||||
projectConfig={projectConfig}
|
||||
onUpdateToolsSelection={handleUpdateToolsSelection}
|
||||
onProjectConfigUpdate={handleProjectConfigUpdate}
|
||||
onRemoveToolkitTools={handleRemoveToolkitTools}
|
||||
isSaving={savingTools}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PictureImg } from '@/components/ui/picture-img';
|
||||
import { Checkbox } from '@heroui/react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { listTools } from '@/app/actions/composio_actions';
|
||||
import { Button, Checkbox } from '@heroui/react';
|
||||
import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react';
|
||||
import { listTools, deleteConnectedAccount } from '@/app/actions/composio_actions';
|
||||
import { z } from 'zod';
|
||||
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
||||
import { SlidePanel } from '@/components/ui/slide-panel';
|
||||
import { Project } from '@/app/lib/types/project_types';
|
||||
import { ToolkitAuthModal } from './ToolkitAuthModal';
|
||||
|
||||
type ToolType = z.infer<typeof ZTool>;
|
||||
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
|
||||
|
|
@ -29,6 +29,8 @@ interface ComposioToolsPanelProps {
|
|||
onClose: () => void;
|
||||
projectConfig: ProjectType | null;
|
||||
onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void;
|
||||
onProjectConfigUpdate: () => void;
|
||||
onRemoveToolkitTools: (toolkitSlug: string) => void;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +40,8 @@ export function ComposioToolsPanel({
|
|||
onClose,
|
||||
projectConfig,
|
||||
onUpdateToolsSelection,
|
||||
onProjectConfigUpdate,
|
||||
onRemoveToolkitTools,
|
||||
isSaving
|
||||
}: ComposioToolsPanelProps) {
|
||||
const params = useParams();
|
||||
|
|
@ -51,6 +55,8 @@ export function ComposioToolsPanel({
|
|||
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
||||
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
||||
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) => {
|
||||
try {
|
||||
|
|
@ -117,6 +123,34 @@ export function ComposioToolsPanel({
|
|||
setHasChanges(false);
|
||||
}, [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(() => {
|
||||
setTools([]);
|
||||
setSelectedTools(new Set());
|
||||
|
|
@ -151,119 +185,170 @@ export function ComposioToolsPanel({
|
|||
const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||
|
||||
return (
|
||||
<SlidePanel
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
{toolkit.meta.logo && (
|
||||
<PictureImg
|
||||
src={toolkit.meta.logo}
|
||||
alt={`${toolkit.name} logo`}
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
)}
|
||||
<span>{toolkit.name}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<>
|
||||
<SlidePanel
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
{toolkit.meta.logo && (
|
||||
<PictureImg
|
||||
src={toolkit.meta.logo}
|
||||
alt={`${toolkit.name} logo`}
|
||||
width={24}
|
||||
height={24}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
)}
|
||||
<span>{toolkit.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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 className="flex flex-col h-full">
|
||||
{/* Connection Status Banner */}
|
||||
{!toolkit.no_auth && (
|
||||
<div className={`mb-6 p-4 rounded-lg border-2 ${
|
||||
isToolkitConnected
|
||||
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'
|
||||
: 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
isToolkitConnected ? 'bg-emerald-500' : 'bg-orange-500'
|
||||
}`}></div>
|
||||
<div>
|
||||
<h3 className={`font-semibold text-sm ${
|
||||
isToolkitConnected
|
||||
? 'text-emerald-800 dark:text-emerald-200'
|
||||
: 'text-orange-800 dark:text-orange-200'
|
||||
}`}>
|
||||
{isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'}
|
||||
</h3>
|
||||
<p className={`text-xs mt-0.5 ${
|
||||
isToolkitConnected
|
||||
? 'text-emerald-700 dark:text-emerald-300'
|
||||
: 'text-orange-700 dark:text-orange-300'
|
||||
}`}>
|
||||
{isToolkitConnected
|
||||
? 'You can select and use tools from this toolkit'
|
||||
: 'Connect your account to access and use tools'
|
||||
}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
{/* 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="secondary"
|
||||
size="sm"
|
||||
onClick={handlePreviousPage}
|
||||
disabled={cursorHistory.length === 0 || toolsLoading}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleNextPage}
|
||||
disabled={!nextCursor || toolsLoading}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
{/* 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">
|
||||
{hasChanges && (
|
||||
<Button
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="primary"
|
||||
onPress={handleSaveTools}
|
||||
disabled={isSaving || !isToolkitConnected}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
</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';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useCallback } from 'react';
|
||||
import { PictureImg } from '@/components/ui/picture-img';
|
||||
import { Wrench } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { Spinner } from '@heroui/react';
|
||||
import { deleteConnectedAccount } from '@/app/actions/composio_actions';
|
||||
import { z } from 'zod';
|
||||
import { ZToolkit } from '@/app/lib/composio/composio';
|
||||
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 ProjectType = z.infer<typeof Project>;
|
||||
|
||||
const toolkitCardStyles = {
|
||||
base: clsx(
|
||||
"group p-6 rounded-xl transition-all duration-200",
|
||||
"bg-white dark:bg-gray-900 shadow-sm dark:shadow-none",
|
||||
"border-2 border-gray-200/80 dark:border-gray-700/80",
|
||||
"hover:shadow-md dark:hover:shadow-none",
|
||||
"hover:border-blue-200 dark:hover:border-blue-900",
|
||||
"min-h-[280px] flex flex-col"
|
||||
"group p-6 rounded-xl transition-all duration-200 cursor-pointer",
|
||||
"bg-white dark:bg-gray-900",
|
||||
"border border-gray-200 dark:border-gray-700",
|
||||
"shadow-md dark:shadow-gray-900/20",
|
||||
"hover:shadow-lg dark:hover:shadow-gray-900/30",
|
||||
"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,
|
||||
onRemoveToolkitTools
|
||||
}: ToolkitCardProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
|
||||
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]);
|
||||
const handleCardClick = useCallback(() => {
|
||||
onManageTools();
|
||||
}, [onManageTools]);
|
||||
|
||||
// Calculate selected tools count for this toolkit
|
||||
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
|
||||
|
|
@ -94,127 +57,70 @@ export function ToolkitCard({
|
|||
).length || 0;
|
||||
|
||||
return (
|
||||
<div className={toolkitCardStyles.base}>
|
||||
<div className={toolkitCardStyles.base} onClick={handleCardClick}>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{toolkit.meta.logo && (
|
||||
<PictureImg
|
||||
src={toolkit.meta.logo}
|
||||
alt={`${toolkit.name} logo`}
|
||||
className="w-8 h-8 rounded-md object-cover"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100">
|
||||
{toolkit.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
||||
bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
|
||||
{toolkit.meta.tools_count} tools
|
||||
</span>
|
||||
{selectedToolsCount > 0 && (
|
||||
<span className="px-1.5 py-0.5 rounded-full text-xs font-medium
|
||||
bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300">
|
||||
{selectedToolsCount} selected
|
||||
</span>
|
||||
)}
|
||||
{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>
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
{toolkit.meta.logo && (
|
||||
<PictureImg
|
||||
src={toolkit.meta.logo}
|
||||
alt={`${toolkit.name} logo`}
|
||||
className="w-8 h-8 rounded-md object-cover flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100 truncate">
|
||||
{toolkit.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
<Chip
|
||||
color="secondary"
|
||||
variant="faded"
|
||||
size="sm"
|
||||
>
|
||||
{selectedToolsCount > 0
|
||||
? `${toolkit.meta.tools_count} tools, ${selectedToolsCount} selected`
|
||||
: `${toolkit.meta.tools_count} tools`
|
||||
}
|
||||
</Chip>
|
||||
</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>
|
||||
|
||||
{/* Description */}
|
||||
<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}
|
||||
</p>
|
||||
</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="text-xs text-gray-400 dark:text-gray-500">
|
||||
ID: {toolkit.slug}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isProcessing && (
|
||||
<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">
|
||||
<Spinner size="sm" />
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
{isConnected && !toolkit.no_auth && (
|
||||
<Chip
|
||||
color='success'
|
||||
variant='flat'
|
||||
size="sm"
|
||||
startContent={<LinkIcon className="w-3 h-3 mr-1" />}
|
||||
>
|
||||
Connected
|
||||
</Chip>
|
||||
)}
|
||||
{(isConnected || toolkit.no_auth) && !isProcessing && (
|
||||
<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">
|
||||
{toolkit.no_auth ? 'Available' : 'Connected'}
|
||||
</div>
|
||||
{toolkit.no_auth && (
|
||||
<Chip
|
||||
color='success'
|
||||
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>
|
||||
|
||||
<ToolkitAuthModal
|
||||
key={toolkit.slug}
|
||||
isOpen={showAuthModal}
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
toolkitSlug={toolkit.slug}
|
||||
projectId={projectId}
|
||||
onComplete={handleAuthComplete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue