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 {
|
import {
|
||||||
listToolkits as libListToolkits,
|
listToolkits as libListToolkits,
|
||||||
listTools as libListTools,
|
listTools as libListTools,
|
||||||
|
searchTools as libSearchTools,
|
||||||
|
getToolsByIds as libGetToolsByIds,
|
||||||
|
getTool as libGetTool,
|
||||||
getConnectedAccount as libGetConnectedAccount,
|
getConnectedAccount as libGetConnectedAccount,
|
||||||
deleteConnectedAccount as libDeleteConnectedAccount,
|
deleteConnectedAccount as libDeleteConnectedAccount,
|
||||||
listAuthConfigs as libListAuthConfigs,
|
listAuthConfigs as libListAuthConfigs,
|
||||||
|
|
@ -20,6 +23,7 @@ import {
|
||||||
ZCredentials,
|
ZCredentials,
|
||||||
} from "@/app/lib/composio/composio";
|
} from "@/app/lib/composio/composio";
|
||||||
import { ComposioConnectedAccount } from "@/app/lib/types/project_types";
|
import { ComposioConnectedAccount } from "@/app/lib/types/project_types";
|
||||||
|
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||||
import { getProjectConfig, projectAuthCheck } from "./project_actions";
|
import { getProjectConfig, projectAuthCheck } from "./project_actions";
|
||||||
import { projectsCollection } from "../lib/mongodb";
|
import { projectsCollection } from "../lib/mongodb";
|
||||||
|
|
||||||
|
|
@ -47,6 +51,24 @@ export async function listTools(projectId: string, toolkitSlug: string, cursor:
|
||||||
return await libListTools(toolkitSlug, 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>> {
|
export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
|
||||||
await projectAuthCheck(projectId);
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
|
|
@ -215,12 +237,196 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str
|
||||||
const key = `composioConnectedAccounts.${toolkitSlug}`;
|
const key = `composioConnectedAccounts.${toolkitSlug}`;
|
||||||
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
|
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;
|
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> {
|
export async function updateComposioSelectedTools(projectId: string, tools: z.infer<typeof ZTool>[]): Promise<void> {
|
||||||
await projectAuthCheck(projectId);
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
// update project with new selected tools
|
// Get the project to access draft workflow
|
||||||
await projectsCollection.updateOne({ _id: projectId }, { $set: { composioSelectedTools: tools } });
|
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 { Workflow } from "../lib/types/workflow_types";
|
||||||
import { WorkflowTool } from "../lib/types/workflow_types";
|
import { WorkflowTool } from "../lib/types/workflow_types";
|
||||||
import { collectProjectTools as libCollectProjectTools } from "../lib/project_tools";
|
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 || '';
|
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
||||||
|
|
||||||
|
|
@ -306,6 +313,113 @@ export async function createProjectFromPrompt(formData: FormData): Promise<{ id:
|
||||||
return { id: projectId };
|
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 }> {
|
export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> {
|
||||||
const user = await authCheck();
|
const user = await authCheck();
|
||||||
const workflowJson = formData.get('workflowJson') as string;
|
const workflowJson = formData.get('workflowJson') as string;
|
||||||
|
|
@ -327,6 +441,15 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
const projectId = response.id;
|
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 };
|
return { id: projectId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { dataSourceDocsCollection, dataSourcesCollection, projectsCollection } f
|
||||||
import { qdrantClient } from '../lib/qdrant';
|
import { qdrantClient } from '../lib/qdrant';
|
||||||
import { EmbeddingRecord } from "./types/datasource_types";
|
import { EmbeddingRecord } from "./types/datasource_types";
|
||||||
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPrompt, WorkflowTool } from "./types/workflow_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 { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions";
|
||||||
import { PrefixLogger } from "./utils";
|
import { PrefixLogger } from "./utils";
|
||||||
import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types";
|
import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "./types/types";
|
||||||
|
|
@ -280,6 +281,8 @@ async function invokeComposioTool(
|
||||||
name: string,
|
name: string,
|
||||||
composioData: z.infer<typeof WorkflowTool>['composioData'] & {},
|
composioData: z.infer<typeof WorkflowTool>['composioData'] & {},
|
||||||
input: any,
|
input: any,
|
||||||
|
workflow: z.infer<typeof Workflow>,
|
||||||
|
toolDescription?: string,
|
||||||
) {
|
) {
|
||||||
logger = logger.child(`invokeComposioTool`);
|
logger = logger.child(`invokeComposioTool`);
|
||||||
logger.log(`projectId: ${projectId}`);
|
logger.log(`projectId: ${projectId}`);
|
||||||
|
|
@ -288,12 +291,36 @@ async function invokeComposioTool(
|
||||||
|
|
||||||
const { slug, toolkitSlug, noAuth } = composioData;
|
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;
|
let connectedAccountId: string | undefined = undefined;
|
||||||
if (!noAuth) {
|
if (!noAuth) {
|
||||||
const project = await projectsCollection.findOne({ _id: projectId });
|
|
||||||
if (!project) {
|
|
||||||
throw new Error(`project ${projectId} not found`);
|
|
||||||
}
|
|
||||||
connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id;
|
connectedAccountId = project.composioConnectedAccounts?.[toolkitSlug]?.id;
|
||||||
if (!connectedAccountId) {
|
if (!connectedAccountId) {
|
||||||
throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`);
|
throw new Error(`connected account id not found for project ${projectId} and toolkit ${toolkitSlug}`);
|
||||||
|
|
@ -452,7 +479,8 @@ function createMcpTool(
|
||||||
function createComposioTool(
|
function createComposioTool(
|
||||||
logger: PrefixLogger,
|
logger: PrefixLogger,
|
||||||
config: z.infer<typeof WorkflowTool>,
|
config: z.infer<typeof WorkflowTool>,
|
||||||
projectId: string
|
projectId: string,
|
||||||
|
workflow: z.infer<typeof Workflow>
|
||||||
): Tool {
|
): Tool {
|
||||||
const { name, description, parameters, composioData } = config;
|
const { name, description, parameters, composioData } = config;
|
||||||
|
|
||||||
|
|
@ -472,7 +500,7 @@ function createComposioTool(
|
||||||
},
|
},
|
||||||
async execute(input: any) {
|
async execute(input: any) {
|
||||||
try {
|
try {
|
||||||
const result = await invokeComposioTool(logger, projectId, name, composioData, input);
|
const result = await invokeComposioTool(logger, projectId, name, composioData, input, workflow, description);
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
result,
|
result,
|
||||||
});
|
});
|
||||||
|
|
@ -813,7 +841,7 @@ function createTools(
|
||||||
tools[toolName] = createMcpTool(logger, config, projectId);
|
tools[toolName] = createMcpTool(logger, config, projectId);
|
||||||
logger.log(`created mcp tool: ${toolName}`);
|
logger.log(`created mcp tool: ${toolName}`);
|
||||||
} else if (config.isComposio) {
|
} else if (config.isComposio) {
|
||||||
tools[toolName] = createComposioTool(logger, config, projectId);
|
tools[toolName] = createComposioTool(logger, config, projectId, workflow);
|
||||||
logger.log(`created composio tool: ${toolName}`);
|
logger.log(`created composio tool: ${toolName}`);
|
||||||
} else if (config.mockTool) {
|
} else if (config.mockTool) {
|
||||||
tools[toolName] = createMockTool(logger, config);
|
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());
|
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>>>> {
|
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`);
|
const url = new URL(`${BASE_URL}/auth_configs`);
|
||||||
url.searchParams.set("toolkit_slug", toolkitSlug);
|
url.searchParams.set("toolkit_slug", toolkitSlug);
|
||||||
|
|
|
||||||
|
|
@ -33,28 +33,8 @@ export async function collectProjectTools(projectId: string): Promise<z.infer<ty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Composio tools
|
// Note: Composio tools are now stored in workflow.tools array with isComposio: true
|
||||||
if (project.composioSelectedTools) {
|
// This function now only collects MCP tools since composio tools are managed in workflow
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { MCPServer } from "./types";
|
import { MCPServer } from "./types";
|
||||||
import { Workflow, WorkflowTool } from "./workflow_types";
|
import { Workflow, WorkflowTool } from "./workflow_types";
|
||||||
import { ZTool } from "../composio/composio";
|
|
||||||
|
|
||||||
export const ComposioConnectedAccount = z.object({
|
export const ComposioConnectedAccount = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|
@ -30,7 +29,6 @@ export const Project = z.object({
|
||||||
testRunCounter: z.number().default(0),
|
testRunCounter: z.number().default(0),
|
||||||
mcpServers: z.array(MCPServer).optional(),
|
mcpServers: z.array(MCPServer).optional(),
|
||||||
composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(),
|
composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(),
|
||||||
composioSelectedTools: z.array(ZTool).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ProjectMember = z.object({
|
export const ProjectMember = z.object({
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,14 @@ export const Workflow = z.object({
|
||||||
startAgent: z.string(),
|
startAgent: z.string(),
|
||||||
lastUpdatedAt: z.string().datetime(),
|
lastUpdatedAt: z.string().datetime(),
|
||||||
mockTools: z.record(z.string(), z.string()).optional(), // a dict of toolName => mockInstructions
|
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
|
export const WorkflowTemplate = Workflow
|
||||||
.omit({
|
.omit({
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Info, RefreshCw, Search } from 'lucide-react';
|
import { Info, RefreshCw, Search } from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
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 { getProjectConfig } from '@/app/actions/project_actions';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio';
|
import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio';
|
||||||
|
|
@ -30,6 +30,7 @@ export function Composio() {
|
||||||
const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null);
|
const [selectedToolkit, setSelectedToolkit] = useState<ToolkitType | null>(null);
|
||||||
const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false);
|
const [isToolsPanelOpen, setIsToolsPanelOpen] = useState(false);
|
||||||
const [savingTools, setSavingTools] = useState(false);
|
const [savingTools, setSavingTools] = useState(false);
|
||||||
|
const [composioSelectedTools, setComposioSelectedTools] = useState<z.infer<typeof ZTool>[]>([]);
|
||||||
|
|
||||||
const loadProjectConfig = useCallback(async () => {
|
const loadProjectConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -41,6 +42,15 @@ export function Composio() {
|
||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [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 () => {
|
const loadAllToolkits = useCallback(async () => {
|
||||||
let cursor: string | null = null;
|
let cursor: string | null = null;
|
||||||
let allToolkits: ToolkitType[] = [];
|
let allToolkits: ToolkitType[] = [];
|
||||||
|
|
@ -87,15 +97,16 @@ export function Composio() {
|
||||||
|
|
||||||
const handleProjectConfigUpdate = useCallback(() => {
|
const handleProjectConfigUpdate = useCallback(() => {
|
||||||
loadProjectConfig();
|
loadProjectConfig();
|
||||||
}, [loadProjectConfig]);
|
loadComposioSelectedTools();
|
||||||
|
}, [loadProjectConfig, loadComposioSelectedTools]);
|
||||||
|
|
||||||
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
|
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
setSavingTools(true);
|
setSavingTools(true);
|
||||||
try {
|
try {
|
||||||
// Get existing selected tools from project config
|
// Get existing selected tools from workflow
|
||||||
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
|
const existingSelectedTools = composioSelectedTools;
|
||||||
|
|
||||||
// Create a map of existing tools by slug for easy lookup
|
// Create a map of existing tools by slug for easy lookup
|
||||||
const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool]));
|
const existingToolsMap = new Map(existingSelectedTools.map(tool => [tool.slug, tool]));
|
||||||
|
|
@ -110,22 +121,22 @@ export function Composio() {
|
||||||
|
|
||||||
await updateComposioSelectedTools(projectId, mergedSelectedTools);
|
await updateComposioSelectedTools(projectId, mergedSelectedTools);
|
||||||
|
|
||||||
// Refresh project config to get updated data
|
// Refresh data to get updated tools
|
||||||
await loadProjectConfig();
|
await loadComposioSelectedTools();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving tool selection:', error);
|
console.error('Error saving tool selection:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setSavingTools(false);
|
setSavingTools(false);
|
||||||
}
|
}
|
||||||
}, [projectId, projectConfig, loadProjectConfig]);
|
}, [projectId, composioSelectedTools, loadComposioSelectedTools]);
|
||||||
|
|
||||||
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
|
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
setSavingTools(true);
|
setSavingTools(true);
|
||||||
try {
|
try {
|
||||||
// Get existing selected tools from project config
|
// Get existing selected tools from workflow
|
||||||
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
|
const existingSelectedTools = composioSelectedTools;
|
||||||
|
|
||||||
// Filter out all tools from the specified toolkit
|
// Filter out all tools from the specified toolkit
|
||||||
const filteredSelectedTools = existingSelectedTools.filter(tool =>
|
const filteredSelectedTools = existingSelectedTools.filter(tool =>
|
||||||
|
|
@ -134,18 +145,19 @@ export function Composio() {
|
||||||
|
|
||||||
await updateComposioSelectedTools(projectId, filteredSelectedTools);
|
await updateComposioSelectedTools(projectId, filteredSelectedTools);
|
||||||
|
|
||||||
// Refresh project config to get updated data
|
// Refresh data to get updated tools
|
||||||
await loadProjectConfig();
|
await loadComposioSelectedTools();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error removing toolkit tools:', error);
|
console.error('Error removing toolkit tools:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setSavingTools(false);
|
setSavingTools(false);
|
||||||
}
|
}
|
||||||
}, [projectId, projectConfig, loadProjectConfig]);
|
}, [projectId, composioSelectedTools, loadComposioSelectedTools]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProjectConfig();
|
loadProjectConfig();
|
||||||
}, [loadProjectConfig]);
|
loadComposioSelectedTools();
|
||||||
|
}, [loadProjectConfig, loadComposioSelectedTools]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAllToolkits();
|
loadAllToolkits();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useParams } from 'next/navigation';
|
||||||
import { PictureImg } from '@/components/ui/picture-img';
|
import { PictureImg } from '@/components/ui/picture-img';
|
||||||
import { Button, Checkbox } from '@heroui/react';
|
import { Button, Checkbox } from '@heroui/react';
|
||||||
import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-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 { z } from 'zod';
|
||||||
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
||||||
import { SlidePanel } from '@/components/ui/slide-panel';
|
import { SlidePanel } from '@/components/ui/slide-panel';
|
||||||
|
|
@ -57,6 +57,7 @@ export function ComposioToolsPanel({
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||||
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
|
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
|
||||||
|
const [composioSelectedTools, setComposioSelectedTools] = useState<ToolType[]>([]);
|
||||||
|
|
||||||
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
|
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -80,6 +81,15 @@ export function ComposioToolsPanel({
|
||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [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 () => {
|
const handleNextPage = useCallback(async () => {
|
||||||
if (!nextCursor || !toolkit) return;
|
if (!nextCursor || !toolkit) return;
|
||||||
|
|
||||||
|
|
@ -164,14 +174,21 @@ export function ComposioToolsPanel({
|
||||||
}
|
}
|
||||||
}, [onClose, hasChanges]);
|
}, [onClose, hasChanges]);
|
||||||
|
|
||||||
// Initialize selected tools from project config when opening the panel
|
// Initialize selected tools from workflow when opening the panel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (toolkit && isOpen && projectConfig?.composioSelectedTools) {
|
if (toolkit && isOpen) {
|
||||||
const toolSlugs = new Set(projectConfig.composioSelectedTools.map(tool => tool.slug));
|
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);
|
setSelectedTools(toolSlugs);
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
}
|
}
|
||||||
}, [toolkit, isOpen, projectConfig]);
|
}, [toolkit, composioSelectedTools]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (toolkit && isOpen) {
|
if (toolkit && isOpen) {
|
||||||
|
|
@ -210,44 +227,46 @@ export function ComposioToolsPanel({
|
||||||
<div className={`mb-6 p-4 rounded-lg border-2 ${
|
<div className={`mb-6 p-4 rounded-lg border-2 ${
|
||||||
isToolkitConnected
|
isToolkitConnected
|
||||||
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'
|
? '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 justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-3 h-3 rounded-full ${
|
<div className={`w-3 h-3 rounded-full ${
|
||||||
isToolkitConnected ? 'bg-emerald-500' : 'bg-orange-500'
|
isToolkitConnected ? 'bg-emerald-500' : 'bg-blue-500'
|
||||||
}`}></div>
|
}`}></div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className={`font-semibold text-sm ${
|
<h3 className={`font-semibold text-sm ${
|
||||||
isToolkitConnected
|
isToolkitConnected
|
||||||
? 'text-emerald-800 dark:text-emerald-200'
|
? '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'}
|
{isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={`text-xs mt-0.5 ${
|
<p className={`text-xs mt-0.5 ${
|
||||||
isToolkitConnected
|
isToolkitConnected
|
||||||
? 'text-emerald-700 dark:text-emerald-300'
|
? 'text-emerald-700 dark:text-emerald-300'
|
||||||
: 'text-orange-700 dark:text-orange-300'
|
: 'text-blue-700 dark:text-blue-300'
|
||||||
}`}>
|
}`}>
|
||||||
{isToolkitConnected
|
{isToolkitConnected
|
||||||
? 'You can select and use tools from this toolkit'
|
? '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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{isToolkitConnected && (
|
||||||
variant="solid"
|
<Button
|
||||||
size="sm"
|
variant="solid"
|
||||||
onPress={isToolkitConnected ? handleDisconnect : handleConnect}
|
size="sm"
|
||||||
disabled={isProcessingAuth}
|
onPress={handleDisconnect}
|
||||||
color={isToolkitConnected ? "danger" : "primary"}
|
disabled={isProcessingAuth}
|
||||||
isLoading={isProcessingAuth}
|
color="danger"
|
||||||
startContent={isToolkitConnected ? <UnlinkIcon className="h-4 w-4" /> : <LinkIcon className="h-4 w-4" />}
|
isLoading={isProcessingAuth}
|
||||||
>
|
startContent={<UnlinkIcon className="h-4 w-4" />}
|
||||||
{isToolkitConnected ? 'Disconnect' : 'Connect Now'}
|
>
|
||||||
</Button>
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -263,7 +282,7 @@ export function ComposioToolsPanel({
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={handleSaveTools}
|
onPress={handleSaveTools}
|
||||||
disabled={isSaving || !isToolkitConnected}
|
disabled={isSaving}
|
||||||
isLoading={isSaving}
|
isLoading={isSaving}
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
|
|
@ -283,17 +302,12 @@ export function ComposioToolsPanel({
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{tools.map((tool) => (
|
{tools.map((tool) => (
|
||||||
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${
|
<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">
|
||||||
isToolkitConnected
|
|
||||||
? 'bg-gray-50/50 dark:bg-gray-800/50 hover:bg-gray-100/50 dark:hover:bg-gray-700/50 hover:border-gray-200 dark:hover:border-gray-600'
|
|
||||||
: 'bg-gray-100/50 dark:bg-gray-900/50 opacity-60'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isSelected={selectedTools.has(tool.slug)}
|
isSelected={selectedTools.has(tool.slug)}
|
||||||
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
|
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!isToolkitConnected}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-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 McpServerType = z.infer<typeof MCPServer>;
|
||||||
type McpToolType = z.infer<typeof MCPServer>['tools'][number];
|
type McpToolType = z.infer<typeof MCPServer>['tools'][number];
|
||||||
|
|
||||||
export function CustomServers() {
|
export function CustomServers({ onToolsUpdated }: { onToolsUpdated?: () => void }) {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
|
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
|
||||||
if (!projectId) throw new Error('Project ID is required');
|
if (!projectId) throw new Error('Project ID is required');
|
||||||
|
|
@ -92,6 +92,9 @@ export function CustomServers() {
|
||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Notify parent component about tool updates
|
||||||
|
onToolsUpdated?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Toggle failed:', { server: server.name, error: err });
|
console.error('Toggle failed:', { server: server.name, error: err });
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -161,6 +164,9 @@ export function CustomServers() {
|
||||||
// Update selectedTools to include all tools for the custom server
|
// Update selectedTools to include all tools for the custom server
|
||||||
setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id)));
|
setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify parent component about tool updates
|
||||||
|
onToolsUpdated?.();
|
||||||
} finally {
|
} finally {
|
||||||
setSyncingServers(prev => {
|
setSyncingServers(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|
@ -207,6 +213,9 @@ export function CustomServers() {
|
||||||
|
|
||||||
// Fetch tools for the new server using the formatted URL
|
// Fetch tools for the new server using the formatted URL
|
||||||
await handleSyncServer(formattedServer);
|
await handleSyncServer(formattedServer);
|
||||||
|
|
||||||
|
// Notify parent component about tool updates
|
||||||
|
onToolsUpdated?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error adding server:', err);
|
console.error('Error adding server:', err);
|
||||||
setError('Failed to add server. Please try again.');
|
setError('Failed to add server. Please try again.');
|
||||||
|
|
@ -229,6 +238,9 @@ export function CustomServers() {
|
||||||
if (selectedServer?.name === server.name) {
|
if (selectedServer?.name === server.name) {
|
||||||
setSelectedServer(null);
|
setSelectedServer(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify parent component about tool updates
|
||||||
|
onToolsUpdated?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error removing server:', err);
|
console.error('Error removing server:', err);
|
||||||
setError('Failed to remove server. Please try again.');
|
setError('Failed to remove server. Please try again.');
|
||||||
|
|
@ -271,6 +283,9 @@ export function CustomServers() {
|
||||||
});
|
});
|
||||||
|
|
||||||
setHasToolChanges(false);
|
setHasToolChanges(false);
|
||||||
|
|
||||||
|
// Notify parent component about tool updates
|
||||||
|
onToolsUpdated?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving tool selection:', error);
|
console.error('Error saving tool selection:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,8 @@ export function ToolkitCard({
|
||||||
}, [onManageTools]);
|
}, [onManageTools]);
|
||||||
|
|
||||||
// Calculate selected tools count for this toolkit
|
// Calculate selected tools count for this toolkit
|
||||||
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
|
// TODO: Update to use workflow-based tools count
|
||||||
tool.toolkit.slug === toolkit.slug
|
const selectedToolsCount = 0;
|
||||||
).length || 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={toolkitCardStyles.base} onClick={handleCardClick}>
|
<div className={toolkitCardStyles.base} onClick={handleCardClick}>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export default async function ToolsPage() {
|
||||||
<div className="flex-1 p-6">
|
<div className="flex-1 p-6">
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<ToolsConfig
|
<ToolsConfig
|
||||||
useComposioTools={USE_COMPOSIO_TOOLS}
|
useComposioTools={false}
|
||||||
useKlavisTools={USE_KLAVIS_TOOLS}
|
useKlavisTools={USE_KLAVIS_TOOLS}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { MCPServer, WithStringId } from "../../../lib/types/types";
|
import { MCPServer, WithStringId } from "../../../lib/types/types";
|
||||||
import { DataSource } from "../../../lib/types/datasource_types";
|
import { DataSource } from "../../../lib/types/datasource_types";
|
||||||
|
import { Project } from "../../../lib/types/project_types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { WorkflowEditor } from "./workflow_editor";
|
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 { Workflow, WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||||
import { getEligibleModels } from "@/app/actions/billing_actions";
|
import { getEligibleModels } from "@/app/actions/billing_actions";
|
||||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
||||||
import { Project } from "@/app/lib/types/project_types";
|
|
||||||
|
|
||||||
export function App({
|
export function App({
|
||||||
projectId,
|
projectId,
|
||||||
|
|
@ -26,6 +26,7 @@ export function App({
|
||||||
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null);
|
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null);
|
||||||
const [dataSources, setDataSources] = useState<WithStringId<z.infer<typeof DataSource>>[] | null>(null);
|
const [dataSources, setDataSources] = useState<WithStringId<z.infer<typeof DataSource>>[] | null>(null);
|
||||||
const [projectTools, setProjectTools] = useState<z.infer<typeof WorkflowTool>[] | 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 [loading, setLoading] = useState(false);
|
||||||
const [eligibleModels, setEligibleModels] = useState<z.infer<typeof ModelsResponse> | "*">("*");
|
const [eligibleModels, setEligibleModels] = useState<z.infer<typeof ModelsResponse> | "*">("*");
|
||||||
const [projectMcpServers, setProjectMcpServers] = useState<Array<z.infer<typeof MCPServer>>>([]);
|
const [projectMcpServers, setProjectMcpServers] = useState<Array<z.infer<typeof MCPServer>>>([]);
|
||||||
|
|
@ -66,11 +67,70 @@ export function App({
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [projectId]);
|
}, [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
|
// Add this useEffect for initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
}, [mode, loadData, projectId]);
|
}, [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') {
|
function handleSetMode(mode: 'draft' | 'live') {
|
||||||
setMode(mode);
|
setMode(mode);
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +155,7 @@ export function App({
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
dataSources={dataSources}
|
dataSources={dataSources}
|
||||||
projectTools={projectTools}
|
projectTools={projectTools}
|
||||||
|
projectConfig={projectConfig || project}
|
||||||
useRag={useRag}
|
useRag={useRag}
|
||||||
mcpServerUrls={projectMcpServers}
|
mcpServerUrls={projectMcpServers}
|
||||||
toolWebhookUrl={webhookUrl}
|
toolWebhookUrl={webhookUrl}
|
||||||
|
|
@ -102,6 +163,7 @@ export function App({
|
||||||
eligibleModels={eligibleModels}
|
eligibleModels={eligibleModels}
|
||||||
onChangeMode={handleSetMode}
|
onChangeMode={handleSetMode}
|
||||||
onRevertToLive={handleRevertToLive}
|
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 { 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 { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react";
|
||||||
import { useRef, useEffect, useState } from "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 { DndContext, DragEndEvent, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||||
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
|
@ -13,6 +15,9 @@ import { clsx } from "clsx";
|
||||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
|
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
|
||||||
import { ServerLogo } from '../tools/components/MCPServersCommon';
|
import { ServerLogo } from '../tools/components/MCPServersCommon';
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/react";
|
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
|
// Reduced gap size to match Cursor's UI
|
||||||
const GAP_SIZE = 4; // 1 unit * 4px (tailwind's default spacing unit)
|
const GAP_SIZE = 4; // 1 unit * 4px (tailwind's default spacing unit)
|
||||||
|
|
@ -35,6 +40,7 @@ interface EntityListProps {
|
||||||
tools: z.infer<typeof WorkflowTool>[];
|
tools: z.infer<typeof WorkflowTool>[];
|
||||||
projectTools: z.infer<typeof WorkflowTool>[];
|
projectTools: z.infer<typeof WorkflowTool>[];
|
||||||
prompts: z.infer<typeof WorkflowPrompt>[];
|
prompts: z.infer<typeof WorkflowPrompt>[];
|
||||||
|
workflow: z.infer<typeof Workflow>;
|
||||||
selectedEntity: {
|
selectedEntity: {
|
||||||
type: "agent" | "tool" | "prompt" | "visualise";
|
type: "agent" | "tool" | "prompt" | "visualise";
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -52,6 +58,8 @@ interface EntityListProps {
|
||||||
onDeleteTool: (name: string) => void;
|
onDeleteTool: (name: string) => void;
|
||||||
onDeletePrompt: (name: string) => void;
|
onDeletePrompt: (name: string) => void;
|
||||||
onShowVisualise: (name: string) => void;
|
onShowVisualise: (name: string) => void;
|
||||||
|
onProjectToolsUpdated?: () => void;
|
||||||
|
projectConfig?: z.infer<typeof Project>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmptyStateProps {
|
interface EmptyStateProps {
|
||||||
|
|
@ -95,7 +103,7 @@ const ListItemWithMenu = ({
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<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,
|
"bg-indigo-50 dark:bg-indigo-950/30": isSelected,
|
||||||
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !isSelected
|
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !isSelected
|
||||||
|
|
@ -116,22 +124,20 @@ const ListItemWithMenu = ({
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
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 ? (
|
{mcpServerName ? (
|
||||||
<ServerLogo
|
<ServerLogo
|
||||||
serverName={mcpServerName}
|
serverName={mcpServerName}
|
||||||
className="h-4 w-4"
|
className="h-3 w-3"
|
||||||
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />}
|
fallback={<ImportIcon className="w-3 h-3 text-blue-600 dark:text-blue-500" />}
|
||||||
/>
|
/>
|
||||||
) : icon}
|
) : icon}
|
||||||
</div>
|
</div>
|
||||||
{name}
|
<span className="text-xs">{name}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1">
|
||||||
{statusLabel}
|
{statusLabel}
|
||||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
{menuContent}
|
||||||
{menuContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -166,43 +172,56 @@ const ServerCard = ({
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-1 group">
|
||||||
<button
|
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
<button
|
||||||
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"
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px]"
|
||||||
{isExpanded ? (
|
>
|
||||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
{/* Chevron - only show when has tools and on hover */}
|
||||||
) : (
|
<div className={`w-4 h-4 flex items-center justify-center transition-opacity ${
|
||||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
tools.length > 0 ? 'group-hover:opacity-100 opacity-60' : 'opacity-0'
|
||||||
)}
|
}`}>
|
||||||
<div className="flex items-center gap-1">
|
{tools.length > 0 && (isExpanded ? (
|
||||||
<ServerLogo
|
<ChevronDown className="w-3 h-3 text-gray-500" />
|
||||||
serverName={serverName}
|
) : (
|
||||||
className="h-4 w-4"
|
<ChevronRight className="w-3 h-3 text-gray-500" />
|
||||||
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />}
|
))}
|
||||||
/>
|
</div>
|
||||||
<span>{serverName}</span>
|
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
</button>
|
<ServerLogo
|
||||||
{isExpanded && (
|
serverName={serverName}
|
||||||
<div className="ml-6 mt-1 space-y-1">
|
className="h-4 w-4"
|
||||||
{tools.map((tool, index) => (
|
fallback={<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />}
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -222,6 +241,7 @@ export function EntityList({
|
||||||
tools,
|
tools,
|
||||||
projectTools,
|
projectTools,
|
||||||
prompts,
|
prompts,
|
||||||
|
workflow,
|
||||||
selectedEntity,
|
selectedEntity,
|
||||||
startAgentName,
|
startAgentName,
|
||||||
onSelectAgent,
|
onSelectAgent,
|
||||||
|
|
@ -235,7 +255,9 @@ export function EntityList({
|
||||||
onDeleteAgent,
|
onDeleteAgent,
|
||||||
onDeleteTool,
|
onDeleteTool,
|
||||||
onDeletePrompt,
|
onDeletePrompt,
|
||||||
|
onProjectToolsUpdated,
|
||||||
projectId,
|
projectId,
|
||||||
|
projectConfig,
|
||||||
onReorderAgents,
|
onReorderAgents,
|
||||||
onShowVisualise,
|
onShowVisualise,
|
||||||
}: EntityListProps & {
|
}: EntityListProps & {
|
||||||
|
|
@ -243,6 +265,7 @@ export function EntityList({
|
||||||
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
|
onReorderAgents: (agents: z.infer<typeof WorkflowAgent>[]) => void
|
||||||
}) {
|
}) {
|
||||||
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
||||||
|
const [showComposioToolsModal, setShowComposioToolsModal] = useState(false);
|
||||||
|
|
||||||
const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => {
|
const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => {
|
||||||
onAddAgent({
|
onAddAgent({
|
||||||
|
|
@ -496,20 +519,36 @@ export function EntityList({
|
||||||
<Wrench className="w-4 h-4" />
|
<Wrench className="w-4 h-4" />
|
||||||
<span>Tools</span>
|
<span>Tools</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="secondary"
|
<Button
|
||||||
size="sm"
|
variant="secondary"
|
||||||
onClick={(e) => {
|
size="sm"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
setExpandedPanels(prev => ({ ...prev, tools: true }));
|
e.stopPropagation();
|
||||||
onAddTool({});
|
setExpandedPanels(prev => ({ ...prev, tools: true }));
|
||||||
}}
|
setShowComposioToolsModal(true);
|
||||||
className={`group ${buttonClasses}`}
|
}}
|
||||||
showHoverContent={true}
|
className={`group ${buttonClasses}`}
|
||||||
hoverContent="Add Tool"
|
showHoverContent={true}
|
||||||
>
|
hoverContent="Composio Tools"
|
||||||
<PlusIcon className="w-4 h-4" />
|
>
|
||||||
</Button>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -536,17 +575,37 @@ export function EntityList({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Show composio cards */}
|
{/* Show composio cards - ordered by status */}
|
||||||
{Object.values(composioTools).map((card) => (
|
{Object.values(composioTools)
|
||||||
<ComposioCard
|
.sort((a, b) => {
|
||||||
key={card.slug}
|
// Helper function to get toolkit status priority
|
||||||
card={card}
|
const getStatusPriority = (toolkit: ComposioToolkit) => {
|
||||||
selectedEntity={selectedEntity}
|
const hasAuth = toolkit.tools.some(tool => tool.composioData && !tool.composioData.noAuth);
|
||||||
onSelectTool={handleToolSelection}
|
const isConnected = !hasAuth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||||
onDeleteTool={onDeleteTool}
|
const isMocked = workflow?.composioMockToolkitStates?.[toolkit.slug]?.isMocked || false;
|
||||||
selectedRef={selectedRef}
|
|
||||||
/>
|
// 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 */}
|
{/* Show MCP server cards */}
|
||||||
{Object.entries(serverTools).map(([serverName, tools]) => (
|
{Object.entries(serverTools).map(([serverName, tools]) => (
|
||||||
|
|
@ -682,6 +741,12 @@ export function EntityList({
|
||||||
onClose={() => setShowAgentTypeModal(false)}
|
onClose={() => setShowAgentTypeModal(false)}
|
||||||
onConfirm={handleAddAgentWithType}
|
onConfirm={handleAddAgentWithType}
|
||||||
/>
|
/>
|
||||||
|
<ComposioToolsModal
|
||||||
|
isOpen={showComposioToolsModal}
|
||||||
|
onClose={() => setShowComposioToolsModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
onToolsUpdated={onProjectToolsUpdated}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -769,6 +834,10 @@ interface ComposioCardProps {
|
||||||
onSelectTool: (name: string) => void;
|
onSelectTool: (name: string) => void;
|
||||||
onDeleteTool: (name: string) => void;
|
onDeleteTool: (name: string) => void;
|
||||||
selectedRef: React.RefObject<HTMLButtonElement | null>;
|
selectedRef: React.RefObject<HTMLButtonElement | null>;
|
||||||
|
projectConfig?: z.infer<typeof Project>;
|
||||||
|
projectId: string;
|
||||||
|
workflow: z.infer<typeof Workflow>;
|
||||||
|
onProjectToolsUpdated?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposioCard = ({
|
const ComposioCard = ({
|
||||||
|
|
@ -777,70 +846,231 @@ const ComposioCard = ({
|
||||||
onSelectTool,
|
onSelectTool,
|
||||||
onDeleteTool,
|
onDeleteTool,
|
||||||
selectedRef,
|
selectedRef,
|
||||||
|
projectConfig,
|
||||||
|
projectId,
|
||||||
|
workflow,
|
||||||
|
onProjectToolsUpdated,
|
||||||
}: ComposioCardProps) => {
|
}: ComposioCardProps) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
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 (
|
return (
|
||||||
<div className="mb-2">
|
<>
|
||||||
<button
|
<div className="mb-1 group">
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
<div className="flex items-center gap-2 px-2 py-1 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md transition-colors">
|
||||||
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"
|
<button
|
||||||
>
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
{isExpanded ? (
|
className="flex-1 flex items-center gap-2 text-sm text-left min-h-[28px]"
|
||||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
>
|
||||||
) : (
|
{/* Chevron - only show on hover or when has tools */}
|
||||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
<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'
|
||||||
<div className="flex items-center gap-1">
|
}`}>
|
||||||
{card.logo ? (
|
{card.tools.length > 0 && (isExpanded ? (
|
||||||
<div className="relative w-4 h-4">
|
<ChevronDown className="w-3 h-3 text-gray-500" />
|
||||||
<PictureImg
|
) : (
|
||||||
src={card.logo}
|
<ChevronRight className="w-3 h-3 text-gray-500" />
|
||||||
alt={`${card.name} logo`}
|
))}
|
||||||
className="w-full h-full object-contain rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
|
<div className="flex items-center gap-2">
|
||||||
)}
|
{card.logo ? (
|
||||||
<span>{card.name}</span>
|
<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>
|
</div>
|
||||||
</button>
|
|
||||||
{isExpanded && (
|
{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) => (
|
{card.tools.map((tool, index) => (
|
||||||
<ListItemWithMenu
|
<div key={`composio-tool-${index}`} className="group/tool">
|
||||||
key={`composio-tool-${index}`}
|
<ListItemWithMenu
|
||||||
name={tool.name}
|
name={tool.name}
|
||||||
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
|
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
|
||||||
onClick={() => onSelectTool(tool.name)}
|
onClick={() => onSelectTool(tool.name)}
|
||||||
disabled={tool.isLibrary}
|
disabled={tool.isLibrary}
|
||||||
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
||||||
icon={
|
icon={
|
||||||
card.logo ? (
|
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600"></div>
|
||||||
<div className="relative w-4 h-4">
|
}
|
||||||
<PictureImg
|
menuContent={
|
||||||
src={card.logo}
|
<div className="opacity-0 group-hover/tool:opacity-100 transition-opacity">
|
||||||
alt={`${card.name} logo`}
|
<EntityDropdown
|
||||||
className="w-full h-full object-contain rounded"
|
name={tool.name}
|
||||||
|
onDelete={onDeleteTool}
|
||||||
|
isLocked={tool.isComposio}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
}
|
||||||
<ImportIcon className="w-4 h-4 text-blue-600 dark:text-blue-500" />
|
/>
|
||||||
)
|
</div>
|
||||||
}
|
|
||||||
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 { MCPServer, Message, WithStringId } from "../../../lib/types/types";
|
||||||
import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types";
|
import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent } from "../../../lib/types/workflow_types";
|
||||||
import { DataSource } from "../../../lib/types/datasource_types";
|
import { DataSource } from "../../../lib/types/datasource_types";
|
||||||
|
import { Project } from "../../../lib/types/project_types";
|
||||||
import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer';
|
import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer';
|
||||||
import { AgentConfig } from "../entities/agent_config";
|
import { AgentConfig } from "../entities/agent_config";
|
||||||
import { ToolConfig } from "../entities/tool_config";
|
import { ToolConfig } from "../entities/tool_config";
|
||||||
|
|
@ -142,6 +143,9 @@ export type Action = {
|
||||||
type: "show_visualise";
|
type: "show_visualise";
|
||||||
} | {
|
} | {
|
||||||
type: "hide_visualise";
|
type: "hide_visualise";
|
||||||
|
} | {
|
||||||
|
type: "sync_workflow";
|
||||||
|
workflow: z.infer<typeof Workflow>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
|
|
@ -207,6 +211,13 @@ function reducer(state: State, action: Action): State {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "sync_workflow": {
|
||||||
|
newState = produce(state, draft => {
|
||||||
|
draft.present.workflow = action.workflow;
|
||||||
|
draft.present.lastUpdatedAt = action.workflow.lastUpdatedAt;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "reorder_agents": {
|
case "reorder_agents": {
|
||||||
const newState = produce(state.present, draft => {
|
const newState = produce(state.present, draft => {
|
||||||
draft.workflow.agents = action.agents;
|
draft.workflow.agents = action.agents;
|
||||||
|
|
@ -579,10 +590,12 @@ export function WorkflowEditor({
|
||||||
toolWebhookUrl,
|
toolWebhookUrl,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
projectTools,
|
projectTools,
|
||||||
|
projectConfig,
|
||||||
eligibleModels,
|
eligibleModels,
|
||||||
isLive,
|
isLive,
|
||||||
onChangeMode,
|
onChangeMode,
|
||||||
onRevertToLive,
|
onRevertToLive,
|
||||||
|
onProjectToolsUpdated,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||||
|
|
@ -592,10 +605,12 @@ export function WorkflowEditor({
|
||||||
toolWebhookUrl: string;
|
toolWebhookUrl: string;
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
projectTools: z.infer<typeof WorkflowTool>[];
|
projectTools: z.infer<typeof WorkflowTool>[];
|
||||||
|
projectConfig: z.infer<typeof Project>;
|
||||||
eligibleModels: z.infer<typeof ModelsResponse> | "*";
|
eligibleModels: z.infer<typeof ModelsResponse> | "*";
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
onChangeMode: (mode: 'draft' | 'live') => void;
|
onChangeMode: (mode: 'draft' | 'live') => void;
|
||||||
onRevertToLive: () => void;
|
onRevertToLive: () => void;
|
||||||
|
onProjectToolsUpdated?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
|
@ -615,6 +630,12 @@ export function WorkflowEditor({
|
||||||
isLive,
|
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 [chatMessages, setChatMessages] = useState<z.infer<typeof Message>[]>([]);
|
||||||
const updateChatMessages = useCallback((messages: z.infer<typeof Message>[]) => {
|
const updateChatMessages = useCallback((messages: z.infer<typeof Message>[]) => {
|
||||||
setChatMessages(messages);
|
setChatMessages(messages);
|
||||||
|
|
@ -980,6 +1001,7 @@ export function WorkflowEditor({
|
||||||
tools={state.present.workflow.tools}
|
tools={state.present.workflow.tools}
|
||||||
projectTools={projectTools}
|
projectTools={projectTools}
|
||||||
prompts={state.present.workflow.prompts}
|
prompts={state.present.workflow.prompts}
|
||||||
|
workflow={state.present.workflow}
|
||||||
selectedEntity={
|
selectedEntity={
|
||||||
state.present.selection &&
|
state.present.selection &&
|
||||||
(state.present.selection.type === "agent" ||
|
(state.present.selection.type === "agent" ||
|
||||||
|
|
@ -1002,6 +1024,8 @@ export function WorkflowEditor({
|
||||||
onDeletePrompt={handleDeletePrompt}
|
onDeletePrompt={handleDeletePrompt}
|
||||||
onShowVisualise={handleShowVisualise}
|
onShowVisualise={handleShowVisualise}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
onProjectToolsUpdated={onProjectToolsUpdated}
|
||||||
|
projectConfig={projectConfig}
|
||||||
onReorderAgents={handleReorderAgents}
|
onReorderAgents={handleReorderAgents}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
Moon,
|
Moon,
|
||||||
Sun,
|
Sun,
|
||||||
HelpCircle,
|
HelpCircle
|
||||||
Wrench
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { getProjectConfig } from "@/app/actions/project_actions";
|
import { getProjectConfig } from "@/app/actions/project_actions";
|
||||||
import { useTheme } from "@/app/providers/theme-provider";
|
import { useTheme } from "@/app/providers/theme-provider";
|
||||||
|
|
@ -69,13 +68,6 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
||||||
icon: DatabaseIcon,
|
icon: DatabaseIcon,
|
||||||
requiresProject: true
|
requiresProject: true
|
||||||
}] : []),
|
}] : []),
|
||||||
{
|
|
||||||
href: 'tools',
|
|
||||||
label: 'Tools',
|
|
||||||
icon: Wrench,
|
|
||||||
requiresProject: true,
|
|
||||||
beta: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: 'config',
|
href: 'config',
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
|
|
@ -162,14 +154,7 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<>
|
<span>{item.label}</span>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
57
apps/rowboat/package-lock.json
generated
57
apps/rowboat/package-lock.json
generated
|
|
@ -43,7 +43,7 @@
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"lucide-react": "^0.465.0",
|
"lucide-react": "^0.465.0",
|
||||||
"mermaid": "^11.8.1",
|
"mermaid": "^11.9.0",
|
||||||
"mongodb": "^6.8.0",
|
"mongodb": "^6.8.0",
|
||||||
"next": "15.3.4",
|
"next": "15.3.4",
|
||||||
"openai": "^4.67.2",
|
"openai": "^4.67.2",
|
||||||
|
|
@ -1396,7 +1396,6 @@
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
|
||||||
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
|
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chevrotain/gast": "11.0.3",
|
"@chevrotain/gast": "11.0.3",
|
||||||
"@chevrotain/types": "11.0.3",
|
"@chevrotain/types": "11.0.3",
|
||||||
|
|
@ -1407,7 +1406,6 @@
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
|
||||||
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
|
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chevrotain/types": "11.0.3",
|
"@chevrotain/types": "11.0.3",
|
||||||
"lodash-es": "4.17.21"
|
"lodash-es": "4.17.21"
|
||||||
|
|
@ -1416,20 +1414,17 @@
|
||||||
"node_modules/@chevrotain/regexp-to-ast": {
|
"node_modules/@chevrotain/regexp-to-ast": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
|
||||||
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
|
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
},
|
||||||
"node_modules/@chevrotain/types": {
|
"node_modules/@chevrotain/types": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
|
||||||
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
|
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
},
|
||||||
"node_modules/@chevrotain/utils": {
|
"node_modules/@chevrotain/utils": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
|
||||||
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
|
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
},
|
||||||
"node_modules/@composio/client": {
|
"node_modules/@composio/client": {
|
||||||
"version": "0.1.0-alpha.26",
|
"version": "0.1.0-alpha.26",
|
||||||
|
|
@ -4582,10 +4577,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mermaid-js/parser": {
|
"node_modules/@mermaid-js/parser": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
|
||||||
"integrity": "sha512-lCQNpV8R4lgsGcjX5667UiuDLk2micCtjtxR1YKbBXvN5w2v+FeLYoHrTSSrjwXdMcDYvE4ZBPvKT31dfeSmmA==",
|
"integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"langium": "3.3.1"
|
"langium": "3.3.1"
|
||||||
}
|
}
|
||||||
|
|
@ -9222,7 +9216,6 @@
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
|
||||||
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chevrotain/cst-dts-gen": "11.0.3",
|
"@chevrotain/cst-dts-gen": "11.0.3",
|
||||||
"@chevrotain/gast": "11.0.3",
|
"@chevrotain/gast": "11.0.3",
|
||||||
|
|
@ -9236,7 +9229,6 @@
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
|
||||||
"integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
|
"integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash-es": "^4.17.21"
|
"lodash-es": "^4.17.21"
|
||||||
},
|
},
|
||||||
|
|
@ -12928,7 +12920,6 @@
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
|
||||||
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
|
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chevrotain": "~11.0.3",
|
"chevrotain": "~11.0.3",
|
||||||
"chevrotain-allstar": "~0.3.0",
|
"chevrotain-allstar": "~0.3.0",
|
||||||
|
|
@ -13440,15 +13431,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "15.0.12",
|
"version": "16.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-16.1.0.tgz",
|
||||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
"integrity": "sha512-Me7BNa1aqrxVinDnFfvCgHh2yHvLbFvILBs899MhuBpbE5VPzpSqv7alaESfkqkgc9JNvUGH4gqwZeOzLnY8Jg==",
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
|
|
@ -13781,14 +13771,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mermaid": {
|
"node_modules/mermaid": {
|
||||||
"version": "11.8.1",
|
"version": "11.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.9.0.tgz",
|
||||||
"integrity": "sha512-VSXJLqP1Sqw5sGr273mhvpPRhXwE6NlmMSqBZQw+yZJoAJkOIPPn/uT3teeCBx60Fkt5zEI3FrH2eVT0jXRDzw==",
|
"integrity": "sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.0.4",
|
"@braintree/sanitize-url": "^7.0.4",
|
||||||
"@iconify/utils": "^2.1.33",
|
"@iconify/utils": "^2.1.33",
|
||||||
"@mermaid-js/parser": "^0.6.1",
|
"@mermaid-js/parser": "^0.6.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"cytoscape": "^3.29.3",
|
"cytoscape": "^3.29.3",
|
||||||
"cytoscape-cose-bilkent": "^4.1.0",
|
"cytoscape-cose-bilkent": "^4.1.0",
|
||||||
|
|
@ -13798,10 +13787,10 @@
|
||||||
"dagre-d3-es": "7.0.11",
|
"dagre-d3-es": "7.0.11",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.5",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.22",
|
||||||
"khroma": "^2.1.0",
|
"khroma": "^2.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^15.0.7",
|
"marked": "^16.0.0",
|
||||||
"roughjs": "^4.6.6",
|
"roughjs": "^4.6.6",
|
||||||
"stylis": "^4.3.6",
|
"stylis": "^4.3.6",
|
||||||
"ts-dedent": "^2.2.0",
|
"ts-dedent": "^2.2.0",
|
||||||
|
|
@ -17797,7 +17786,6 @@
|
||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -17806,7 +17794,6 @@
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
|
||||||
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
|
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-languageserver-protocol": "3.17.5"
|
"vscode-languageserver-protocol": "3.17.5"
|
||||||
},
|
},
|
||||||
|
|
@ -17818,7 +17805,6 @@
|
||||||
"version": "3.17.5",
|
"version": "3.17.5",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||||
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-jsonrpc": "8.2.0",
|
"vscode-jsonrpc": "8.2.0",
|
||||||
"vscode-languageserver-types": "3.17.5"
|
"vscode-languageserver-types": "3.17.5"
|
||||||
|
|
@ -17827,20 +17813,17 @@
|
||||||
"node_modules/vscode-languageserver-textdocument": {
|
"node_modules/vscode-languageserver-textdocument": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
|
||||||
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
|
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/vscode-languageserver-types": {
|
"node_modules/vscode-languageserver-types": {
|
||||||
"version": "3.17.5",
|
"version": "3.17.5",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||||
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/vscode-uri": {
|
"node_modules/vscode-uri": {
|
||||||
"version": "3.0.8",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||||
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
|
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "4.0.0-beta.3",
|
"version": "4.0.0-beta.3",
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
"jose": "^5.9.6",
|
"jose": "^5.9.6",
|
||||||
"lucide-react": "^0.465.0",
|
"lucide-react": "^0.465.0",
|
||||||
"mermaid": "^11.8.1",
|
"mermaid": "^11.9.0",
|
||||||
"mongodb": "^6.8.0",
|
"mongodb": "^6.8.0",
|
||||||
"next": "15.3.4",
|
"next": "15.3.4",
|
||||||
"openai": "^4.67.2",
|
"openai": "^4.67.2",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue