mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-08 14:52:38 +02:00
refactor tools UX: part 1
This commit is contained in:
parent
cccd383b92
commit
751a86c34d
21 changed files with 1462 additions and 258 deletions
|
|
@ -3,6 +3,9 @@ import { z } from "zod";
|
|||
import {
|
||||
listToolkits as libListToolkits,
|
||||
listTools as libListTools,
|
||||
searchTools as libSearchTools,
|
||||
getToolsByIds as libGetToolsByIds,
|
||||
getTool as libGetTool,
|
||||
getConnectedAccount as libGetConnectedAccount,
|
||||
deleteConnectedAccount as libDeleteConnectedAccount,
|
||||
listAuthConfigs as libListAuthConfigs,
|
||||
|
|
@ -20,6 +23,7 @@ import {
|
|||
ZCredentials,
|
||||
} from "@/app/lib/composio/composio";
|
||||
import { ComposioConnectedAccount } from "@/app/lib/types/project_types";
|
||||
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||
import { getProjectConfig, projectAuthCheck } from "./project_actions";
|
||||
import { projectsCollection } from "../lib/mongodb";
|
||||
|
||||
|
|
@ -47,6 +51,24 @@ export async function listTools(projectId: string, toolkitSlug: string, cursor:
|
|||
return await libListTools(toolkitSlug, cursor);
|
||||
}
|
||||
|
||||
// New efficient search functions
|
||||
|
||||
export async function searchTools(projectId: string, searchQuery: string, cursor: string | null = null, limit?: number): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libSearchTools(searchQuery, cursor, limit);
|
||||
}
|
||||
|
||||
export async function getToolsByIds(projectId: string, toolSlugs: string[], cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libGetToolsByIds(toolSlugs, cursor);
|
||||
}
|
||||
|
||||
export async function getTool(projectId: string, toolSlug: string): Promise<z.infer<typeof ZTool>> {
|
||||
await projectAuthCheck(projectId);
|
||||
return await libGetTool(toolSlug);
|
||||
}
|
||||
|
||||
|
||||
export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
|
|
@ -215,12 +237,196 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str
|
|||
const key = `composioConnectedAccounts.${toolkitSlug}`;
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
|
||||
|
||||
// Notify other tabs about the tools update (lightweight refresh)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Note: composio tools are now stored in workflow.tools array with isComposio: true
|
||||
// This function provides backward compatibility by updating workflow tools
|
||||
export async function getComposioToolsFromWorkflow(projectId: string): Promise<z.infer<typeof ZTool>[]> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Extract composio tools from workflow and convert back to ZTool format
|
||||
const composioTools = project.draftWorkflow.tools
|
||||
.filter(tool => tool.isComposio && tool.composioData)
|
||||
.map(tool => ({
|
||||
slug: tool.composioData!.slug,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
no_auth: tool.composioData!.noAuth,
|
||||
input_parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.parameters.properties,
|
||||
required: tool.parameters.required || []
|
||||
},
|
||||
toolkit: {
|
||||
name: tool.composioData!.toolkitName,
|
||||
slug: tool.composioData!.toolkitSlug,
|
||||
logo: tool.composioData!.logo,
|
||||
}
|
||||
}));
|
||||
|
||||
return composioTools;
|
||||
}
|
||||
|
||||
export async function updateComposioSelectedTools(projectId: string, tools: z.infer<typeof ZTool>[]): Promise<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// update project with new selected tools
|
||||
await projectsCollection.updateOne({ _id: projectId }, { $set: { composioSelectedTools: tools } });
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
throw new Error(`Project ${projectId} not found or has no draft workflow`);
|
||||
}
|
||||
|
||||
// Convert Composio tools to workflow tool format
|
||||
const composioWorkflowTools: z.infer<typeof WorkflowTool>[] = tools.map(tool => ({
|
||||
name: tool.slug,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.input_parameters?.properties || {},
|
||||
required: tool.input_parameters?.required || []
|
||||
},
|
||||
isComposio: true,
|
||||
composioData: {
|
||||
slug: tool.slug,
|
||||
noAuth: tool.no_auth,
|
||||
toolkitName: tool.toolkit.name,
|
||||
toolkitSlug: tool.toolkit.slug,
|
||||
logo: tool.toolkit.logo,
|
||||
},
|
||||
}));
|
||||
|
||||
// Remove existing composio tools and add new ones
|
||||
const nonComposioTools = project.draftWorkflow.tools.filter(tool => !tool.isComposio);
|
||||
const updatedWorkflow = {
|
||||
...project.draftWorkflow,
|
||||
tools: [...nonComposioTools, ...composioWorkflowTools],
|
||||
lastUpdatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Update the project's draft workflow
|
||||
const result = await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { draftWorkflow: updatedWorkflow } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
throw new Error(`Failed to update workflow for project ${projectId}`);
|
||||
}
|
||||
|
||||
// Notify other tabs about the tools update (lightweight refresh)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Note: composio mock states are now stored in workflow.composioMockToolkitStates
|
||||
// This function provides backward compatibility by updating workflow mock states
|
||||
export async function toggleMockToolkitState(projectId: string, toolkitSlug: string, isMocked: boolean, mockInstructions?: string): Promise<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
throw new Error(`Project ${projectId} not found or has no draft workflow`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) };
|
||||
|
||||
if (isMocked) {
|
||||
// Enable mock mode
|
||||
updatedMockToolkitStates[toolkitSlug] = {
|
||||
toolkitSlug,
|
||||
isMocked: true,
|
||||
mockInstructions: mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.',
|
||||
autoSubmitMockedResponse: false,
|
||||
createdAt: now,
|
||||
lastUpdatedAt: now,
|
||||
};
|
||||
} else {
|
||||
// Disable mock mode - remove the toolkit from the object
|
||||
delete updatedMockToolkitStates[toolkitSlug];
|
||||
}
|
||||
|
||||
// Update the workflow with new mock states
|
||||
const updatedWorkflow = {
|
||||
...project.draftWorkflow,
|
||||
composioMockToolkitStates: updatedMockToolkitStates,
|
||||
lastUpdatedAt: now
|
||||
};
|
||||
|
||||
// Update the project's draft workflow
|
||||
const result = await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { draftWorkflow: updatedWorkflow } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
throw new Error(`Failed to update workflow mock states for project ${projectId}`);
|
||||
}
|
||||
|
||||
// Notify other tabs about the tools update (lightweight refresh)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-light-refresh-${projectId}`, Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Note: composio mock states are now stored in workflow.composioMockToolkitStates
|
||||
// This function provides backward compatibility by updating workflow mock states
|
||||
export async function updateMockToolkitInstructions(projectId: string, toolkitSlug: string, mockInstructions: string): Promise<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
// Get the project to access draft workflow
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project || !project.draftWorkflow) {
|
||||
throw new Error(`Project ${projectId} not found or has no draft workflow`);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
let updatedMockToolkitStates = { ...(project.draftWorkflow.composioMockToolkitStates || {}) };
|
||||
|
||||
// Update the mock instructions for the specified toolkit
|
||||
if (updatedMockToolkitStates[toolkitSlug]) {
|
||||
updatedMockToolkitStates[toolkitSlug] = {
|
||||
...updatedMockToolkitStates[toolkitSlug],
|
||||
mockInstructions,
|
||||
lastUpdatedAt: now
|
||||
};
|
||||
|
||||
// Update the workflow with new mock states
|
||||
const updatedWorkflow = {
|
||||
...project.draftWorkflow,
|
||||
composioMockToolkitStates: updatedMockToolkitStates,
|
||||
lastUpdatedAt: now
|
||||
};
|
||||
|
||||
// Update the project's draft workflow
|
||||
const result = await projectsCollection.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { draftWorkflow: updatedWorkflow } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
throw new Error(`Failed to update workflow mock instructions for project ${projectId}`);
|
||||
}
|
||||
|
||||
// Notify other tabs about the tools update
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`tools-updated-${projectId}`, Date.now().toString());
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Mock toolkit state for ${toolkitSlug} not found in project ${projectId}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,13 @@ import { authorizeUserAction } from "./billing_actions";
|
|||
import { Workflow } from "../lib/types/workflow_types";
|
||||
import { WorkflowTool } from "../lib/types/workflow_types";
|
||||
import { collectProjectTools as libCollectProjectTools } from "../lib/project_tools";
|
||||
import {
|
||||
searchTools as libSearchTools,
|
||||
getToolsByIds as libGetToolsByIds,
|
||||
getTool as libGetTool,
|
||||
ZTool,
|
||||
ZToolkit
|
||||
} from "../lib/composio/composio";
|
||||
|
||||
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
||||
|
||||
|
|
@ -306,6 +313,113 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id:
|
|||
return { id: projectId };
|
||||
}
|
||||
|
||||
async function detectAndAddComposioTools(projectId: string, workflow: z.infer<typeof Workflow>) {
|
||||
// Extract tool mentions from agent instructions
|
||||
const toolMentionPattern = /\[@tool:([^\]]+)\]\(#mention[^\)]*\)/g;
|
||||
const mentionedToolNames = new Set<string>();
|
||||
|
||||
// Scan all agent instructions for tool mentions
|
||||
for (const agent of workflow.agents || []) {
|
||||
const instructions = agent.instructions || "";
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = toolMentionPattern.exec(instructions))) {
|
||||
mentionedToolNames.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionedToolNames.size === 0) {
|
||||
return; // No tool mentions found
|
||||
}
|
||||
|
||||
console.log(`Found ${mentionedToolNames.size} tool mentions in workflow:`, Array.from(mentionedToolNames));
|
||||
|
||||
// Search for these tools in Composio using the new efficient search methods
|
||||
const foundTools: z.infer<typeof ZTool>[] = [];
|
||||
|
||||
try {
|
||||
// Method 1: Try to get tools directly by their exact slugs/names
|
||||
const mentionedToolNamesArray = Array.from(mentionedToolNames);
|
||||
|
||||
try {
|
||||
const directToolsResponse = await libGetToolsByIds(mentionedToolNamesArray);
|
||||
foundTools.push(...directToolsResponse.items);
|
||||
console.log(`Found ${directToolsResponse.items.length} tools by direct lookup`);
|
||||
} catch (error) {
|
||||
console.log('Direct tool lookup failed, trying search approach');
|
||||
}
|
||||
|
||||
// Method 2: For any remaining tools, use search functionality
|
||||
const foundToolSlugs = new Set(foundTools.map(tool => tool.slug));
|
||||
const foundToolNames = new Set(foundTools.map(tool => tool.name));
|
||||
const remainingToolNames = mentionedToolNamesArray.filter(name =>
|
||||
!foundToolSlugs.has(name) && !foundToolNames.has(name)
|
||||
);
|
||||
|
||||
for (const toolName of remainingToolNames) {
|
||||
try {
|
||||
// Search for tools by name/description
|
||||
const searchResponse = await libSearchTools(toolName, null, 10);
|
||||
|
||||
// Find exact matches by name or slug
|
||||
const exactMatches = searchResponse.items.filter(tool =>
|
||||
tool.name === toolName ||
|
||||
tool.slug === toolName ||
|
||||
tool.name.toLowerCase() === toolName.toLowerCase() ||
|
||||
tool.slug.toLowerCase() === toolName.toLowerCase()
|
||||
);
|
||||
|
||||
if (exactMatches.length > 0) {
|
||||
foundTools.push(...exactMatches);
|
||||
console.log(`Found ${exactMatches.length} tools for search term "${toolName}"`);
|
||||
} else {
|
||||
console.log(`No exact matches found for tool "${toolName}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error searching for tool "${toolName}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error searching for Composio tools:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundTools.length > 0) {
|
||||
console.log(`Adding ${foundTools.length} Composio tools to workflow`);
|
||||
|
||||
// Remove duplicates based on slug
|
||||
const uniqueTools = foundTools.filter((tool, index, self) =>
|
||||
index === self.findIndex(t => t.slug === tool.slug)
|
||||
);
|
||||
|
||||
// Convert Composio tools to workflow tool format
|
||||
const composioWorkflowTools: z.infer<typeof WorkflowTool>[] = uniqueTools.map(tool => ({
|
||||
name: tool.slug,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.input_parameters?.properties || {},
|
||||
required: tool.input_parameters?.required || []
|
||||
},
|
||||
isComposio: true,
|
||||
composioData: {
|
||||
slug: tool.slug,
|
||||
noAuth: tool.no_auth,
|
||||
toolkitName: tool.toolkit.name,
|
||||
toolkitSlug: tool.toolkit.slug,
|
||||
logo: tool.toolkit.logo,
|
||||
},
|
||||
}));
|
||||
|
||||
// Add these tools to the workflow.tools array
|
||||
workflow.tools = [...workflow.tools, ...composioWorkflowTools];
|
||||
|
||||
console.log(`Added ${composioWorkflowTools.length} Composio tools to workflow`);
|
||||
} else {
|
||||
console.log('No matching Composio tools found for the mentioned tool names');
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> {
|
||||
const user = await authCheck();
|
||||
const workflowJson = formData.get('workflowJson') as string;
|
||||
|
|
@ -327,6 +441,15 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
|
|||
return response;
|
||||
}
|
||||
const projectId = response.id;
|
||||
|
||||
// Automatically detect and add Composio tools mentioned in agent instructions
|
||||
try {
|
||||
await detectAndAddComposioTools(projectId, workflow);
|
||||
} catch (error) {
|
||||
// Log error but don't fail the import if tool detection fails
|
||||
console.error('Failed to auto-detect Composio tools:', error);
|
||||
}
|
||||
|
||||
return { id: projectId };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } f
|
|||
import { qdrantClient } from '../lib/qdrant';
|
||||
import { EmbeddingRecord } from "./types/datasource_types";
|
||||
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_types";
|
||||
import { Project } from "./types/project_types";
|
||||
import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions";
|
||||
import { PrefixLogger } from "./utils";
|
||||
import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types";
|
||||
|
|
@ -280,6 +281,8 @@ async function invokeComposioTool(
|
|||
name: string,
|
||||
composioData: z.infer<typeof WorkflowTool>['composioData'] & {},
|
||||
input: any,
|
||||
workflow: z.infer<typeof Workflow>,
|
||||
toolDescription?: string,
|
||||
) {
|
||||
logger = logger.child(`invokeComposioTool`);
|
||||
logger.log(`projectId: ${projectId}`);
|
||||
|
|
@ -288,12 +291,36 @@ async function invokeComposioTool(
|
|||
|
||||
const { slug, toolkitSlug, noAuth } = composioData;
|
||||
|
||||
// Get project configuration to check for connected accounts (still stored in project)
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project) {
|
||||
throw new Error(`project ${projectId} not found`);
|
||||
}
|
||||
|
||||
// Check if toolkit is in mock mode (now from workflow)
|
||||
const mockState = workflow.composioMockToolkitStates?.[toolkitSlug];
|
||||
if (mockState?.isMocked) {
|
||||
logger.log(`toolkit ${toolkitSlug} is in mock mode, using mock response`);
|
||||
|
||||
// Use the existing invokeMockTool function to generate a mock response
|
||||
const mockInstructions = mockState.mockInstructions || 'Mock responses using GPT-4.1 based on tool descriptions.';
|
||||
const description = toolDescription || `${name} tool from ${toolkitSlug} toolkit`;
|
||||
|
||||
const mockResponse = await invokeMockTool(
|
||||
logger,
|
||||
name,
|
||||
JSON.stringify(input),
|
||||
description,
|
||||
mockInstructions
|
||||
);
|
||||
|
||||
logger.log(`mock tool result: ${mockResponse}`);
|
||||
return mockResponse;
|
||||
}
|
||||
|
||||
// Normal execution path - check for authentication
|
||||
let connectedAccountId: string | undefined = undefined;
|
||||
if (!noAuth) {
|
||||
const project = await projectsCollection.findOne({ _id: projectId });
|
||||
if (!project) {
|
||||
throw new Error(`project ${projectId} not found`);
|
||||
}
|
||||
connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id;
|
||||
if (!connectedAccountId) {
|
||||
throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`);
|
||||
|
|
@ -452,7 +479,8 @@ function createMcpTool(
|
|||
function createComposioTool(
|
||||
logger: PrefixLogger,
|
||||
config: z.infer<typeof WorkflowTool>,
|
||||
projectId: string
|
||||
projectId: string,
|
||||
workflow: z.infer<typeof Workflow>
|
||||
): Tool {
|
||||
const { name, description, parameters, composioData } = config;
|
||||
|
||||
|
|
@ -472,7 +500,7 @@ function createComposioTool(
|
|||
},
|
||||
async execute(input: any) {
|
||||
try {
|
||||
const result = await invokeComposioTool(logger, projectId, name, composioData, input);
|
||||
const result = await invokeComposioTool(logger, projectId, name, composioData, input, workflow, description);
|
||||
return JSON.stringify({
|
||||
result,
|
||||
});
|
||||
|
|
@ -813,7 +841,7 @@ function createTools(
|
|||
tools[toolName] = createMcpTool(logger, config, projectId);
|
||||
logger.log(`created mcp tool: ${toolName}`);
|
||||
} else if (config.isComposio) {
|
||||
tools[toolName] = createComposioTool(logger, config, projectId);
|
||||
tools[toolName] = createComposioTool(logger, config, projectId, workflow);
|
||||
logger.log(`created composio tool: ${toolName}`);
|
||||
} else if (config.mockTool) {
|
||||
tools[toolName] = createMockTool(logger, config);
|
||||
|
|
|
|||
|
|
@ -291,6 +291,39 @@ export async function listTools(toolkitSlug: string, cursor: string | null = nul
|
|||
return composioApiCall(ZListResponse(ZTool), url.toString());
|
||||
}
|
||||
|
||||
export async function searchTools(searchQuery: string, cursor: string | null = null, limit: number = 50): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
const url = new URL(`${BASE_URL}/tools`);
|
||||
|
||||
// set params
|
||||
url.searchParams.set("search", searchQuery);
|
||||
if (cursor) {
|
||||
url.searchParams.set("cursor", cursor);
|
||||
}
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
|
||||
// fetch
|
||||
return composioApiCall(ZListResponse(ZTool), url.toString());
|
||||
}
|
||||
|
||||
export async function getToolsByIds(toolSlugs: string[], cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
const url = new URL(`${BASE_URL}/tools`);
|
||||
|
||||
// set params - pass tool slugs as comma-separated string
|
||||
url.searchParams.set("tool_slugs", toolSlugs.join(","));
|
||||
if (cursor) {
|
||||
url.searchParams.set("cursor", cursor);
|
||||
}
|
||||
|
||||
// fetch
|
||||
return composioApiCall(ZListResponse(ZTool), url.toString());
|
||||
}
|
||||
|
||||
export async function getTool(toolSlug: string): Promise<z.infer<typeof ZTool>> {
|
||||
const url = new URL(`${BASE_URL}/tools/${toolSlug}`);
|
||||
return composioApiCall(ZTool, url.toString());
|
||||
}
|
||||
|
||||
|
||||
export async function listAuthConfigs(toolkitSlug: string, cursor: string | null = null, managedOnly: boolean = false): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZAuthConfig>>>> {
|
||||
const url = new URL(`${BASE_URL}/auth_configs`);
|
||||
url.searchParams.set("toolkit_slug", toolkitSlug);
|
||||
|
|
|
|||
|
|
@ -33,28 +33,8 @@ export async function collectProjectTools(projectId: string): Promise<z.infer<ty
|
|||
}
|
||||
}
|
||||
|
||||
// Add Composio tools
|
||||
if (project.composioSelectedTools) {
|
||||
for (const tool of project.composioSelectedTools) {
|
||||
tools.push({
|
||||
name: tool.slug,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: 'object' as const,
|
||||
properties: tool.input_parameters?.properties || {},
|
||||
required: tool.input_parameters?.required || []
|
||||
},
|
||||
isComposio: true,
|
||||
composioData: {
|
||||
slug: tool.slug,
|
||||
noAuth: tool.no_auth,
|
||||
toolkitName: tool.toolkit.name,
|
||||
toolkitSlug: tool.toolkit.slug,
|
||||
logo: tool.toolkit.logo,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// Note: Composio tools are now stored in workflow.tools array with isComposio: true
|
||||
// This function now only collects MCP tools since composio tools are managed in workflow
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { z } from "zod";
|
||||
import { MCPServer } from "./types";
|
||||
import { Workflow, WorkflowTool } from "./workflow_types";
|
||||
import { ZTool } from "../composio/composio";
|
||||
|
||||
export const ComposioConnectedAccount = z.object({
|
||||
id: z.string(),
|
||||
|
|
@ -30,7 +29,6 @@ export const Project = z.object({
|
|||
testRunCounter: z.number().default(0),
|
||||
mcpServers: z.array(MCPServer).optional(),
|
||||
composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(),
|
||||
composioSelectedTools: z.array(ZTool).optional(),
|
||||
});
|
||||
|
||||
export const ProjectMember = z.object({
|
||||
|
|
|
|||
|
|
@ -67,6 +67,14 @@ export const Workflow = z.object({
|
|||
startAgent: z.string(),
|
||||
lastUpdatedAt: z.string().datetime(),
|
||||
mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions
|
||||
composioMockToolkitStates: z.record(z.string(), z.object({
|
||||
toolkitSlug: z.string(),
|
||||
isMocked: z.boolean(),
|
||||
mockInstructions: z.string().optional(),
|
||||
autoSubmitMockedResponse: z.boolean().default(false),
|
||||
createdAt: z.string().datetime(),
|
||||
lastUpdatedAt: z.string().datetime(),
|
||||
})).optional(),
|
||||
});
|
||||
export const WorkflowTemplate = Workflow
|
||||
.omit({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default async function ToolsPage() {
|
|||
<div className="flex-1 p-6">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ToolsConfig
|
||||
useComposioTools={USE_COMPOSIO_TOOLS}
|
||||
useComposioTools={false}
|
||||
useKlavisTools={USE_KLAVIS_TOOLS}
|
||||
/>
|
||||
</Suspense>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
import { MCPServer, WithStringId } from "../../../lib/types/types";
|
||||
import { DataSource } from "../../../lib/types/datasource_types";
|
||||
import { Project } from "../../../lib/types/project_types";
|
||||
import { z } from "zod";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { WorkflowEditor } from "./workflow_editor";
|
||||
|
|
@ -11,7 +12,6 @@ import { getProjectConfig } from "@/app/actions/project_actions";
|
|||
import { Workflow, WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||
import { getEligibleModels } from "@/app/actions/billing_actions";
|
||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
||||
import { Project } from "@/app/lib/types/project_types";
|
||||
|
||||
export function App({
|
||||
projectId,
|
||||
|
|
@ -26,6 +26,7 @@ export function App({
|
|||
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null);
|
||||
const [dataSources, setDataSources] = useState<WithStringId<z.infer<typeof DataSource>>[] | null>(null);
|
||||
const [projectTools, setProjectTools] = useState<z.infer<typeof WorkflowTool>[] | null>(null);
|
||||
const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [eligibleModels, setEligibleModels] = useState<z.infer<typeof ModelsResponse> | "*">("*");
|
||||
const [projectMcpServers, setProjectMcpServers] = useState<Array<z.infer<typeof MCPServer>>>([]);
|
||||
|
|
@ -66,11 +67,70 @@ export function App({
|
|||
setLoading(false);
|
||||
}, [projectId]);
|
||||
|
||||
const handleProjectToolsUpdate = useCallback(async () => {
|
||||
// Lightweight refresh for tool-only updates
|
||||
const [projectConfig, projectTools] = await Promise.all([
|
||||
getProjectConfig(projectId),
|
||||
collectProjectTools(projectId),
|
||||
]);
|
||||
|
||||
setProject(projectConfig);
|
||||
setProjectConfig(projectConfig);
|
||||
setProjectTools(projectTools);
|
||||
|
||||
// Update MCP servers if they changed
|
||||
if (projectConfig.mcpServers) {
|
||||
setProjectMcpServers(projectConfig.mcpServers);
|
||||
}
|
||||
|
||||
// Update webhook URL if it changed
|
||||
if (projectConfig.webhookUrl) {
|
||||
setWebhookUrl(projectConfig.webhookUrl);
|
||||
}
|
||||
}, [projectId]);
|
||||
// Add this useEffect for initial load
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [mode, loadData, projectId]);
|
||||
|
||||
// Add focus-based refresh to handle cross-page updates
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
// Refresh data when user returns to this page/tab
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
loadData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
// Listen for tool updates from other tabs
|
||||
if (e.key === `tools-updated-${projectId}` && e.newValue) {
|
||||
loadData();
|
||||
// Clear the flag
|
||||
localStorage.removeItem(`tools-updated-${projectId}`);
|
||||
} else if (e.key === `tools-light-refresh-${projectId}` && e.newValue) {
|
||||
// Lightweight refresh for tool-only updates
|
||||
handleProjectToolsUpdate();
|
||||
// Clear the flag
|
||||
localStorage.removeItem(`tools-light-refresh-${projectId}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('focus', handleFocus);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}, [loadData, handleProjectToolsUpdate, projectId]);
|
||||
|
||||
function handleSetMode(mode: 'draft' | 'live') {
|
||||
setMode(mode);
|
||||
}
|
||||
|
|
@ -95,6 +155,7 @@ export function App({
|
|||
workflow={workflow}
|
||||
dataSources={dataSources}
|
||||
projectTools={projectTools}
|
||||
projectConfig={projectConfig || project}
|
||||
useRag={useRag}
|
||||
mcpServerUrls={projectMcpServers}
|
||||
toolWebhookUrl={webhookUrl}
|
||||
|
|
@ -102,6 +163,7 @@ export function App({
|
|||
eligibleModels={eligibleModels}
|
||||
onChangeMode={handleSetMode}
|
||||
onRevertToLive={handleRevertToLive}
|
||||
onProjectToolsUpdated={handleProjectToolsUpdate}
|
||||
/>}
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, Tabs, Tab } from '@heroui/react';
|
||||
import { Composio } from '../../tools/components/Composio';
|
||||
import { ComposioWithCallback } from './ComposioWithCallback';
|
||||
import { CustomServers } from '../../tools/components/CustomServers';
|
||||
import { WebhookConfig } from '../../tools/components/WebhookConfig';
|
||||
import type { Key } from 'react';
|
||||
|
||||
interface ComposioToolsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: string;
|
||||
onToolsUpdated?: () => void;
|
||||
}
|
||||
|
||||
export function ComposioToolsModal({ isOpen, onClose, projectId, onToolsUpdated }: ComposioToolsModalProps) {
|
||||
const [activeTab, setActiveTab] = useState('composio');
|
||||
|
||||
const handleTabChange = (key: Key) => {
|
||||
setActiveTab(key.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="5xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
base: "max-h-[90vh]",
|
||||
body: "p-0",
|
||||
header: "pb-3"
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<h3 className="text-lg font-semibold">
|
||||
Tools
|
||||
</h3>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Tabs
|
||||
selectedKey={activeTab}
|
||||
onSelectionChange={handleTabChange}
|
||||
aria-label="Tool configuration options"
|
||||
className="w-full h-full"
|
||||
fullWidth
|
||||
classNames={{
|
||||
panel: "h-full min-h-[60vh]"
|
||||
}}
|
||||
>
|
||||
<Tab key="composio" title="Composio">
|
||||
<div className="p-6 h-full">
|
||||
<ComposioWithCallback projectId={projectId} onToolsUpdated={onToolsUpdated} />
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab key="custom" title="Custom MCP Servers">
|
||||
<div className="p-6 h-full">
|
||||
<CustomServers onToolsUpdated={onToolsUpdated} />
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab key="webhook" title="Webhook">
|
||||
<div className="p-6 h-full">
|
||||
<WebhookConfig />
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Info, RefreshCw, Search } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
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';
|
||||
import { Project } from '@/app/lib/types/project_types';
|
||||
import { ComposioToolsPanel } from '../../tools/components/ComposioToolsPanel';
|
||||
import { ToolkitCard } from '../../tools/components/ToolkitCard';
|
||||
|
||||
type ToolkitType = z.infer<typeof ZToolkit>;
|
||||
type ToolkitListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>;
|
||||
type ProjectType = z.infer<typeof Project>;
|
||||
|
||||
interface ComposioWithCallbackProps {
|
||||
projectId: string;
|
||||
onToolsUpdated?: () => void;
|
||||
}
|
||||
|
||||
export function ComposioWithCallback({ projectId, onToolsUpdated }: ComposioWithCallbackProps) {
|
||||
|
||||
const [toolkits, setToolkits] = useState<ToolkitType[]>([]);
|
||||
const [projectConfig, setProjectConfig] = useState<ProjectType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
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 {
|
||||
const config = await getProjectConfig(projectId);
|
||||
setProjectConfig(config);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching project config:', err);
|
||||
setError('Unable to load project configuration.');
|
||||
}
|
||||
}, [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[] = [];
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
do {
|
||||
const response: ToolkitListResponse = await listToolkits(projectId, cursor);
|
||||
allToolkits = [...allToolkits, ...response.items];
|
||||
cursor = response.next_cursor;
|
||||
} while (cursor !== null);
|
||||
|
||||
setToolkits(allToolkits);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError('Unable to load all Composio toolkits. Please check your connection and try again.');
|
||||
console.error('Error fetching all toolkits:', err);
|
||||
setToolkits([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const handleManageTools = useCallback((toolkit: ToolkitType) => {
|
||||
setSelectedToolkit(toolkit);
|
||||
setIsToolsPanelOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseToolsPanel = useCallback(() => {
|
||||
setSelectedToolkit(null);
|
||||
setIsToolsPanelOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleProjectConfigUpdate = useCallback(() => {
|
||||
loadProjectConfig();
|
||||
loadComposioSelectedTools();
|
||||
}, [loadProjectConfig, loadComposioSelectedTools]);
|
||||
|
||||
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
|
||||
if (!projectId) return;
|
||||
|
||||
setSavingTools(true);
|
||||
try {
|
||||
// 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]));
|
||||
|
||||
// Add or update the new selections
|
||||
for (const tool of selectedToolObjects) {
|
||||
existingToolsMap.set(tool.slug, tool);
|
||||
}
|
||||
|
||||
// Convert back to array
|
||||
const mergedSelectedTools = Array.from(existingToolsMap.values());
|
||||
|
||||
await updateComposioSelectedTools(projectId, mergedSelectedTools);
|
||||
|
||||
// Refresh data to get updated tools
|
||||
await loadComposioSelectedTools();
|
||||
|
||||
// Notify parent component that tools were updated
|
||||
if (onToolsUpdated) {
|
||||
onToolsUpdated();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving tool selection:', error);
|
||||
} finally {
|
||||
setSavingTools(false);
|
||||
}
|
||||
}, [projectId, composioSelectedTools, loadComposioSelectedTools, onToolsUpdated]);
|
||||
|
||||
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
|
||||
if (!projectId) return;
|
||||
|
||||
setSavingTools(true);
|
||||
try {
|
||||
// Get existing selected tools from workflow
|
||||
const existingSelectedTools = composioSelectedTools;
|
||||
|
||||
// Filter out all tools from the specified toolkit
|
||||
const filteredSelectedTools = existingSelectedTools.filter(tool =>
|
||||
tool.toolkit.slug !== toolkitSlug
|
||||
);
|
||||
|
||||
await updateComposioSelectedTools(projectId, filteredSelectedTools);
|
||||
|
||||
// Refresh data to get updated tools
|
||||
await loadComposioSelectedTools();
|
||||
|
||||
// Notify parent component that tools were updated
|
||||
if (onToolsUpdated) {
|
||||
onToolsUpdated();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing toolkit tools:', error);
|
||||
} finally {
|
||||
setSavingTools(false);
|
||||
}
|
||||
}, [projectId, composioSelectedTools, loadComposioSelectedTools, onToolsUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
loadProjectConfig();
|
||||
}, [loadProjectConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
loadAllToolkits();
|
||||
loadComposioSelectedTools();
|
||||
}, [loadAllToolkits, loadComposioSelectedTools]);
|
||||
|
||||
const filteredToolkits = toolkits.filter(toolkit => {
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
return (
|
||||
toolkit.name.toLowerCase().includes(searchLower) ||
|
||||
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) {
|
||||
return (
|
||||
<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 Composio toolkits...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[50vh] space-y-6 px-4">
|
||||
<p className="text-center text-red-500 dark:text-red-400 max-w-[600px]">
|
||||
{error}
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
loadProjectConfig();
|
||||
loadAllToolkits();
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Search Bar */}
|
||||
<div className="mb-6">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search toolkits..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
||||
placeholder-gray-500 dark:placeholder-gray-400
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolkits Grid */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{filteredToolkits.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{searchQuery ? 'No toolkits found matching your search.' : 'No toolkits available.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredToolkits.map((toolkit) => {
|
||||
const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
|
||||
|
||||
return (
|
||||
<ToolkitCard
|
||||
key={toolkit.slug}
|
||||
toolkit={toolkit}
|
||||
projectId={projectId}
|
||||
isConnected={isConnected}
|
||||
connectedAccountId={connectedAccountId}
|
||||
projectConfig={projectConfig}
|
||||
onManageTools={() => handleManageTools(toolkit)}
|
||||
onProjectConfigUpdate={handleProjectConfigUpdate}
|
||||
onRemoveToolkitTools={handleRemoveToolkitTools}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tools Panel */}
|
||||
<ComposioToolsPanel
|
||||
toolkit={selectedToolkit}
|
||||
isOpen={isToolsPanelOpen}
|
||||
onClose={handleCloseToolsPanel}
|
||||
projectConfig={projectConfig}
|
||||
onUpdateToolsSelection={handleUpdateToolsSelection}
|
||||
onProjectConfigUpdate={handleProjectConfigUpdate}
|
||||
onRemoveToolkitTools={handleRemoveToolkitTools}
|
||||
isSaving={savingTools}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { z } from "zod";
|
||||
import { WorkflowPrompt, WorkflowAgent, WorkflowTool } from "../../../lib/types/workflow_types";
|
||||
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, Eye } from "lucide-react";
|
||||
import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Boxes, Wrench, PenLine, Library, ChevronDown, ChevronRight, ServerIcon, Component, ScrollText, GripVertical, Users, Cog, CheckCircle2, LinkIcon, UnlinkIcon, TestTube, Play, MoreVertical, Eye } 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';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
|
@ -13,6 +15,9 @@ import { clsx } from "clsx";
|
|||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
|
||||
import { ServerLogo } from '../tools/components/MCPServersCommon';
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react";
|
||||
import { ComposioToolsModal } from './components/ComposioToolsModal';
|
||||
import { ToolkitAuthModal } from '../tools/components/ToolkitAuthModal';
|
||||
import { deleteConnectedAccount, toggleMockToolkitState } from '@/app/actions/composio_actions';
|
||||
|
||||
// Reduced gap size to match Cursor's UI
|
||||
const GAP_SIZE = 4; // 1 unit * 4px (tailwind's default spacing unit)
|
||||
|
|
@ -35,6 +40,7 @@ interface EntityListProps {
|
|||
tools: z.infer<typeof WorkflowTool>[];
|
||||
projectTools: z.infer<typeof WorkflowTool>[];
|
||||
prompts: z.infer<typeof WorkflowPrompt>[];
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
selectedEntity: {
|
||||
type: "agent" | "tool" | "prompt" | "visualise";
|
||||
name: string;
|
||||
|
|
@ -52,6 +58,8 @@ interface EntityListProps {
|
|||
onDeleteTool: (name: string) => void;
|
||||
onDeletePrompt: (name: string) => void;
|
||||
onShowVisualise: (name: string) => void;
|
||||
onProjectToolsUpdated?: () => void;
|
||||
projectConfig?: z.infer<typeof Project>;
|
||||
}
|
||||
|
||||
interface EmptyStateProps {
|
||||
|
|
@ -95,7 +103,7 @@ const ListItemWithMenu = ({
|
|||
}) => {
|
||||
return (
|
||||
<div className={clsx(
|
||||
"group flex items-center gap-2 px-2 py-1.5 rounded-md",
|
||||
"group flex items-center gap-2 px-2 py-0.5 rounded-md min-h-[24px]",
|
||||
{
|
||||
"bg-indigo-50 dark:bg-indigo-950/30": isSelected,
|
||||
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !isSelected
|
||||
|
|
@ -116,22 +124,20 @@ const ListItemWithMenu = ({
|
|||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className={clsx("shrink-0 flex items-center justify-center w-4 h-4", iconClassName)}>
|
||||
<div className={clsx("shrink-0 flex items-center justify-center w-3 h-3", iconClassName)}>
|
||||
{mcpServerName ? (
|
||||
<ServerLogo
|
||||
serverName={mcpServerName}
|
||||
className="h-4 w-4"
|
||||
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />}
|
||||
className="h-3 w-3"
|
||||
fallback={<ImportIcon className="w-3 h-3 text-blue-600 dark:text-blue-500" />}
|
||||
/>
|
||||
) : icon}
|
||||
</div>
|
||||
{name}
|
||||
<span className="text-xs">{name}</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{statusLabel}
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{menuContent}
|
||||
</div>
|
||||
{menuContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -166,43 +172,56 @@ const ServerCard = ({
|
|||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center gap-2 px-2 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md text-sm text-left"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<ServerLogo
|
||||
serverName={serverName}
|
||||
className="h-4 w-4"
|
||||
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />}
|
||||
/>
|
||||
<span>{serverName}</span>
|
||||
</div>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
{tools.map((tool, index) => (
|
||||
<ListItemWithMenu
|
||||
key={`tool-${index}`}
|
||||
name={tool.name}
|
||||
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
|
||||
onClick={() => onSelectTool(tool.name)}
|
||||
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
||||
mcpServerName={serverName}
|
||||
menuContent={
|
||||
<EntityDropdown
|
||||
name={tool.name}
|
||||
onDelete={onDeleteTool}
|
||||
isLocked={tool.isMcp || tool.isLibrary}
|
||||
/>
|
||||
}
|
||||
<div className="mb-1 group">
|
||||
<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]"
|
||||
>
|
||||
{/* Chevron - only show when has tools and on hover */}
|
||||
<div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
|
||||
tools.length > 0 ? 'group-hover:opacity-100 opacity-60' : 'opacity-0'
|
||||
}`}>
|
||||
{tools.length > 0 && (isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 text-gray-500" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<ServerLogo
|
||||
serverName={serverName}
|
||||
className="h-4 w-4"
|
||||
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />}
|
||||
/>
|
||||
<span className="text-sm">{serverName}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="ml-6 mt-0.5 space-y-0.5 border-l border-gray-200 dark:border-gray-700 pl-3">
|
||||
{tools.map((tool, index) => (
|
||||
<div key={`tool-${index}`} className="group/tool">
|
||||
<ListItemWithMenu
|
||||
name={tool.name}
|
||||
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
|
||||
onClick={() => onSelectTool(tool.name)}
|
||||
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
||||
mcpServerName={serverName}
|
||||
menuContent={
|
||||
<div className="opacity-0 group-hover/tool:opacity-100 transition-opacity">
|
||||
<EntityDropdown
|
||||
name={tool.name}
|
||||
onDelete={onDeleteTool}
|
||||
isLocked={tool.isMcp || tool.isLibrary}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -222,6 +241,7 @@ export function EntityList({
|
|||
tools,
|
||||
projectTools,
|
||||
prompts,
|
||||
workflow,
|
||||
selectedEntity,
|
||||
startAgentName,
|
||||
onSelectAgent,
|
||||
|
|
@ -235,7 +255,9 @@ export function EntityList({
|
|||
onDeleteAgent,
|
||||
onDeleteTool,
|
||||
onDeletePrompt,
|
||||
onProjectToolsUpdated,
|
||||
projectId,
|
||||
projectConfig,
|
||||
onReorderAgents,
|
||||
onShowVisualise,
|
||||
}: EntityListProps & {
|
||||
|
|
@ -243,6 +265,7 @@ export function EntityList({
|
|||
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
|
||||
}) {
|
||||
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
||||
const [showComposioToolsModal, setShowComposioToolsModal] = useState(false);
|
||||
|
||||
const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => {
|
||||
onAddAgent({
|
||||
|
|
@ -496,20 +519,36 @@ export function EntityList({
|
|||
<Wrench className="w-4 h-4" />
|
||||
<span>Tools</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedPanels(prev => ({ ...prev, tools: true }));
|
||||
onAddTool({});
|
||||
}}
|
||||
className={`group ${buttonClasses}`}
|
||||
showHoverContent={true}
|
||||
hoverContent="Add Tool"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedPanels(prev => ({ ...prev, tools: true }));
|
||||
setShowComposioToolsModal(true);
|
||||
}}
|
||||
className={`group ${buttonClasses}`}
|
||||
showHoverContent={true}
|
||||
hoverContent="Composio Tools"
|
||||
>
|
||||
<Component className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedPanels(prev => ({ ...prev, tools: true }));
|
||||
onAddTool({});
|
||||
}}
|
||||
className={`group ${buttonClasses}`}
|
||||
showHoverContent={true}
|
||||
hoverContent="Add Tool"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -536,17 +575,37 @@ export function EntityList({
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* Show composio cards */}
|
||||
{Object.values(composioTools).map((card) => (
|
||||
<ComposioCard
|
||||
key={card.slug}
|
||||
card={card}
|
||||
selectedEntity={selectedEntity}
|
||||
onSelectTool={handleToolSelection}
|
||||
onDeleteTool={onDeleteTool}
|
||||
selectedRef={selectedRef}
|
||||
/>
|
||||
))}
|
||||
{/* Show composio cards - ordered by status */}
|
||||
{Object.values(composioTools)
|
||||
.sort((a, b) => {
|
||||
// Helper function to get toolkit status priority
|
||||
const getStatusPriority = (toolkit: ComposioToolkit) => {
|
||||
const hasAuth = toolkit.tools.some(tool => tool.composioData && !tool.composioData.noAuth);
|
||||
const isConnected = !hasAuth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||
const isMocked = workflow?.composioMockToolkitStates?.[toolkit.slug]?.isMocked || false;
|
||||
|
||||
// Priority: Connected (1) > Mock (2) > Disconnected (3)
|
||||
if (isMocked) return 2;
|
||||
if (isConnected) return 1;
|
||||
return 3;
|
||||
};
|
||||
|
||||
return getStatusPriority(a) - getStatusPriority(b);
|
||||
})
|
||||
.map((card) => (
|
||||
<ComposioCard
|
||||
key={card.slug}
|
||||
card={card}
|
||||
selectedEntity={selectedEntity}
|
||||
onSelectTool={handleToolSelection}
|
||||
onDeleteTool={onDeleteTool}
|
||||
selectedRef={selectedRef}
|
||||
projectConfig={projectConfig}
|
||||
projectId={projectId}
|
||||
workflow={workflow}
|
||||
onProjectToolsUpdated={onProjectToolsUpdated}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Show MCP server cards */}
|
||||
{Object.entries(serverTools).map(([serverName, tools]) => (
|
||||
|
|
@ -682,6 +741,12 @@ export function EntityList({
|
|||
onClose={() => setShowAgentTypeModal(false)}
|
||||
onConfirm={handleAddAgentWithType}
|
||||
/>
|
||||
<ComposioToolsModal
|
||||
isOpen={showComposioToolsModal}
|
||||
onClose={() => setShowComposioToolsModal(false)}
|
||||
projectId={projectId}
|
||||
onToolsUpdated={onProjectToolsUpdated}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -769,6 +834,10 @@ interface ComposioCardProps {
|
|||
onSelectTool: (name: string) => void;
|
||||
onDeleteTool: (name: string) => void;
|
||||
selectedRef: React.RefObject<HTMLButtonElement | null>;
|
||||
projectConfig?: z.infer<typeof Project>;
|
||||
projectId: string;
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
onProjectToolsUpdated?: () => void;
|
||||
}
|
||||
|
||||
const ComposioCard = ({
|
||||
|
|
@ -777,70 +846,231 @@ const ComposioCard = ({
|
|||
onSelectTool,
|
||||
onDeleteTool,
|
||||
selectedRef,
|
||||
projectConfig,
|
||||
projectId,
|
||||
workflow,
|
||||
onProjectToolsUpdated,
|
||||
}: ComposioCardProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
|
||||
const [isProcessingMock, setIsProcessingMock] = 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';
|
||||
|
||||
// Check if toolkit is mocked
|
||||
const isToolkitMocked = workflow?.composioMockToolkitStates?.[card.slug]?.isMocked || false;
|
||||
|
||||
const handleConnect = () => {
|
||||
setShowAuthModal(true);
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[card.slug]?.id;
|
||||
|
||||
setIsProcessingAuth(true);
|
||||
try {
|
||||
if (connectedAccountId) {
|
||||
await deleteConnectedAccount(projectId, card.slug, connectedAccountId);
|
||||
onProjectToolsUpdated?.();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Disconnect failed:', err);
|
||||
} finally {
|
||||
setIsProcessingAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthComplete = () => {
|
||||
setShowAuthModal(false);
|
||||
onProjectToolsUpdated?.();
|
||||
};
|
||||
|
||||
const handleToggleMock = async () => {
|
||||
setIsProcessingMock(true);
|
||||
try {
|
||||
await toggleMockToolkitState(projectId, card.slug, !isToolkitMocked);
|
||||
onProjectToolsUpdated?.();
|
||||
} catch (err: any) {
|
||||
console.error('Mock toggle failed:', err);
|
||||
} finally {
|
||||
setIsProcessingMock(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center gap-2 px-2 py-1.5 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md text-sm text-left"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
{card.logo ? (
|
||||
<div className="relative w-4 h-4">
|
||||
<PictureImg
|
||||
src={card.logo}
|
||||
alt={`${card.name} logo`}
|
||||
className="w-full h-full object-contain rounded"
|
||||
/>
|
||||
<>
|
||||
<div className="mb-1 group">
|
||||
<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]"
|
||||
>
|
||||
{/* Chevron - only show on hover or when has tools */}
|
||||
<div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
|
||||
card.tools.length > 0 ? 'group-hover:opacity-100 opacity-60' : 'opacity-0'
|
||||
}`}>
|
||||
{card.tools.length > 0 && (isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3 text-gray-500" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
|
||||
)}
|
||||
<span>{card.name}</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{card.logo ? (
|
||||
<div className="relative w-4 h-4">
|
||||
<PictureImg
|
||||
src={card.logo}
|
||||
alt={`${card.name} logo`}
|
||||
className="w-full h-full object-contain rounded"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
|
||||
)}
|
||||
<span className="text-sm">{card.name}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Compact Status Badge */}
|
||||
<Tooltip
|
||||
content={isToolkitMocked ? 'Mocked' : isToolkitConnected ? 'Connected' : 'Disconnected'}
|
||||
size="sm"
|
||||
delay={500}
|
||||
>
|
||||
<div className={`w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium text-white ${
|
||||
isToolkitMocked
|
||||
? 'bg-purple-500'
|
||||
: isToolkitConnected
|
||||
? 'bg-emerald-500'
|
||||
: 'bg-orange-500'
|
||||
}`}>
|
||||
{isToolkitMocked ? 'M' : isToolkitConnected ? '●' : '○'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{/* Actions Dropdown - only show on hover */}
|
||||
<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 'mock':
|
||||
handleToggleMock();
|
||||
break;
|
||||
case 'connect':
|
||||
handleConnect();
|
||||
break;
|
||||
case 'disconnect':
|
||||
handleDisconnect();
|
||||
break;
|
||||
}
|
||||
}}
|
||||
disabledKeys={[
|
||||
...(isProcessingMock ? ['mock'] : []),
|
||||
...(isProcessingAuth ? ['connect', 'disconnect'] : []),
|
||||
...(hasToolkitWithAuth && !isToolkitMocked && isToolkitConnected ? [] : ['disconnect']),
|
||||
...(hasToolkitWithAuth && !isToolkitConnected ? [] : ['connect'])
|
||||
]}
|
||||
>
|
||||
<DropdownItem
|
||||
key="mock"
|
||||
startContent={
|
||||
isProcessingMock ? (
|
||||
<div className="animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600"></div>
|
||||
) : isToolkitMocked ? (
|
||||
<Play className="h-3 w-3" />
|
||||
) : (
|
||||
<TestTube className="h-3 w-3" />
|
||||
)
|
||||
}
|
||||
>
|
||||
{isProcessingMock
|
||||
? (isToolkitMocked ? 'Disabling Mock...' : 'Enabling Mock...')
|
||||
: (isToolkitMocked ? 'Switch to Real Mode' : 'Switch to Mock Mode')
|
||||
}
|
||||
</DropdownItem>
|
||||
|
||||
<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>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
<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) => (
|
||||
<ListItemWithMenu
|
||||
key={`composio-tool-${index}`}
|
||||
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={
|
||||
card.logo ? (
|
||||
<div className="relative w-4 h-4">
|
||||
<PictureImg
|
||||
src={card.logo}
|
||||
alt={`${card.name} logo`}
|
||||
className="w-full h-full object-contain rounded"
|
||||
<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>
|
||||
}
|
||||
menuContent={
|
||||
<div className="opacity-0 group-hover/tool:opacity-100 transition-opacity">
|
||||
<EntityDropdown
|
||||
name={tool.name}
|
||||
onDelete={onDeleteTool}
|
||||
isLocked={tool.isComposio}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
|
||||
)
|
||||
}
|
||||
menuContent={
|
||||
<EntityDropdown
|
||||
name={tool.name}
|
||||
onDelete={onDeleteTool}
|
||||
isLocked={tool.isComposio}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auth Modal */}
|
||||
{hasToolkitWithAuth && (
|
||||
<ToolkitAuthModal
|
||||
key={card.slug}
|
||||
isOpen={showAuthModal}
|
||||
onClose={() => setShowAuthModal(false)}
|
||||
toolkitSlug={card.slug}
|
||||
projectId={projectId}
|
||||
onComplete={handleAuthComplete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
151
apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx
Normal file
151
apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"use client";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Spinner, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Checkbox } from "@heroui/react";
|
||||
import { z } from "zod";
|
||||
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
import { fetchMcpTools } from "@/app/actions/mcp_actions";
|
||||
|
||||
interface McpImportToolsProps {
|
||||
projectId: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onImport: (tools: z.infer<typeof WorkflowTool>[]) => void;
|
||||
}
|
||||
|
||||
export function McpImportTools({ projectId, isOpen, onOpenChange, onImport }: McpImportToolsProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tools, setTools] = useState<z.infer<typeof WorkflowTool>[]>([]);
|
||||
const [selectedTools, setSelectedTools] = useState<Set<number>>(new Set());
|
||||
|
||||
const process = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSelectedTools(new Set());
|
||||
try {
|
||||
const result = await fetchMcpTools(projectId);
|
||||
setTools(result);
|
||||
// Select all tools by default
|
||||
setSelectedTools(new Set(result.map((_, index) => index)));
|
||||
} catch (error) {
|
||||
setError(`Unable to fetch tools: ${error}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("mcp import tools useEffect", isOpen);
|
||||
if (isOpen) {
|
||||
process();
|
||||
}
|
||||
}, [isOpen, process]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="xl">
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>Import from MCP servers</ModalHeader>
|
||||
<ModalBody>
|
||||
{loading && <div className="flex gap-2 items-center">
|
||||
<Spinner size="sm" />
|
||||
Fetching tools...
|
||||
</div>}
|
||||
{error && <div className="bg-red-100 p-2 rounded-md text-red-800 flex items-center gap-2 text-sm">
|
||||
{error}
|
||||
<Button size="sm" color="danger" onPress={() => process()}>Retry</Button>
|
||||
</div>}
|
||||
{!loading && !error && <>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-gray-600">
|
||||
{tools.length === 0 ? "No tools found" : `Found ${tools.length} tools:`}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => {
|
||||
setTools([]);
|
||||
process();
|
||||
}}
|
||||
startContent={<RefreshCwIcon className="w-4 h-4" />}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
{tools.length > 0 && <div className="flex flex-col w-full mt-4">
|
||||
<div className="flex items-center gap-4 px-4 py-2 bg-gray-50 rounded-t-lg border-b text-sm text-gray-700 font-medium">
|
||||
<div className="w-8">
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={selectedTools.size === tools.length}
|
||||
isIndeterminate={selectedTools.size > 0 && selectedTools.size < tools.length}
|
||||
onValueChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedTools(new Set(tools.map((_, i) => i)));
|
||||
} else {
|
||||
setSelectedTools(new Set());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-36">Server</div>
|
||||
<div className="flex-1">Tool Name</div>
|
||||
</div>
|
||||
<div className="border rounded-b-lg divide-y overflow-y-auto max-h-[300px]">
|
||||
{tools.map((t, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-4 px-4 py-2 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="w-8">
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={selectedTools.has(index)}
|
||||
onValueChange={(checked) => {
|
||||
const newSelected = new Set(selectedTools);
|
||||
if (checked) {
|
||||
newSelected.add(index);
|
||||
} else {
|
||||
newSelected.delete(index);
|
||||
}
|
||||
setSelectedTools(newSelected);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-36">
|
||||
<div className="bg-blue-50 px-2 py-1 rounded text-blue-700 text-sm font-medium border border-blue-100">
|
||||
{t.mcpServerName}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 truncate text-gray-700">{t.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
{tools.length > 0 && (
|
||||
<div className="mt-4 text-sm text-gray-600">
|
||||
{selectedTools.size} of {tools.length} tools selected
|
||||
</div>
|
||||
)}
|
||||
</>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="flat" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
{tools.length > 0 && <Button size="sm" onPress={() => {
|
||||
const selectedToolsList = tools.filter((_, index) => selectedTools.has(index));
|
||||
onImport(selectedToolsList);
|
||||
onClose();
|
||||
}}>
|
||||
Import
|
||||
</Button>}
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, c
|
|||
import { MCPServer, Message, WithStringId } from "../../../lib/types/types";
|
||||
import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types";
|
||||
import { DataSource } from "../../../lib/types/datasource_types";
|
||||
import { Project } from "../../../lib/types/project_types";
|
||||
import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer';
|
||||
import { AgentConfig } from "../entities/agent_config";
|
||||
import { ToolConfig } from "../entities/tool_config";
|
||||
|
|
@ -142,6 +143,9 @@ export type Action = {
|
|||
type: "show_visualise";
|
||||
} | {
|
||||
type: "hide_visualise";
|
||||
} | {
|
||||
type: "sync_workflow";
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
};
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
|
|
@ -207,6 +211,13 @@ function reducer(state: State, action: Action): State {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "sync_workflow": {
|
||||
newState = produce(state, draft => {
|
||||
draft.present.workflow = action.workflow;
|
||||
draft.present.lastUpdatedAt = action.workflow.lastUpdatedAt;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "reorder_agents": {
|
||||
const newState = produce(state.present, draft => {
|
||||
draft.workflow.agents = action.agents;
|
||||
|
|
@ -579,10 +590,12 @@ export function WorkflowEditor({
|
|||
toolWebhookUrl,
|
||||
defaultModel,
|
||||
projectTools,
|
||||
projectConfig,
|
||||
eligibleModels,
|
||||
isLive,
|
||||
onChangeMode,
|
||||
onRevertToLive,
|
||||
onProjectToolsUpdated,
|
||||
}: {
|
||||
projectId: string;
|
||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||
|
|
@ -592,10 +605,12 @@ export function WorkflowEditor({
|
|||
toolWebhookUrl: string;
|
||||
defaultModel: string;
|
||||
projectTools: z.infer<typeof WorkflowTool>[];
|
||||
projectConfig: z.infer<typeof Project>;
|
||||
eligibleModels: z.infer<typeof ModelsResponse> | "*";
|
||||
isLive: boolean;
|
||||
onChangeMode: (mode: 'draft' | 'live') => void;
|
||||
onRevertToLive: () => void;
|
||||
onProjectToolsUpdated?: () => void;
|
||||
}) {
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
|
|
@ -615,6 +630,12 @@ export function WorkflowEditor({
|
|||
isLive,
|
||||
}
|
||||
});
|
||||
|
||||
// Sync workflow prop changes with reducer state (e.g., when composio tools are updated)
|
||||
useEffect(() => {
|
||||
dispatch({ type: "sync_workflow", workflow });
|
||||
}, [workflow]);
|
||||
|
||||
const [chatMessages, setChatMessages] = useState<z.infer<typeof Message>[]>([]);
|
||||
const updateChatMessages = useCallback((messages: z.infer<typeof Message>[]) => {
|
||||
setChatMessages(messages);
|
||||
|
|
@ -980,6 +1001,7 @@ export function WorkflowEditor({
|
|||
tools={state.present.workflow.tools}
|
||||
projectTools={projectTools}
|
||||
prompts={state.present.workflow.prompts}
|
||||
workflow={state.present.workflow}
|
||||
selectedEntity={
|
||||
state.present.selection &&
|
||||
(state.present.selection.type === "agent" ||
|
||||
|
|
@ -1002,6 +1024,8 @@ export function WorkflowEditor({
|
|||
onDeletePrompt={handleDeletePrompt}
|
||||
onShowVisualise={handleShowVisualise}
|
||||
projectId={projectId}
|
||||
onProjectToolsUpdated={onProjectToolsUpdated}
|
||||
projectConfig={projectConfig}
|
||||
onReorderAgents={handleReorderAgents}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ import {
|
|||
ChevronRightIcon,
|
||||
Moon,
|
||||
Sun,
|
||||
HelpCircle,
|
||||
Wrench
|
||||
HelpCircle
|
||||
} from "lucide-react";
|
||||
import { getProjectConfig } from "@/app/actions/project_actions";
|
||||
import { useTheme } from "@/app/providers/theme-provider";
|
||||
|
|
@ -69,13 +68,6 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
|||
icon: DatabaseIcon,
|
||||
requiresProject: true
|
||||
}] : []),
|
||||
{
|
||||
href: 'tools',
|
||||
label: 'Tools',
|
||||
icon: Wrench,
|
||||
requiresProject: true,
|
||||
beta: true
|
||||
},
|
||||
{
|
||||
href: 'config',
|
||||
label: 'Settings',
|
||||
|
|
@ -162,14 +154,7 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
|||
`}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span>{item.label}</span>
|
||||
{item.beta && (
|
||||
<span className="ml-1.5 leading-none px-1.5 py-[2px] text-[9px] font-medium bg-linear-to-r from-pink-500 to-violet-500 text-white rounded-full">
|
||||
BETA
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
<span>{item.label}</span>
|
||||
)}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
|
|
|
|||
57
apps/rowboat/package-lock.json
generated
57
apps/rowboat/package-lock.json
generated
|
|
@ -43,7 +43,7 @@
|
|||
"ioredis": "^5.6.1",
|
||||
"jose": "^5.9.6",
|
||||
"lucide-react": "^0.465.0",
|
||||
"mermaid": "^11.8.1",
|
||||
"mermaid": "^11.9.0",
|
||||
"mongodb": "^6.8.0",
|
||||
"next": "15.3.4",
|
||||
"openai": "^4.67.2",
|
||||
|
|
@ -1396,7 +1396,6 @@
|
|||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
|
||||
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
"@chevrotain/types": "11.0.3",
|
||||
|
|
@ -1407,7 +1406,6 @@
|
|||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
|
||||
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/types": "11.0.3",
|
||||
"lodash-es": "4.17.21"
|
||||
|
|
@ -1416,20 +1414,17 @@
|
|||
"node_modules/@chevrotain/regexp-to-ast": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
|
||||
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
|
||||
"license": "Apache-2.0"
|
||||
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="
|
||||
},
|
||||
"node_modules/@chevrotain/types": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
|
||||
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
|
||||
"license": "Apache-2.0"
|
||||
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="
|
||||
},
|
||||
"node_modules/@chevrotain/utils": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
|
||||
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
|
||||
"license": "Apache-2.0"
|
||||
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="
|
||||
},
|
||||
"node_modules/@composio/client": {
|
||||
"version": "0.1.0-alpha.26",
|
||||
|
|
@ -4582,10 +4577,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mermaid-js/parser": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.1.tgz",
|
||||
"integrity": "sha512-lCQNpV8R4lgsGcjX5667UiuDLk2micCtjtxR1YKbBXvN5w2v+FeLYoHrTSSrjwXdMcDYvE4ZBPvKT31dfeSmmA==",
|
||||
"license": "MIT",
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
|
||||
"integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==",
|
||||
"dependencies": {
|
||||
"langium": "3.3.1"
|
||||
}
|
||||
|
|
@ -9222,7 +9216,6 @@
|
|||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||
"@chevrotain/gast": "11.0.3",
|
||||
|
|
@ -9236,7 +9229,6 @@
|
|||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
|
||||
"integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
|
|
@ -12928,7 +12920,6 @@
|
|||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
|
||||
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chevrotain": "~11.0.3",
|
||||
"chevrotain-allstar": "~0.3.0",
|
||||
|
|
@ -13440,15 +13431,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-16.1.0.tgz",
|
||||
"integrity": "sha512-Me7BNa1aqrxVinDnFfvCgHh2yHvLbFvILBs899MhuBpbE5VPzpSqv7alaESfkqkgc9JNvUGH4gqwZeOzLnY8Jg==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
|
|
@ -13781,14 +13771,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.8.1.tgz",
|
||||
"integrity": "sha512-VSXJLqP1Sqw5sGr273mhvpPRhXwE6NlmMSqBZQw+yZJoAJkOIPPn/uT3teeCBx60Fkt5zEI3FrH2eVT0jXRDzw==",
|
||||
"license": "MIT",
|
||||
"version": "11.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.9.0.tgz",
|
||||
"integrity": "sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
"@iconify/utils": "^2.1.33",
|
||||
"@mermaid-js/parser": "^0.6.1",
|
||||
"@mermaid-js/parser": "^0.6.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.3",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
|
|
@ -13798,10 +13787,10 @@
|
|||
"dagre-d3-es": "7.0.11",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.9",
|
||||
"katex": "^0.16.22",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.7",
|
||||
"marked": "^16.0.0",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.6",
|
||||
"ts-dedent": "^2.2.0",
|
||||
|
|
@ -17797,7 +17786,6 @@
|
|||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
|
|
@ -17806,7 +17794,6 @@
|
|||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
|
||||
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-languageserver-protocol": "3.17.5"
|
||||
},
|
||||
|
|
@ -17818,7 +17805,6 @@
|
|||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-jsonrpc": "8.2.0",
|
||||
"vscode-languageserver-types": "3.17.5"
|
||||
|
|
@ -17827,20 +17813,17 @@
|
|||
"node_modules/vscode-languageserver-textdocument": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
|
||||
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="
|
||||
},
|
||||
"node_modules/vscode-languageserver-types": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
"ioredis": "^5.6.1",
|
||||
"jose": "^5.9.6",
|
||||
"lucide-react": "^0.465.0",
|
||||
"mermaid": "^11.8.1",
|
||||
"mermaid": "^11.9.0",
|
||||
"mongodb": "^6.8.0",
|
||||
"next": "15.3.4",
|
||||
"openai": "^4.67.2",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue