refactor tools UX: part 1

This commit is contained in:
arkml 2025-07-16 20:13:03 +05:30 committed by Ramnique Singh
parent cccd383b92
commit 751a86c34d
21 changed files with 1462 additions and 258 deletions

View file

@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Info, RefreshCw, Search } from 'lucide-react';
import clsx from 'clsx';
import { listToolkits, listTools, updateComposioSelectedTools } from '@/app/actions/composio_actions';
import { listToolkits, listTools, updateComposioSelectedTools, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions';
import { getProjectConfig } from '@/app/actions/project_actions';
import { z } from 'zod';
import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio';
@ -30,6 +30,7 @@ export function Composio() {
const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null);
const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false);
const [savingTools, setSavingTools] = useState(false);
const [composioSelectedTools, setComposioSelectedTools] = useState<z.infer<typeof ZTool>[]>([]);
const loadProjectConfig = useCallback(async () => {
try {
@ -41,6 +42,15 @@ export function Composio() {
}
}, [projectId]);
const loadComposioSelectedTools = useCallback(async () => {
try {
const tools = await getComposioToolsFromWorkflow(projectId);
setComposioSelectedTools(tools);
} catch (err: any) {
console.error('Error fetching composio selected tools:', err);
}
}, [projectId]);
const loadAllToolkits = useCallback(async () => {
let cursor: string | null = null;
let allToolkits: ToolkitType[] = [];
@ -87,15 +97,16 @@ export function Composio() {
const handleProjectConfigUpdate = useCallback(() => {
loadProjectConfig();
}, [loadProjectConfig]);
loadComposioSelectedTools();
}, [loadProjectConfig, loadComposioSelectedTools]);
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
if (!projectId) return;
setSavingTools(true);
try {
// Get existing selected tools from project config
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
// Get existing selected tools from workflow
const existingSelectedTools = composioSelectedTools;
// Create a map of existing tools by slug for easy lookup
const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool]));
@ -110,22 +121,22 @@ export function Composio() {
await updateComposioSelectedTools(projectId, mergedSelectedTools);
// Refresh project config to get updated data
await loadProjectConfig();
// Refresh data to get updated tools
await loadComposioSelectedTools();
} catch (error) {
console.error('Error saving tool selection:', error);
} finally {
setSavingTools(false);
}
}, [projectId, projectConfig, loadProjectConfig]);
}, [projectId, composioSelectedTools, loadComposioSelectedTools]);
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
if (!projectId) return;
setSavingTools(true);
try {
// Get existing selected tools from project config
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
// Get existing selected tools from workflow
const existingSelectedTools = composioSelectedTools;
// Filter out all tools from the specified toolkit
const filteredSelectedTools = existingSelectedTools.filter(tool =>
@ -134,18 +145,19 @@ export function Composio() {
await updateComposioSelectedTools(projectId, filteredSelectedTools);
// Refresh project config to get updated data
await loadProjectConfig();
// Refresh data to get updated tools
await loadComposioSelectedTools();
} catch (error) {
console.error('Error removing toolkit tools:', error);
} finally {
setSavingTools(false);
}
}, [projectId, projectConfig, loadProjectConfig]);
}, [projectId, composioSelectedTools, loadComposioSelectedTools]);
useEffect(() => {
loadProjectConfig();
}, [loadProjectConfig]);
loadComposioSelectedTools();
}, [loadProjectConfig, loadComposioSelectedTools]);
useEffect(() => {
loadAllToolkits();

View file

@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
import { PictureImg } from '@/components/ui/picture-img';
import { Button, Checkbox } from '@heroui/react';
import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react';
import { listTools, deleteConnectedAccount } from '@/app/actions/composio_actions';
import { listTools, deleteConnectedAccount, getComposioToolsFromWorkflow } from '@/app/actions/composio_actions';
import { z } from 'zod';
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
import { SlidePanel } from '@/components/ui/slide-panel';
@ -57,6 +57,7 @@ export function ComposioToolsPanel({
const [hasChanges, setHasChanges] = useState(false);
const [showAuthModal, setShowAuthModal] = useState(false);
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
const [composioSelectedTools, setComposioSelectedTools] = useState<ToolType[]>([]);
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
try {
@ -80,6 +81,15 @@ export function ComposioToolsPanel({
}
}, [projectId]);
const loadComposioSelectedTools = useCallback(async () => {
try {
const tools = await getComposioToolsFromWorkflow(projectId);
setComposioSelectedTools(tools);
} catch (err: any) {
console.error('Error fetching composio selected tools:', err);
}
}, [projectId]);
const handleNextPage = useCallback(async () => {
if (!nextCursor || !toolkit) return;
@ -164,14 +174,21 @@ export function ComposioToolsPanel({
}
}, [onClose, hasChanges]);
// Initialize selected tools from project config when opening the panel
// Initialize selected tools from workflow when opening the panel
useEffect(() => {
if (toolkit && isOpen && projectConfig?.composioSelectedTools) {
const toolSlugs = new Set(projectConfig.composioSelectedTools.map(tool => tool.slug));
if (toolkit && isOpen) {
loadComposioSelectedTools();
}
}, [toolkit, isOpen, loadComposioSelectedTools]);
// Set selected tools when composioSelectedTools is loaded
useEffect(() => {
if (toolkit && composioSelectedTools.length > 0) {
const toolSlugs = new Set(composioSelectedTools.map(tool => tool.slug));
setSelectedTools(toolSlugs);
setHasChanges(false);
}
}, [toolkit, isOpen, projectConfig]);
}, [toolkit, composioSelectedTools]);
useEffect(() => {
if (toolkit && isOpen) {
@ -210,44 +227,46 @@ export function ComposioToolsPanel({
<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'
: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-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'
isToolkitConnected ? 'bg-emerald-500' : 'bg-blue-500'
}`}></div>
<div>
<h3 className={`font-semibold text-sm ${
isToolkitConnected
? 'text-emerald-800 dark:text-emerald-200'
: 'text-orange-800 dark:text-orange-200'
: 'text-blue-800 dark:text-blue-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'
: 'text-blue-700 dark:text-blue-300'
}`}>
{isToolkitConnected
? 'You can select and use tools from this toolkit'
: 'Connect your account to access and use tools'
: 'You can select tools now. Authentication will be required in the build view to use them.'
}
</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>
{isToolkitConnected && (
<Button
variant="solid"
size="sm"
onPress={handleDisconnect}
disabled={isProcessingAuth}
color="danger"
isLoading={isProcessingAuth}
startContent={<UnlinkIcon className="h-4 w-4" />}
>
Disconnect
</Button>
)}
</div>
</div>
)}
@ -263,7 +282,7 @@ export function ComposioToolsPanel({
size="sm"
color="primary"
onPress={handleSaveTools}
disabled={isSaving || !isToolkitConnected}
disabled={isSaving}
isLoading={isSaving}
>
Save Changes
@ -283,17 +302,12 @@ export function ComposioToolsPanel({
) : (
<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 key={tool.slug} className="group p-4 rounded-lg transition-all duration-200 border border-transparent 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 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">

View file

@ -24,7 +24,7 @@ import { Modal } from '@/components/ui/modal';
type McpServerType = z.infer<typeof MCPServer>;
type McpToolType = z.infer<typeof MCPServer>['tools'][number];
export function CustomServers() {
export function CustomServers({ onToolsUpdated }: { onToolsUpdated?: () => void }) {
const params = useParams();
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
if (!projectId) throw new Error('Project ID is required');
@ -92,6 +92,9 @@ export function CustomServers() {
return s;
});
});
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (err) {
console.error('Toggle failed:', { server: server.name, error: err });
} finally {
@ -161,6 +164,9 @@ export function CustomServers() {
// Update selectedTools to include all tools for the custom server
setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id)));
}
// Notify parent component about tool updates
onToolsUpdated?.();
} finally {
setSyncingServers(prev => {
const next = new Set(prev);
@ -207,6 +213,9 @@ export function CustomServers() {
// Fetch tools for the new server using the formatted URL
await handleSyncServer(formattedServer);
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (err) {
console.error('Error adding server:', err);
setError('Failed to add server. Please try again.');
@ -229,6 +238,9 @@ export function CustomServers() {
if (selectedServer?.name === server.name) {
setSelectedServer(null);
}
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (err) {
console.error('Error removing server:', err);
setError('Failed to remove server. Please try again.');
@ -271,6 +283,9 @@ export function CustomServers() {
});
setHasToolChanges(false);
// Notify parent component about tool updates
onToolsUpdated?.();
} catch (error) {
console.error('Error saving tool selection:', error);
} finally {

View file

@ -52,9 +52,8 @@ export function ToolkitCard({
}, [onManageTools]);
// Calculate selected tools count for this toolkit
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
tool.toolkit.slug === toolkit.slug
).length || 0;
// TODO: Update to use workflow-based tools count
const selectedToolsCount = 0;
return (
<div className={toolkitCardStyles.base} onClick={handleCardClick}>