mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-03 19:25:19 +02:00
Merge pull request #176 from rowboatlabs/copilot_tools_improvements
Copilot tools improvements
This commit is contained in:
commit
775a64c5a8
42 changed files with 1930 additions and 1488 deletions
|
|
@ -60,7 +60,6 @@ export async function scrapeWebpage(url: string): Promise<z.infer<typeof Webpage
|
||||||
export async function getAssistantResponseStreamId(
|
export async function getAssistantResponseStreamId(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
projectTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
messages: z.infer<typeof Message>[],
|
messages: z.infer<typeof Message>[],
|
||||||
): Promise<{ streamId: string } | { billingError: string }> {
|
): Promise<{ streamId: string } | { billingError: string }> {
|
||||||
await projectAuthCheck(projectId);
|
await projectAuthCheck(projectId);
|
||||||
|
|
@ -83,6 +82,6 @@ export async function getAssistantResponseStreamId(
|
||||||
return { billingError: error || 'Billing error' };
|
return { billingError: error || 'Billing error' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getAgenticResponseStreamId(projectId, workflow, projectTools, messages);
|
const response = await getAgenticResponseStreamId(projectId, workflow, messages);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
@ -42,9 +42,9 @@ export async function getToolkit(projectId: string, toolkitSlug: string): Promis
|
||||||
return await libGetToolkit(toolkitSlug);
|
return await libGetToolkit(toolkitSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listTools(projectId: string, toolkitSlug: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
export async function listTools(projectId: string, toolkitSlug: string, searchQuery: string | null, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||||
await projectAuthCheck(projectId);
|
await projectAuthCheck(projectId);
|
||||||
return await libListTools(toolkitSlug, cursor);
|
return await libListTools(toolkitSlug, searchQuery, cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
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>> {
|
||||||
|
|
@ -216,11 +216,4 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str
|
||||||
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
|
await projectsCollection.updateOne({ _id: projectId }, { $unset: { [key]: "" } });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateComposioSelectedTools(projectId: string, tools: z.infer<typeof ZTool>[]): Promise<void> {
|
|
||||||
await projectAuthCheck(projectId);
|
|
||||||
|
|
||||||
// update project with new selected tools
|
|
||||||
await projectsCollection.updateOne({ _id: projectId }, { $set: { composioSelectedTools: tools } });
|
|
||||||
}
|
}
|
||||||
|
|
@ -11,8 +11,6 @@ import { check_query_limit } from "../lib/rate_limiting";
|
||||||
import { QueryLimitError } from "../lib/client_utils";
|
import { QueryLimitError } from "../lib/client_utils";
|
||||||
import { projectAuthCheck } from "./project_actions";
|
import { projectAuthCheck } from "./project_actions";
|
||||||
import { redisClient } from "../lib/redis";
|
import { redisClient } from "../lib/redis";
|
||||||
import { collectProjectTools } from "../lib/project_tools";
|
|
||||||
import { mergeProjectTools } from "../lib/types/project_types";
|
|
||||||
import { authorizeUserAction, logUsage } from "./billing_actions";
|
import { authorizeUserAction, logUsage } from "./billing_actions";
|
||||||
import { USE_BILLING } from "../lib/feature_flags";
|
import { USE_BILLING } from "../lib/feature_flags";
|
||||||
import { WithStringId } from "../lib/types/types";
|
import { WithStringId } from "../lib/types/types";
|
||||||
|
|
@ -44,21 +42,12 @@ export async function getCopilotResponseStream(
|
||||||
if (!await check_query_limit(projectId)) {
|
if (!await check_query_limit(projectId)) {
|
||||||
throw new QueryLimitError();
|
throw new QueryLimitError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get MCP tools from project and merge with workflow tools
|
|
||||||
const projectTools = await collectProjectTools(projectId);
|
|
||||||
|
|
||||||
// Convert workflow to copilot format with both workflow and project tools
|
|
||||||
const wflow = {
|
|
||||||
...current_workflow_config,
|
|
||||||
tools: mergeProjectTools(current_workflow_config.tools, projectTools)
|
|
||||||
};
|
|
||||||
|
|
||||||
// prepare request
|
// prepare request
|
||||||
const request: z.infer<typeof CopilotAPIRequest> = {
|
const request: z.infer<typeof CopilotAPIRequest> = {
|
||||||
projectId,
|
projectId,
|
||||||
messages,
|
messages,
|
||||||
workflow: wflow,
|
workflow: current_workflow_config,
|
||||||
context,
|
context,
|
||||||
dataSources: dataSources,
|
dataSources: dataSources,
|
||||||
};
|
};
|
||||||
|
|
@ -97,20 +86,11 @@ export async function getCopilotAgentInstructions(
|
||||||
return { billingError: authResponse.error || 'Billing error' };
|
return { billingError: authResponse.error || 'Billing error' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get MCP tools from project and merge with workflow tools
|
|
||||||
const projectTools = await collectProjectTools(projectId);
|
|
||||||
|
|
||||||
// Convert workflow to copilot format with both workflow and project tools
|
|
||||||
const wflow = {
|
|
||||||
...current_workflow_config,
|
|
||||||
tools: mergeProjectTools(current_workflow_config.tools, projectTools)
|
|
||||||
};
|
|
||||||
|
|
||||||
// prepare request
|
// prepare request
|
||||||
const request: z.infer<typeof CopilotAPIRequest> = {
|
const request: z.infer<typeof CopilotAPIRequest> = {
|
||||||
projectId,
|
projectId,
|
||||||
messages,
|
messages,
|
||||||
workflow: wflow,
|
workflow: current_workflow_config,
|
||||||
context: {
|
context: {
|
||||||
type: 'agent',
|
type: 'agent',
|
||||||
name: agentName,
|
name: agentName,
|
||||||
|
|
|
||||||
67
apps/rowboat/app/actions/custom_mcp_server_actions.ts
Normal file
67
apps/rowboat/app/actions/custom_mcp_server_actions.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { projectsCollection } from '../lib/mongodb';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { projectAuthCheck } from './project_actions';
|
||||||
|
import { CustomMcpServer } from '../lib/types/project_types';
|
||||||
|
import { getMcpClient } from '../lib/mcp';
|
||||||
|
import { WorkflowTool } from '../lib/types/workflow_types';
|
||||||
|
import { authCheck } from './auth_actions';
|
||||||
|
|
||||||
|
type McpServerType = z.infer<typeof CustomMcpServer>;
|
||||||
|
|
||||||
|
function validateUrl(url: string): string {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||||
|
throw new Error('Invalid protocol');
|
||||||
|
}
|
||||||
|
return parsedUrl.toString();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Invalid URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addServer(projectId: string, name: string, server: McpServerType): Promise<void> {
|
||||||
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
|
// Validate the server URL
|
||||||
|
validateUrl(server.serverUrl);
|
||||||
|
|
||||||
|
// Update the customMcpServers record with the server
|
||||||
|
await projectsCollection.updateOne(
|
||||||
|
{ _id: projectId },
|
||||||
|
{ $set: { [`customMcpServers.${name}`]: server } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeServer(projectId: string, name: string): Promise<void> {
|
||||||
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
|
await projectsCollection.updateOne(
|
||||||
|
{ _id: projectId },
|
||||||
|
{ $unset: { [`customMcpServers.${name}`]: "" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTools(serverUrl: string, serverName: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||||
|
await authCheck();
|
||||||
|
|
||||||
|
const client = await getMcpClient(serverUrl, serverName);
|
||||||
|
const result = await client.listTools();
|
||||||
|
return result.tools.map(tool => {
|
||||||
|
return {
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description || '',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: tool.inputSchema?.properties || {},
|
||||||
|
required: tool.inputSchema?.required || [],
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
isMcp: true,
|
||||||
|
mcpServerName: serverName,
|
||||||
|
mcpServerURL: serverUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
'use server';
|
|
||||||
|
|
||||||
import { projectsCollection } from '../lib/mongodb';
|
|
||||||
import { MCPServer } from '../lib/types/types';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { projectAuthCheck } from './project_actions';
|
|
||||||
|
|
||||||
type McpServerType = z.infer<typeof MCPServer>;
|
|
||||||
|
|
||||||
function formatServerUrl(url: string): string {
|
|
||||||
// Ensure URL starts with http:// or https://
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
url = 'http://' + url;
|
|
||||||
}
|
|
||||||
// Remove trailing slash if present
|
|
||||||
return url.replace(/\/$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchCustomServers(projectId: string) {
|
|
||||||
await projectAuthCheck(projectId);
|
|
||||||
|
|
||||||
const project = await projectsCollection.findOne({ _id: projectId });
|
|
||||||
return (project?.mcpServers || [])
|
|
||||||
.filter(server => server.serverType === 'custom')
|
|
||||||
.map(server => ({
|
|
||||||
...server,
|
|
||||||
serverType: 'custom' as const,
|
|
||||||
isReady: true // Custom servers are always ready
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addCustomServer(projectId: string, server: McpServerType) {
|
|
||||||
await projectAuthCheck(projectId);
|
|
||||||
|
|
||||||
// Format the server URL and ensure isReady is true for custom servers
|
|
||||||
const formattedServer = {
|
|
||||||
...server,
|
|
||||||
serverUrl: formatServerUrl(server.serverUrl || ''),
|
|
||||||
isReady: true // Custom servers are always ready
|
|
||||||
};
|
|
||||||
|
|
||||||
await projectsCollection.updateOne(
|
|
||||||
{ _id: projectId },
|
|
||||||
{ $push: { mcpServers: formattedServer } }
|
|
||||||
);
|
|
||||||
|
|
||||||
return formattedServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeCustomServer(projectId: string, serverName: string) {
|
|
||||||
await projectAuthCheck(projectId);
|
|
||||||
|
|
||||||
await projectsCollection.updateOne(
|
|
||||||
{ _id: projectId },
|
|
||||||
{ $pull: { mcpServers: { name: serverName } } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function toggleCustomServer(projectId: string, serverName: string, isActive: boolean) {
|
|
||||||
await projectAuthCheck(projectId);
|
|
||||||
|
|
||||||
await projectsCollection.updateOne(
|
|
||||||
{ _id: projectId, "mcpServers.name": serverName },
|
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
"mcpServers.$.isActive": isActive,
|
|
||||||
"mcpServers.$.isReady": isActive // Update isReady along with isActive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateCustomServerTools(
|
|
||||||
projectId: string,
|
|
||||||
serverName: string,
|
|
||||||
tools: McpServerType['tools'],
|
|
||||||
availableTools?: McpServerType['availableTools']
|
|
||||||
) {
|
|
||||||
await projectAuthCheck(projectId);
|
|
||||||
|
|
||||||
const update: Record<string, any> = {
|
|
||||||
"mcpServers.$.tools": tools
|
|
||||||
};
|
|
||||||
|
|
||||||
if (availableTools) {
|
|
||||||
update["mcpServers.$.availableTools"] = availableTools;
|
|
||||||
}
|
|
||||||
|
|
||||||
await projectsCollection.updateOne(
|
|
||||||
{ _id: projectId, "mcpServers.name": serverName },
|
|
||||||
{ $set: update }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -14,8 +14,6 @@ import { USE_AUTH } from "../lib/feature_flags";
|
||||||
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
|
import { deleteMcpServerInstance, listActiveServerInstances } from "./klavis_actions";
|
||||||
import { authorizeUserAction } from "./billing_actions";
|
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 { collectProjectTools as libCollectProjectTools } from "../lib/project_tools";
|
|
||||||
|
|
||||||
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
const KLAVIS_API_KEY = process.env.KLAVIS_API_KEY || '';
|
||||||
|
|
||||||
|
|
@ -330,11 +328,6 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
|
||||||
return { id: projectId };
|
return { id: projectId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function collectProjectTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
|
||||||
await projectAuthCheck(projectId);
|
|
||||||
return libCollectProjectTools(projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveWorkflow(projectId: string, workflow: z.infer<typeof Workflow>) {
|
export async function saveWorkflow(projectId: string, workflow: z.infer<typeof Workflow>) {
|
||||||
await projectAuthCheck(projectId);
|
await projectAuthCheck(projectId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId:
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the payload
|
// parse the payload
|
||||||
const { projectId, workflow, projectTools, messages } = ZStreamAgentResponsePayload.parse(JSON.parse(payload));
|
const { projectId, workflow, messages } = ZStreamAgentResponsePayload.parse(JSON.parse(payload));
|
||||||
console.log('payload', payload);
|
console.log('payload', payload);
|
||||||
|
|
||||||
// fetch billing customer id
|
// fetch billing customer id
|
||||||
|
|
@ -29,7 +29,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId:
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
try {
|
try {
|
||||||
// Iterate over the generator
|
// Iterate over the generator
|
||||||
for await (const event of streamResponse(projectId, workflow, projectTools, messages)) {
|
for await (const event of streamResponse(projectId, workflow, messages)) {
|
||||||
// Check if this is a message event (has role property)
|
// Check if this is a message event (has role property)
|
||||||
if ('role' in event) {
|
if ('role' in event) {
|
||||||
if (event.role === 'assistant') {
|
if (event.role === 'assistant') {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getResponse } from "@/app/lib/agents";
|
import { getResponse } from "@/app/lib/agents";
|
||||||
import { projectsCollection, twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb";
|
import { projectsCollection, twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb";
|
||||||
import { collectProjectTools } from "@/app/lib/project_tools";
|
|
||||||
import { PrefixLogger } from "@/app/lib/utils";
|
import { PrefixLogger } from "@/app/lib/utils";
|
||||||
import VoiceResponse from "twilio/lib/twiml/VoiceResponse";
|
import VoiceResponse from "twilio/lib/twiml/VoiceResponse";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -78,12 +77,9 @@ export async function POST(request: Request) {
|
||||||
return reject('rejected');
|
return reject('rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch project tools
|
|
||||||
const projectTools = await collectProjectTools(projectId);
|
|
||||||
|
|
||||||
// this is the first turn, get the initial assistant response
|
// this is the first turn, get the initial assistant response
|
||||||
// and validate it
|
// and validate it
|
||||||
const { messages } = await getResponse(projectId, workflow, projectTools, []);
|
const { messages } = await getResponse(projectId, workflow, []);
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
logger.log('Agent response is empty');
|
logger.log('Agent response is empty');
|
||||||
return hangup();
|
return hangup();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getResponse } from "@/app/lib/agents";
|
import { getResponse } from "@/app/lib/agents";
|
||||||
import { projectsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb";
|
import { projectsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb";
|
||||||
import { collectProjectTools } from "@/app/lib/project_tools";
|
|
||||||
import { PrefixLogger } from "@/app/lib/utils";
|
import { PrefixLogger } from "@/app/lib/utils";
|
||||||
import VoiceResponse from "twilio/lib/twiml/VoiceResponse";
|
import VoiceResponse from "twilio/lib/twiml/VoiceResponse";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -50,9 +49,6 @@ export async function POST(
|
||||||
return hangup();
|
return hangup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch project tools
|
|
||||||
const projectTools = await collectProjectTools(projectId);
|
|
||||||
|
|
||||||
// add user speech as user message, and get assistant response
|
// add user speech as user message, and get assistant response
|
||||||
const reqMessages: z.infer<typeof Message>[] = [
|
const reqMessages: z.infer<typeof Message>[] = [
|
||||||
...call.messages,
|
...call.messages,
|
||||||
|
|
@ -61,7 +57,7 @@ export async function POST(
|
||||||
content: data.SpeechResult,
|
content: data.SpeechResult,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const { messages } = await getResponse(projectId, workflow, projectTools, reqMessages);
|
const { messages } = await getResponse(projectId, workflow, reqMessages);
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
logger.log('Agent response is empty');
|
logger.log('Agent response is empty');
|
||||||
return hangup();
|
return hangup();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { authCheck } from "../../utils";
|
||||||
import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
|
import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
|
||||||
import { check_query_limit } from "../../../../lib/rate_limiting";
|
import { check_query_limit } from "../../../../lib/rate_limiting";
|
||||||
import { PrefixLogger } from "../../../../lib/utils";
|
import { PrefixLogger } from "../../../../lib/utils";
|
||||||
import { collectProjectTools } from "@/app/lib/project_tools";
|
|
||||||
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
||||||
import { USE_BILLING } from "@/app/lib/feature_flags";
|
import { USE_BILLING } from "@/app/lib/feature_flags";
|
||||||
import { getResponse } from "@/app/lib/agents";
|
import { getResponse } from "@/app/lib/agents";
|
||||||
|
|
@ -61,9 +60,6 @@ export async function POST(
|
||||||
return Response.json({ error: "Project not found" }, { status: 404 });
|
return Response.json({ error: "Project not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch project tools
|
|
||||||
const projectTools = await collectProjectTools(projectId);
|
|
||||||
|
|
||||||
// fetch workflow
|
// fetch workflow
|
||||||
const workflow = project.liveWorkflow;
|
const workflow = project.liveWorkflow;
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
|
|
@ -94,7 +90,7 @@ export async function POST(
|
||||||
}
|
}
|
||||||
|
|
||||||
// get assistant response
|
// get assistant response
|
||||||
const { messages } = await getResponse(projectId, workflow, projectTools, reqMessages);
|
const { messages } = await getResponse(projectId, workflow, reqMessages);
|
||||||
|
|
||||||
// log billing usage
|
// log billing usage
|
||||||
if (USE_BILLING && billingCustomerId) {
|
if (USE_BILLING && billingCustomerId) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { ObjectId, WithId } from "mongodb";
|
||||||
import { authCheck } from "../../../utils";
|
import { authCheck } from "../../../utils";
|
||||||
import { check_query_limit } from "../../../../../../lib/rate_limiting";
|
import { check_query_limit } from "../../../../../../lib/rate_limiting";
|
||||||
import { PrefixLogger } from "../../../../../../lib/utils";
|
import { PrefixLogger } from "../../../../../../lib/utils";
|
||||||
import { collectProjectTools } from "@/app/lib/project_tools";
|
|
||||||
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing";
|
||||||
import { USE_BILLING } from "@/app/lib/feature_flags";
|
import { USE_BILLING } from "@/app/lib/feature_flags";
|
||||||
import { getResponse } from "@/app/lib/agents";
|
import { getResponse } from "@/app/lib/agents";
|
||||||
|
|
@ -181,9 +180,6 @@ export async function POST(
|
||||||
throw new Error("Project settings not found");
|
throw new Error("Project settings not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch project tools
|
|
||||||
const projectTools = await collectProjectTools(session.projectId);
|
|
||||||
|
|
||||||
// fetch workflow
|
// fetch workflow
|
||||||
const workflow = projectSettings.liveWorkflow;
|
const workflow = projectSettings.liveWorkflow;
|
||||||
if (!workflow) {
|
if (!workflow) {
|
||||||
|
|
@ -211,7 +207,7 @@ export async function POST(
|
||||||
const inMessages: z.infer<typeof Message>[] = convert(messages);
|
const inMessages: z.infer<typeof Message>[] = convert(messages);
|
||||||
inMessages.push(userMessage);
|
inMessages.push(userMessage);
|
||||||
|
|
||||||
const { messages: responseMessages } = await getResponse(session.projectId, workflow, projectTools, [systemMessage, ...inMessages]);
|
const { messages: responseMessages } = await getResponse(session.projectId, workflow, [systemMessage, ...inMessages]);
|
||||||
const convertedResponseMessages = convertBack(responseMessages);
|
const convertedResponseMessages = convertBack(responseMessages);
|
||||||
const unsavedMessages = [
|
const unsavedMessages = [
|
||||||
userMessage,
|
userMessage,
|
||||||
|
|
|
||||||
|
|
@ -253,17 +253,27 @@ async function invokeMcpTool(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
name: string,
|
name: string,
|
||||||
input: any,
|
input: any,
|
||||||
mcpServerURL: string,
|
|
||||||
mcpServerName: string
|
mcpServerName: string
|
||||||
) {
|
) {
|
||||||
logger = logger.child(`invokeMcpTool`);
|
logger = logger.child(`invokeMcpTool`);
|
||||||
logger.log(`projectId: ${projectId}`);
|
logger.log(`projectId: ${projectId}`);
|
||||||
logger.log(`name: ${name}`);
|
logger.log(`name: ${name}`);
|
||||||
logger.log(`input: ${JSON.stringify(input)}`);
|
logger.log(`input: ${JSON.stringify(input)}`);
|
||||||
logger.log(`mcpServerURL: ${mcpServerURL}`);
|
|
||||||
logger.log(`mcpServerName: ${mcpServerName}`);
|
logger.log(`mcpServerName: ${mcpServerName}`);
|
||||||
|
|
||||||
const client = await getMcpClient(mcpServerURL, mcpServerName || '');
|
// Get project configuration
|
||||||
|
const project = await projectsCollection.findOne({ _id: projectId });
|
||||||
|
if (!project) {
|
||||||
|
throw new Error(`project ${projectId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get server url from project data
|
||||||
|
const mcpServerURL = project.customMcpServers?.[mcpServerName]?.serverUrl;
|
||||||
|
if (!mcpServerURL) {
|
||||||
|
throw new Error(`mcp server url not found for project ${projectId} and server ${mcpServerName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await getMcpClient(mcpServerURL, mcpServerName);
|
||||||
const result = await client.callTool({
|
const result = await client.callTool({
|
||||||
name,
|
name,
|
||||||
arguments: input,
|
arguments: input,
|
||||||
|
|
@ -420,7 +430,7 @@ function createMcpTool(
|
||||||
config: z.infer<typeof WorkflowTool>,
|
config: z.infer<typeof WorkflowTool>,
|
||||||
projectId: string
|
projectId: string
|
||||||
): Tool {
|
): Tool {
|
||||||
const { name, description, parameters, mcpServerName, mcpServerURL } = config;
|
const { name, description, parameters, mcpServerName } = config;
|
||||||
|
|
||||||
return tool({
|
return tool({
|
||||||
name,
|
name,
|
||||||
|
|
@ -434,7 +444,7 @@ function createMcpTool(
|
||||||
},
|
},
|
||||||
async execute(input: any) {
|
async execute(input: any) {
|
||||||
try {
|
try {
|
||||||
const result = await invokeMcpTool(logger, projectId, name, input, mcpServerURL || '', mcpServerName || '');
|
const result = await invokeMcpTool(logger, projectId, name, input, mcpServerName || '');
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
result,
|
result,
|
||||||
});
|
});
|
||||||
|
|
@ -492,7 +502,6 @@ function createAgent(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
config: z.infer<typeof WorkflowAgent>,
|
config: z.infer<typeof WorkflowAgent>,
|
||||||
tools: Record<string, Tool>,
|
tools: Record<string, Tool>,
|
||||||
projectTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>,
|
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>,
|
||||||
): { agent: Agent, entities: z.infer<typeof ConnectedEntity>[] } {
|
): { agent: Agent, entities: z.infer<typeof ConnectedEntity>[] } {
|
||||||
|
|
@ -522,7 +531,7 @@ ${'-'.repeat(100)}
|
||||||
${CHILD_TRANSFER_RELATED_INSTRUCTIONS}
|
${CHILD_TRANSFER_RELATED_INSTRUCTIONS}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let { sanitized, entities } = sanitizeTextWithMentions(instructions, workflow, projectTools);
|
let { sanitized, entities } = sanitizeTextWithMentions(instructions, workflow);
|
||||||
agentLogger.log(`instructions: ${JSON.stringify(sanitized)}`);
|
agentLogger.log(`instructions: ${JSON.stringify(sanitized)}`);
|
||||||
agentLogger.log(`mentions: ${JSON.stringify(entities)}`);
|
agentLogger.log(`mentions: ${JSON.stringify(entities)}`);
|
||||||
|
|
||||||
|
|
@ -755,7 +764,7 @@ Basic context:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapConfig(workflow: z.infer<typeof Workflow>, projectTools: z.infer<typeof WorkflowTool>[]): {
|
function mapConfig(workflow: z.infer<typeof Workflow>): {
|
||||||
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>;
|
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>;
|
||||||
toolConfig: Record<string, z.infer<typeof WorkflowTool>>;
|
toolConfig: Record<string, z.infer<typeof WorkflowTool>>;
|
||||||
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>;
|
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>;
|
||||||
|
|
@ -764,10 +773,7 @@ function mapConfig(workflow: z.infer<typeof Workflow>, projectTools: z.infer<typ
|
||||||
...acc,
|
...acc,
|
||||||
[agent.name]: agent
|
[agent.name]: agent
|
||||||
}), {});
|
}), {});
|
||||||
const toolConfig: Record<string, z.infer<typeof WorkflowTool>> = [
|
const toolConfig: Record<string, z.infer<typeof WorkflowTool>> = workflow.tools.reduce((acc, tool) => ({
|
||||||
...workflow.tools,
|
|
||||||
...projectTools,
|
|
||||||
].reduce((acc, tool) => ({
|
|
||||||
...acc,
|
...acc,
|
||||||
[tool.name]: tool
|
[tool.name]: tool
|
||||||
}), {});
|
}), {});
|
||||||
|
|
@ -809,15 +815,15 @@ function createTools(
|
||||||
mockInstructions: workflow.mockTools?.[toolName], // override mock instructions
|
mockInstructions: workflow.mockTools?.[toolName], // override mock instructions
|
||||||
});
|
});
|
||||||
logger.log(`created mock tool: ${toolName}`);
|
logger.log(`created mock tool: ${toolName}`);
|
||||||
|
} else if (config.mockTool) {
|
||||||
|
tools[toolName] = createMockTool(logger, config);
|
||||||
|
logger.log(`created mock tool: ${toolName}`);
|
||||||
} else if (config.isMcp) {
|
} else if (config.isMcp) {
|
||||||
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);
|
||||||
logger.log(`created composio tool: ${toolName}`);
|
logger.log(`created composio tool: ${toolName}`);
|
||||||
} else if (config.mockTool) {
|
|
||||||
tools[toolName] = createMockTool(logger, config);
|
|
||||||
logger.log(`created mock tool: ${toolName}`);
|
|
||||||
} else {
|
} else {
|
||||||
tools[toolName] = createWebhookTool(logger, config, projectId);
|
tools[toolName] = createWebhookTool(logger, config, projectId);
|
||||||
logger.log(`created webhook tool: ${toolName}`);
|
logger.log(`created webhook tool: ${toolName}`);
|
||||||
|
|
@ -832,7 +838,6 @@ function createAgents(
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>,
|
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>,
|
||||||
tools: Record<string, Tool>,
|
tools: Record<string, Tool>,
|
||||||
projectTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>,
|
promptConfig: Record<string, z.infer<typeof WorkflowPrompt>>,
|
||||||
): { agents: Record<string, Agent>, mentions: Record<string, z.infer<typeof ConnectedEntity>[]>, originalInstructions: Record<string, string>, originalHandoffs: Record<string, Agent[]> } {
|
): { agents: Record<string, Agent>, mentions: Record<string, z.infer<typeof ConnectedEntity>[]>, originalInstructions: Record<string, string>, originalHandoffs: Record<string, Agent[]> } {
|
||||||
const agents: Record<string, Agent> = {};
|
const agents: Record<string, Agent> = {};
|
||||||
|
|
@ -847,7 +852,6 @@ function createAgents(
|
||||||
projectId,
|
projectId,
|
||||||
config,
|
config,
|
||||||
tools,
|
tools,
|
||||||
projectTools,
|
|
||||||
workflow,
|
workflow,
|
||||||
promptConfig,
|
promptConfig,
|
||||||
);
|
);
|
||||||
|
|
@ -928,7 +932,6 @@ function maybeInjectGiveUpControlInstructions(
|
||||||
export async function* streamResponse(
|
export async function* streamResponse(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
projectTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
messages: z.infer<typeof Message>[],
|
messages: z.infer<typeof Message>[],
|
||||||
): AsyncIterable<z.infer<typeof ZOutMessage> | z.infer<typeof ZUsage>> {
|
): AsyncIterable<z.infer<typeof ZOutMessage> | z.infer<typeof ZUsage>> {
|
||||||
// Divider log for tracking agent loop start
|
// Divider log for tracking agent loop start
|
||||||
|
|
@ -947,7 +950,7 @@ export async function* streamResponse(
|
||||||
}
|
}
|
||||||
|
|
||||||
// create map of agent, tool and prompt configs
|
// create map of agent, tool and prompt configs
|
||||||
const { agentConfig, toolConfig, promptConfig } = mapConfig(workflow, projectTools);
|
const { agentConfig, toolConfig, promptConfig } = mapConfig(workflow);
|
||||||
|
|
||||||
|
|
||||||
const stack: string[] = [];
|
const stack: string[] = [];
|
||||||
|
|
@ -957,7 +960,7 @@ export async function* streamResponse(
|
||||||
const tools = createTools(logger, projectId, workflow, toolConfig);
|
const tools = createTools(logger, projectId, workflow, toolConfig);
|
||||||
|
|
||||||
// create agents
|
// create agents
|
||||||
const { agents, originalInstructions, originalHandoffs } = createAgents(logger, projectId, workflow, agentConfig, tools, projectTools, promptConfig);
|
const { agents, originalInstructions, originalHandoffs } = createAgents(logger, projectId, workflow, agentConfig, tools, promptConfig);
|
||||||
|
|
||||||
// track agent to agent calls
|
// track agent to agent calls
|
||||||
const transferCounter = new AgentTransferCounter();
|
const transferCounter = new AgentTransferCounter();
|
||||||
|
|
@ -1213,7 +1216,6 @@ export async function* streamResponse(
|
||||||
export async function getResponse(
|
export async function getResponse(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
projectTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
messages: z.infer<typeof Message>[],
|
messages: z.infer<typeof Message>[],
|
||||||
): Promise<{
|
): Promise<{
|
||||||
messages: z.infer<typeof ZOutMessage>[],
|
messages: z.infer<typeof ZOutMessage>[],
|
||||||
|
|
@ -1227,7 +1229,7 @@ export async function getResponse(
|
||||||
completion: 0,
|
completion: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
for await (const event of streamResponse(projectId, workflow, projectTools, messages)) {
|
for await (const event of streamResponse(projectId, workflow, messages)) {
|
||||||
if ('role' in event) {
|
if ('role' in event) {
|
||||||
out.push(event);
|
out.push(event);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,11 +278,14 @@ export async function getToolkit(toolkitSlug: string): Promise<z.infer<typeof ZG
|
||||||
return composioApiCall(ZGetToolkitResponse, url.toString());
|
return composioApiCall(ZGetToolkitResponse, url.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listTools(toolkitSlug: string, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
export async function listTools(toolkitSlug: string, searchQuery: string | null = null, cursor: string | null = null): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||||
const url = new URL(`${BASE_URL}/tools`);
|
const url = new URL(`${BASE_URL}/tools`);
|
||||||
|
|
||||||
// set params
|
// set params
|
||||||
url.searchParams.set("toolkit_slug", toolkitSlug);
|
url.searchParams.set("toolkit_slug", toolkitSlug);
|
||||||
|
if (searchQuery) {
|
||||||
|
url.searchParams.set("search", searchQuery);
|
||||||
|
}
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
url.searchParams.set("cursor", cursor);
|
url.searchParams.set("cursor", cursor);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { projectsCollection } from "./mongodb";
|
|
||||||
import { WorkflowTool } from "./types/workflow_types";
|
|
||||||
|
|
||||||
export async function collectProjectTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
|
||||||
const tools: z.infer<typeof WorkflowTool>[] = [];
|
|
||||||
|
|
||||||
// Get project data
|
|
||||||
const project = await projectsCollection.findOne({ _id: projectId });
|
|
||||||
if (!project) {
|
|
||||||
throw new Error(`Project ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert MCP tools to workflow tools format, but only from ready servers
|
|
||||||
if (project.mcpServers) {
|
|
||||||
for (const server of project.mcpServers) {
|
|
||||||
if (server.isReady) {
|
|
||||||
for (const tool of server.tools) {
|
|
||||||
tools.push({
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description || "",
|
|
||||||
parameters: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: tool.parameters?.properties || {},
|
|
||||||
required: tool.parameters?.required || []
|
|
||||||
},
|
|
||||||
isMcp: true,
|
|
||||||
mcpServerName: server.name,
|
|
||||||
mcpServerURL: server.serverUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Composio tools
|
|
||||||
if (project.composioSelectedTools) {
|
|
||||||
for (const tool of project.composioSelectedTools) {
|
|
||||||
tools.push({
|
|
||||||
name: tool.slug,
|
|
||||||
description: tool.description || "",
|
|
||||||
parameters: {
|
|
||||||
type: 'object' as const,
|
|
||||||
properties: tool.input_parameters?.properties || {},
|
|
||||||
required: tool.input_parameters?.required || []
|
|
||||||
},
|
|
||||||
isComposio: true,
|
|
||||||
composioData: {
|
|
||||||
slug: tool.slug,
|
|
||||||
noAuth: tool.no_auth,
|
|
||||||
toolkitName: tool.toolkit.name,
|
|
||||||
toolkitSlug: tool.toolkit.slug,
|
|
||||||
logo: tool.toolkit.logo,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
||||||
|
|
@ -15,6 +14,10 @@ export const ComposioConnectedAccount = z.object({
|
||||||
lastUpdatedAt: z.string().datetime(),
|
lastUpdatedAt: z.string().datetime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const CustomMcpServer = z.object({
|
||||||
|
serverUrl: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Project = z.object({
|
export const Project = z.object({
|
||||||
_id: z.string().uuid(),
|
_id: z.string().uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|
@ -30,7 +33,7 @@ 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(),
|
customMcpServers: z.record(z.string(), CustomMcpServer).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ProjectMember = z.object({
|
export const ProjectMember = z.object({
|
||||||
|
|
@ -45,20 +48,4 @@ export const ApiKey = z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
createdAt: z.string().datetime(),
|
createdAt: z.string().datetime(),
|
||||||
lastUsedAt: z.string().datetime().optional(),
|
lastUsedAt: z.string().datetime().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function mergeProjectTools(
|
|
||||||
workflowTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
projectTools: z.infer<typeof WorkflowTool>[]
|
|
||||||
): z.infer<typeof WorkflowTool>[] {
|
|
||||||
// Filter out any existing MCP tools from workflow tools
|
|
||||||
const nonMcpTools = workflowTools.filter(t => !t.isMcp);
|
|
||||||
|
|
||||||
// Merge with project tools
|
|
||||||
const merged = [
|
|
||||||
...nonMcpTools,
|
|
||||||
...projectTools
|
|
||||||
];
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
@ -210,6 +210,5 @@ export function convertMcpServerToolToWorkflowTool(
|
||||||
export const ZStreamAgentResponsePayload = z.object({
|
export const ZStreamAgentResponsePayload = z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
projectTools: z.array(WorkflowTool),
|
|
||||||
messages: z.array(Message),
|
messages: z.array(Message),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ export const WorkflowTool = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
mockTool: z.boolean().default(false).optional(),
|
mockTool: z.boolean().default(false).optional(),
|
||||||
autoSubmitMockedResponse: z.boolean().default(false).optional(),
|
|
||||||
mockInstructions: z.string().optional(),
|
mockInstructions: z.string().optional(),
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
type: z.literal('object'),
|
type: z.literal('object'),
|
||||||
|
|
@ -48,10 +47,9 @@ export const WorkflowTool = z.object({
|
||||||
additionalProperties: z.boolean().optional(),
|
additionalProperties: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
isMcp: z.boolean().default(false).optional(),
|
isMcp: z.boolean().default(false).optional(),
|
||||||
isLibrary: z.boolean().default(false).optional(),
|
|
||||||
mcpServerName: z.string().optional(),
|
mcpServerName: z.string().optional(),
|
||||||
mcpServerURL: z.string().optional(),
|
|
||||||
isComposio: z.boolean().optional(), // whether this is a Composio tool
|
isComposio: z.boolean().optional(), // whether this is a Composio tool
|
||||||
|
isLibrary: z.boolean().default(false).optional(), // whether this is a library tool
|
||||||
composioData: z.object({
|
composioData: z.object({
|
||||||
slug: z.string(), // the slug for the Composio tool e.g. "GITHUB_CREATE_AN_ISSUE"
|
slug: z.string(), // the slug for the Composio tool e.g. "GITHUB_CREATE_AN_ISSUE"
|
||||||
noAuth: z.boolean(), // whether the tool requires no authentication
|
noAuth: z.boolean(), // whether the tool requires no authentication
|
||||||
|
|
@ -89,7 +87,6 @@ export function sanitizeTextWithMentions(
|
||||||
tools: z.infer<typeof WorkflowTool>[],
|
tools: z.infer<typeof WorkflowTool>[],
|
||||||
prompts: z.infer<typeof WorkflowPrompt>[],
|
prompts: z.infer<typeof WorkflowPrompt>[],
|
||||||
},
|
},
|
||||||
projectTools: z.infer<typeof WorkflowTool>[] = []
|
|
||||||
): {
|
): {
|
||||||
sanitized: string;
|
sanitized: string;
|
||||||
entities: z.infer<typeof ConnectedEntity>[];
|
entities: z.infer<typeof ConnectedEntity>[];
|
||||||
|
|
@ -119,8 +116,7 @@ export function sanitizeTextWithMentions(
|
||||||
if (entity.type === 'agent') {
|
if (entity.type === 'agent') {
|
||||||
return workflow.agents.some(a => a.name === entity.name);
|
return workflow.agents.some(a => a.name === entity.name);
|
||||||
} else if (entity.type === 'tool') {
|
} else if (entity.type === 'tool') {
|
||||||
return workflow.tools.some(t => t.name === entity.name) ||
|
return workflow.tools.some(t => t.name === entity.name);
|
||||||
projectTools.some(t => t.name === entity.name);
|
|
||||||
} else if (entity.type === 'prompt') {
|
} else if (entity.type === 'prompt') {
|
||||||
return workflow.prompts.some(p => p.name === entity.name);
|
return workflow.prompts.some(p => p.name === entity.name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { Message, ZStreamAgentResponsePayload } from "./types/types";
|
||||||
export async function getAgenticResponseStreamId(
|
export async function getAgenticResponseStreamId(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
workflow: z.infer<typeof Workflow>,
|
workflow: z.infer<typeof Workflow>,
|
||||||
projectTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
messages: z.infer<typeof Message>[],
|
messages: z.infer<typeof Message>[],
|
||||||
): Promise<{
|
): Promise<{
|
||||||
streamId: string,
|
streamId: string,
|
||||||
|
|
@ -16,7 +15,6 @@ export async function getAgenticResponseStreamId(
|
||||||
const payload: z.infer<typeof ZStreamAgentResponsePayload> = {
|
const payload: z.infer<typeof ZStreamAgentResponsePayload> = {
|
||||||
projectId,
|
projectId,
|
||||||
workflow,
|
workflow,
|
||||||
projectTools,
|
|
||||||
messages,
|
messages,
|
||||||
}
|
}
|
||||||
// serialize the request
|
// serialize the request
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ export function AgentConfig({
|
||||||
usedAgentNames,
|
usedAgentNames,
|
||||||
agents,
|
agents,
|
||||||
tools,
|
tools,
|
||||||
projectTools,
|
|
||||||
prompts,
|
prompts,
|
||||||
dataSources,
|
dataSources,
|
||||||
handleUpdate,
|
handleUpdate,
|
||||||
|
|
@ -57,7 +56,6 @@ export function AgentConfig({
|
||||||
usedAgentNames: Set<string>,
|
usedAgentNames: Set<string>,
|
||||||
agents: z.infer<typeof WorkflowAgent>[],
|
agents: z.infer<typeof WorkflowAgent>[],
|
||||||
tools: z.infer<typeof WorkflowTool>[],
|
tools: z.infer<typeof WorkflowTool>[],
|
||||||
projectTools: z.infer<typeof WorkflowTool>[],
|
|
||||||
prompts: z.infer<typeof WorkflowPrompt>[],
|
prompts: z.infer<typeof WorkflowPrompt>[],
|
||||||
dataSources: WithStringId<z.infer<typeof DataSource>>[],
|
dataSources: WithStringId<z.infer<typeof DataSource>>[],
|
||||||
handleUpdate: (agent: z.infer<typeof WorkflowAgent>) => void,
|
handleUpdate: (agent: z.infer<typeof WorkflowAgent>) => void,
|
||||||
|
|
@ -172,7 +170,7 @@ export function AgentConfig({
|
||||||
const atMentions = createAtMentions({
|
const atMentions = createAtMentions({
|
||||||
agents,
|
agents,
|
||||||
prompts,
|
prompts,
|
||||||
tools: [...tools, ...projectTools],
|
tools,
|
||||||
currentAgentName: agent.name
|
currentAgentName: agent.name
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { WorkflowTool } from "../../../lib/types/workflow_types";
|
import { WorkflowTool } from "../../../lib/types/workflow_types";
|
||||||
import { Checkbox, Select, SelectItem, RadioGroup, Radio } from "@heroui/react";
|
import { Checkbox, Select, SelectItem, Switch } from "@heroui/react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ImportIcon, XIcon, PlusIcon, FolderIcon} from "lucide-react";
|
import { ImportIcon, XIcon, PlusIcon, FolderIcon, Globe, Zap, ExternalLink } from "lucide-react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
import { Panel } from "@/components/common/panel-common";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -12,6 +13,7 @@ import { SectionCard } from "@/components/common/section-card";
|
||||||
import { ToolParamCard } from "@/components/common/tool-param-card";
|
import { ToolParamCard } from "@/components/common/tool-param-card";
|
||||||
import { UserIcon, Settings, Settings2 } from "lucide-react";
|
import { UserIcon, Settings, Settings2 } from "lucide-react";
|
||||||
import { EditableField } from "@/app/lib/components/editable-field";
|
import { EditableField } from "@/app/lib/components/editable-field";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
// Update textarea styles with improved states
|
// Update textarea styles with improved states
|
||||||
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500";
|
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500";
|
||||||
|
|
@ -173,8 +175,11 @@ export function ToolConfig({
|
||||||
required: tool.parameters?.required || []
|
required: tool.parameters?.required || []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
const projectId = params.projectId as string;
|
||||||
const [selectedParams, setSelectedParams] = useState(new Set([]));
|
const [selectedParams, setSelectedParams] = useState(new Set([]));
|
||||||
const isReadOnly = tool.isMcp || tool.isLibrary || tool.isComposio;
|
const isReadOnly = tool.isMcp || tool.isComposio;
|
||||||
|
const isWebhookTool = !tool.isMcp && !tool.isComposio;
|
||||||
const [nameError, setNameError] = useState<string | null>(null);
|
const [nameError, setNameError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Log when parameters are being rendered
|
// Log when parameters are being rendered
|
||||||
|
|
@ -337,6 +342,57 @@ export function ToolConfig({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4 pb-4 pt-4 p-4">
|
<div className="flex flex-col gap-4 pb-4 pt-4 p-4">
|
||||||
|
{/* Tool Type Section */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
{tool.isMcp ? (
|
||||||
|
<ImportIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
) : tool.isComposio ? (
|
||||||
|
<Zap className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||||
|
) : (
|
||||||
|
<Globe className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-2">
|
||||||
|
How this tool runs
|
||||||
|
</h3>
|
||||||
|
{tool.isMcp ? (
|
||||||
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<p>This tool is powered by the <span className="font-medium text-blue-700 dark:text-blue-300">{tool.mcpServerName}</span> MCP server.</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
MCP (Model Context Protocol) tools are external services that provide additional capabilities to your agent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : tool.isComposio ? (
|
||||||
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<p>This tool is powered by <span className="font-medium text-purple-700 dark:text-purple-300">Composio</span></p>
|
||||||
|
{tool.composioData?.toolkitName && (
|
||||||
|
<span className="text-xs bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300 px-2 py-1 rounded-full">
|
||||||
|
{tool.composioData.toolkitName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Composio provides pre-built integrations with popular services and APIs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<div className="flex items-center gap-1 mb-1">
|
||||||
|
<p>This tool is invoked using the webhook configured in <Link href={`/projects/${projectId}/config`} className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 font-medium underline decoration-green-300 hover:decoration-green-500 transition-colors">project settings</Link></p>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Webhook tools make HTTP requests to your configured endpoint when called by the agent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Identity Section */}
|
{/* Identity Section */}
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<UserIcon className="w-5 h-5 text-indigo-500" />}
|
icon={<UserIcon className="w-5 h-5 text-indigo-500" />}
|
||||||
|
|
@ -350,6 +406,7 @@ export function ToolConfig({
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<EditableField
|
<EditableField
|
||||||
value={tool.name}
|
value={tool.name}
|
||||||
|
locked={isReadOnly}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setNameError(validateToolName(value));
|
setNameError(validateToolName(value));
|
||||||
if (!validateToolName(value)) {
|
if (!validateToolName(value)) {
|
||||||
|
|
@ -375,6 +432,7 @@ export function ToolConfig({
|
||||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<EditableField
|
<EditableField
|
||||||
|
locked={isReadOnly}
|
||||||
value={tool.description || ""}
|
value={tool.description || ""}
|
||||||
onChange={(value) => handleUpdate({ ...tool, description: value })}
|
onChange={(value) => handleUpdate({ ...tool, description: value })}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
|
|
@ -385,56 +443,37 @@ export function ToolConfig({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
{/* Behavior Section */}
|
{/* Mock Section */}
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Settings className="w-5 h-5 text-indigo-500" />}
|
icon={<Settings className="w-5 h-5 text-indigo-500" />}
|
||||||
title="Behavior"
|
title="Mock responses"
|
||||||
labelWidth="md:w-32"
|
labelWidth="md:w-64"
|
||||||
className="mb-1"
|
className="mb-1"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{!isReadOnly && (
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-1">Tool Mode</label>
|
<Switch
|
||||||
<RadioGroup
|
isSelected={tool.mockTool}
|
||||||
defaultValue="mock"
|
|
||||||
value={tool.mockTool ? "mock" : "api"}
|
|
||||||
onValueChange={(value) => handleUpdate({
|
onValueChange={(value) => handleUpdate({
|
||||||
...tool,
|
...tool,
|
||||||
mockTool: value === "mock",
|
mockTool: value,
|
||||||
autoSubmitMockedResponse: value === "mock" ? true : undefined
|
|
||||||
})}
|
})}
|
||||||
orientation="horizontal"
|
size="sm"
|
||||||
classNames={{
|
color="primary"
|
||||||
wrapper: "flex flex-col md:flex-row gap-2 md:gap-8 pl-0 md:pl-3",
|
/>
|
||||||
label: "text-sm"
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300">
|
||||||
}}
|
Mock tool responses
|
||||||
>
|
</label>
|
||||||
<Radio
|
|
||||||
value="mock"
|
|
||||||
classNames={{
|
|
||||||
base: "px-2 py-1 rounded-lg transition-colors",
|
|
||||||
label: "text-sm font-normal text-gray-900 dark:text-gray-100 px-3 py-1"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Mock tool responses
|
|
||||||
</Radio>
|
|
||||||
<Radio
|
|
||||||
value="api"
|
|
||||||
classNames={{
|
|
||||||
base: "px-2 py-1 rounded-lg transition-colors",
|
|
||||||
label: "text-sm font-normal text-gray-900 dark:text-gray-100 px-3 py-1"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Connect tool to your API
|
|
||||||
</Radio>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<span className="text-xs text-gray-500 dark:text-gray-400 ml-12">
|
||||||
|
When enabled, this tool will be mocked.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{tool.mockTool && (
|
{tool.mockTool && (
|
||||||
<div className="flex flex-col gap-2 pl-0 md:pl-3 mt-2">
|
<div className="flex flex-col gap-2 ml-12">
|
||||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-1">Mock Response Schema</label>
|
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-1">Mock Response Instructions</label>
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 mb-1">Describe the response the mock tool should return. This will be shown in the chat when the tool is called. You can also provide a JSON schema for the response.</span>
|
<span className="text-xs text-gray-500 dark:text-gray-400 mb-1">Describe the response the mock tool should return. This will be shown in the chat when the tool is called.</span>
|
||||||
<EditableField
|
<EditableField
|
||||||
value={tool.mockInstructions || ''}
|
value={tool.mockInstructions || ''}
|
||||||
onChange={(value) => handleUpdate({
|
onChange={(value) => handleUpdate({
|
||||||
|
|
@ -445,20 +484,6 @@ export function ToolConfig({
|
||||||
placeholder="Mock response instructions..."
|
placeholder="Mock response instructions..."
|
||||||
className="w-full text-xs p-2 bg-white dark:bg-gray-900"
|
className="w-full text-xs p-2 bg-white dark:bg-gray-900"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
|
||||||
size="sm"
|
|
||||||
isSelected={tool.autoSubmitMockedResponse ?? true}
|
|
||||||
onValueChange={(value) => handleUpdate({
|
|
||||||
...tool,
|
|
||||||
autoSubmitMockedResponse: value
|
|
||||||
})}
|
|
||||||
disabled={isReadOnly}
|
|
||||||
className="mt-2"
|
|
||||||
>
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
Automatically send mock response in chat
|
|
||||||
</span>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,8 @@ export function App({
|
||||||
workflow,
|
workflow,
|
||||||
messageSubscriber,
|
messageSubscriber,
|
||||||
mcpServerUrls,
|
mcpServerUrls,
|
||||||
toolWebhookUrl,
|
|
||||||
isInitialState = false,
|
isInitialState = false,
|
||||||
onPanelClick,
|
onPanelClick,
|
||||||
projectTools,
|
|
||||||
triggerCopilotChat,
|
triggerCopilotChat,
|
||||||
}: {
|
}: {
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
|
@ -30,10 +28,8 @@ export function App({
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
messageSubscriber?: (messages: z.infer<typeof Message>[]) => void;
|
messageSubscriber?: (messages: z.infer<typeof Message>[]) => void;
|
||||||
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||||
toolWebhookUrl: string;
|
|
||||||
isInitialState?: boolean;
|
isInitialState?: boolean;
|
||||||
onPanelClick?: () => void;
|
onPanelClick?: () => void;
|
||||||
projectTools: z.infer<typeof WorkflowTool>[];
|
|
||||||
triggerCopilotChat?: (message: string) => void;
|
triggerCopilotChat?: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [counter, setCounter] = useState<number>(0);
|
const [counter, setCounter] = useState<number>(0);
|
||||||
|
|
@ -153,10 +149,8 @@ export function App({
|
||||||
systemMessage={systemMessage}
|
systemMessage={systemMessage}
|
||||||
onSystemMessageChange={handleSystemMessageChange}
|
onSystemMessageChange={handleSystemMessageChange}
|
||||||
mcpServerUrls={mcpServerUrls}
|
mcpServerUrls={mcpServerUrls}
|
||||||
toolWebhookUrl={toolWebhookUrl}
|
|
||||||
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
|
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
|
||||||
showDebugMessages={showDebugMessages}
|
showDebugMessages={showDebugMessages}
|
||||||
projectTools={projectTools}
|
|
||||||
triggerCopilotChat={triggerCopilotChat}
|
triggerCopilotChat={triggerCopilotChat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,9 @@ export function Chat({
|
||||||
systemMessage,
|
systemMessage,
|
||||||
onSystemMessageChange,
|
onSystemMessageChange,
|
||||||
mcpServerUrls,
|
mcpServerUrls,
|
||||||
toolWebhookUrl,
|
|
||||||
onCopyClick,
|
onCopyClick,
|
||||||
showDebugMessages = true,
|
showDebugMessages = true,
|
||||||
showJsonMode = false,
|
showJsonMode = false,
|
||||||
projectTools,
|
|
||||||
triggerCopilotChat,
|
triggerCopilotChat,
|
||||||
}: {
|
}: {
|
||||||
chat: z.infer<typeof PlaygroundChat>;
|
chat: z.infer<typeof PlaygroundChat>;
|
||||||
|
|
@ -36,11 +34,9 @@ export function Chat({
|
||||||
systemMessage: string;
|
systemMessage: string;
|
||||||
onSystemMessageChange: (message: string) => void;
|
onSystemMessageChange: (message: string) => void;
|
||||||
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||||
toolWebhookUrl: string;
|
|
||||||
onCopyClick: (fn: () => string) => void;
|
onCopyClick: (fn: () => string) => void;
|
||||||
showDebugMessages?: boolean;
|
showDebugMessages?: boolean;
|
||||||
showJsonMode?: boolean;
|
showJsonMode?: boolean;
|
||||||
projectTools: z.infer<typeof WorkflowTool>[];
|
|
||||||
triggerCopilotChat?: (message: string) => void;
|
triggerCopilotChat?: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [messages, setMessages] = useState<z.infer<typeof Message>[]>(chat.messages);
|
const [messages, setMessages] = useState<z.infer<typeof Message>[]>(chat.messages);
|
||||||
|
|
@ -210,7 +206,6 @@ export function Chat({
|
||||||
const response = await getAssistantResponseStreamId(
|
const response = await getAssistantResponseStreamId(
|
||||||
projectId,
|
projectId,
|
||||||
workflow,
|
workflow,
|
||||||
projectTools,
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
|
|
@ -336,9 +331,7 @@ export function Chat({
|
||||||
workflow,
|
workflow,
|
||||||
systemMessage,
|
systemMessage,
|
||||||
mcpServerUrls,
|
mcpServerUrls,
|
||||||
toolWebhookUrl,
|
|
||||||
fetchResponseError,
|
fetchResponseError,
|
||||||
projectTools,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Add a stop handler function
|
// Add a stop handler function
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { WebhookConfig } from './WebhookConfig';
|
||||||
|
import { Button } from '@heroui/react';
|
||||||
|
import { WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface AddWebhookToolProps {
|
||||||
|
projectId: string;
|
||||||
|
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddWebhookTool({ projectId, onAddTool }: AddWebhookToolProps) {
|
||||||
|
function handleAddTool() {
|
||||||
|
onAddTool({
|
||||||
|
description: 'Webhook tool',
|
||||||
|
mockTool: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
Add webhook tool
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WebhookConfig projectId={projectId} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Click here to add a webhook tool:
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
color="primary"
|
||||||
|
onPress={handleAddTool}
|
||||||
|
>Add webhook tool</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,25 +3,30 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Info, RefreshCw, Search } from 'lucide-react';
|
import { RefreshCw, Search } from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { listToolkits, listTools, updateComposioSelectedTools } from '@/app/actions/composio_actions';
|
import { listToolkits } 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';
|
||||||
import { Project } from '@/app/lib/types/project_types';
|
import { Project } from '@/app/lib/types/project_types';
|
||||||
import { ComposioToolsPanel } from './ComposioToolsPanel';
|
import { ComposioToolsPanel } from './ComposioToolsPanel';
|
||||||
import { ToolkitCard } from './ToolkitCard';
|
import { ToolkitCard } from './ToolkitCard';
|
||||||
|
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
|
||||||
type ToolkitType = z.infer<typeof ZToolkit>;
|
type ToolkitType = z.infer<typeof ZToolkit>;
|
||||||
type ToolkitListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>;
|
type ToolkitListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>;
|
||||||
type ProjectType = z.infer<typeof Project>;
|
type ProjectType = z.infer<typeof Project>;
|
||||||
|
|
||||||
export function Composio() {
|
export function Composio({
|
||||||
const params = useParams();
|
projectId,
|
||||||
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
|
tools,
|
||||||
if (!projectId) throw new Error('Project ID is required');
|
onAddTool
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
tools: z.infer<typeof Workflow.shape.tools>;
|
||||||
|
onAddTool: (tool: z.infer<typeof WorkflowTool>) => void;
|
||||||
|
}) {
|
||||||
const [toolkits, setToolkits] = useState<ToolkitType[]>([]);
|
const [toolkits, setToolkits] = useState<ToolkitType[]>([]);
|
||||||
const [projectConfig, setProjectConfig] = useState<ProjectType | null>(null);
|
const [projectConfig, setProjectConfig] = useState<ProjectType | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -29,7 +34,6 @@ export function Composio() {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
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 loadProjectConfig = useCallback(async () => {
|
const loadProjectConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -75,7 +79,7 @@ export function Composio() {
|
||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
const handleManageTools = useCallback((toolkit: ToolkitType) => {
|
const handleSelectToolkit = useCallback((toolkit: ToolkitType) => {
|
||||||
setSelectedToolkit(toolkit);
|
setSelectedToolkit(toolkit);
|
||||||
setIsToolsPanelOpen(true);
|
setIsToolsPanelOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -85,64 +89,6 @@ export function Composio() {
|
||||||
setIsToolsPanelOpen(false);
|
setIsToolsPanelOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleProjectConfigUpdate = useCallback(() => {
|
|
||||||
loadProjectConfig();
|
|
||||||
}, [loadProjectConfig]);
|
|
||||||
|
|
||||||
const handleUpdateToolsSelection = useCallback(async (selectedToolObjects: z.infer<typeof ZTool>[]) => {
|
|
||||||
if (!projectId) return;
|
|
||||||
|
|
||||||
setSavingTools(true);
|
|
||||||
try {
|
|
||||||
// Get existing selected tools from project config
|
|
||||||
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
|
|
||||||
|
|
||||||
// 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 project config to get updated data
|
|
||||||
await loadProjectConfig();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving tool selection:', error);
|
|
||||||
} finally {
|
|
||||||
setSavingTools(false);
|
|
||||||
}
|
|
||||||
}, [projectId, projectConfig, loadProjectConfig]);
|
|
||||||
|
|
||||||
const handleRemoveToolkitTools = useCallback(async (toolkitSlug: string) => {
|
|
||||||
if (!projectId) return;
|
|
||||||
|
|
||||||
setSavingTools(true);
|
|
||||||
try {
|
|
||||||
// Get existing selected tools from project config
|
|
||||||
const existingSelectedTools = projectConfig?.composioSelectedTools || [];
|
|
||||||
|
|
||||||
// Filter out all tools from the specified toolkit
|
|
||||||
const filteredSelectedTools = existingSelectedTools.filter(tool =>
|
|
||||||
tool.toolkit.slug !== toolkitSlug
|
|
||||||
);
|
|
||||||
|
|
||||||
await updateComposioSelectedTools(projectId, filteredSelectedTools);
|
|
||||||
|
|
||||||
// Refresh project config to get updated data
|
|
||||||
await loadProjectConfig();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error removing toolkit tools:', error);
|
|
||||||
} finally {
|
|
||||||
setSavingTools(false);
|
|
||||||
}
|
|
||||||
}, [projectId, projectConfig, loadProjectConfig]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProjectConfig();
|
loadProjectConfig();
|
||||||
}, [loadProjectConfig]);
|
}, [loadProjectConfig]);
|
||||||
|
|
@ -245,19 +191,14 @@ export function Composio() {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{filteredToolkits.map((toolkit) => {
|
{filteredToolkits.map((toolkit) => {
|
||||||
const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
const isConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
||||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolkitCard
|
<ToolkitCard
|
||||||
key={toolkit.slug}
|
key={toolkit.slug}
|
||||||
toolkit={toolkit}
|
toolkit={toolkit}
|
||||||
projectId={projectId}
|
|
||||||
isConnected={isConnected}
|
isConnected={isConnected}
|
||||||
connectedAccountId={connectedAccountId}
|
workflowTools={tools}
|
||||||
projectConfig={projectConfig}
|
onSelectToolkit={() => handleSelectToolkit(toolkit)}
|
||||||
onManageTools={() => handleManageTools(toolkit)}
|
|
||||||
onProjectConfigUpdate={handleProjectConfigUpdate}
|
|
||||||
onRemoveToolkitTools={handleRemoveToolkitTools}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -272,16 +213,13 @@ export function Composio() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tools Panel */}
|
{/* Tools Panel */}
|
||||||
<ComposioToolsPanel
|
{selectedToolkit && <ComposioToolsPanel
|
||||||
toolkit={selectedToolkit}
|
toolkit={selectedToolkit}
|
||||||
isOpen={isToolsPanelOpen}
|
isOpen={isToolsPanelOpen}
|
||||||
onClose={handleCloseToolsPanel}
|
onClose={handleCloseToolsPanel}
|
||||||
projectConfig={projectConfig}
|
tools={tools}
|
||||||
onUpdateToolsSelection={handleUpdateToolsSelection}
|
onAddTool={onAddTool}
|
||||||
onProjectConfigUpdate={handleProjectConfigUpdate}
|
/>}
|
||||||
onRemoveToolkitTools={handleRemoveToolkitTools}
|
|
||||||
isSaving={savingTools}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
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, Input } from '@heroui/react';
|
||||||
import { ChevronLeft, ChevronRight, LinkIcon, Loader2, UnlinkIcon } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Search, X } from 'lucide-react';
|
||||||
import { listTools, deleteConnectedAccount } from '@/app/actions/composio_actions';
|
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
import { listTools } from '@/app/actions/composio_actions';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
import { ZTool, ZListResponse } from '@/app/lib/composio/composio';
|
||||||
import { SlidePanel } from '@/components/ui/slide-panel';
|
import { SlidePanel } from '@/components/ui/slide-panel';
|
||||||
import { Project } from '@/app/lib/types/project_types';
|
|
||||||
import { ToolkitAuthModal } from './ToolkitAuthModal';
|
|
||||||
|
|
||||||
type ToolType = z.infer<typeof ZTool>;
|
type ToolType = z.infer<typeof ZTool>;
|
||||||
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
|
type ToolListResponse = z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>;
|
||||||
type ProjectType = z.infer<typeof Project>;
|
|
||||||
|
|
||||||
interface ComposioToolsPanelProps {
|
interface ComposioToolsPanelProps {
|
||||||
toolkit: {
|
toolkit: {
|
||||||
|
|
@ -24,25 +22,19 @@ interface ComposioToolsPanelProps {
|
||||||
logo: string;
|
logo: string;
|
||||||
};
|
};
|
||||||
no_auth?: boolean;
|
no_auth?: boolean;
|
||||||
} | null;
|
};
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
projectConfig: ProjectType | null;
|
tools: z.infer<typeof Workflow.shape.tools>;
|
||||||
onUpdateToolsSelection: (selectedToolObjects: ToolType[]) => void;
|
onAddTool: (tool: z.infer<typeof WorkflowTool>) => void;
|
||||||
onProjectConfigUpdate: () => void;
|
|
||||||
onRemoveToolkitTools: (toolkitSlug: string) => void;
|
|
||||||
isSaving: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComposioToolsPanel({
|
export function ComposioToolsPanel({
|
||||||
toolkit,
|
toolkit,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
projectConfig,
|
tools: workflowTools,
|
||||||
onUpdateToolsSelection,
|
onAddTool,
|
||||||
onProjectConfigUpdate,
|
|
||||||
onRemoveToolkitTools,
|
|
||||||
isSaving
|
|
||||||
}: ComposioToolsPanelProps) {
|
}: ComposioToolsPanelProps) {
|
||||||
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];
|
||||||
|
|
@ -55,14 +47,30 @@ export function ComposioToolsPanel({
|
||||||
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
||||||
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [isProcessingAuth, setIsProcessingAuth] = useState(false);
|
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
|
||||||
|
|
||||||
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null) => {
|
const selectedToolSlugs = workflowTools
|
||||||
|
.filter(tool => tool.isComposio && tool.composioData?.toolkitSlug === toolkit.slug)
|
||||||
|
.map(tool => tool.composioData!.slug);
|
||||||
|
|
||||||
|
// Filter out already selected tools
|
||||||
|
const availableTools = tools.filter(tool => !selectedToolSlugs.includes(tool.slug));
|
||||||
|
|
||||||
|
// Debounce search query
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchQuery(searchQuery);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
const loadToolsForToolkit = useCallback(async (toolkitSlug: string, cursor: string | null = null, search: string | null = null) => {
|
||||||
try {
|
try {
|
||||||
setToolsLoading(true);
|
setToolsLoading(true);
|
||||||
|
|
||||||
const response: ToolListResponse = await listTools(projectId, toolkitSlug, cursor);
|
const response: ToolListResponse = await listTools(projectId, toolkitSlug, search, cursor);
|
||||||
|
|
||||||
setTools(response.items);
|
setTools(response.items);
|
||||||
setNextCursor(response.next_cursor);
|
setNextCursor(response.next_cursor);
|
||||||
|
|
@ -80,18 +88,25 @@ export function ComposioToolsPanel({
|
||||||
}
|
}
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
// Load tools when search query changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (toolkit && isOpen) {
|
||||||
|
loadToolsForToolkit(toolkit.slug, null, debouncedSearchQuery || null);
|
||||||
|
}
|
||||||
|
}, [toolkit, isOpen, debouncedSearchQuery, loadToolsForToolkit]);
|
||||||
|
|
||||||
const handleNextPage = useCallback(async () => {
|
const handleNextPage = useCallback(async () => {
|
||||||
if (!nextCursor || !toolkit) return;
|
if (!nextCursor) return;
|
||||||
|
|
||||||
// Add current cursor to history
|
// Add current cursor to history
|
||||||
setCursorHistory(prev => [...prev, currentCursor || '']);
|
setCursorHistory(prev => [...prev, currentCursor || '']);
|
||||||
setCurrentCursor(nextCursor);
|
setCurrentCursor(nextCursor);
|
||||||
|
|
||||||
await loadToolsForToolkit(toolkit.slug, nextCursor);
|
await loadToolsForToolkit(toolkit.slug, nextCursor, debouncedSearchQuery || null);
|
||||||
}, [nextCursor, toolkit, currentCursor, loadToolsForToolkit]);
|
}, [nextCursor, toolkit, currentCursor, debouncedSearchQuery, loadToolsForToolkit]);
|
||||||
|
|
||||||
const handlePreviousPage = useCallback(async () => {
|
const handlePreviousPage = useCallback(async () => {
|
||||||
if (cursorHistory.length === 0 || !toolkit) return;
|
if (cursorHistory.length === 0) return;
|
||||||
|
|
||||||
// Get the previous cursor from history
|
// Get the previous cursor from history
|
||||||
const previousCursor = cursorHistory[cursorHistory.length - 1];
|
const previousCursor = cursorHistory[cursorHistory.length - 1];
|
||||||
|
|
@ -100,8 +115,8 @@ export function ComposioToolsPanel({
|
||||||
setCursorHistory(newHistory);
|
setCursorHistory(newHistory);
|
||||||
setCurrentCursor(previousCursor);
|
setCurrentCursor(previousCursor);
|
||||||
|
|
||||||
await loadToolsForToolkit(toolkit.slug, previousCursor);
|
await loadToolsForToolkit(toolkit.slug, previousCursor, debouncedSearchQuery || null);
|
||||||
}, [cursorHistory, toolkit, loadToolsForToolkit]);
|
}, [cursorHistory, toolkit, debouncedSearchQuery, loadToolsForToolkit]);
|
||||||
|
|
||||||
const handleToolSelectionChange = useCallback((toolSlug: string, selected: boolean) => {
|
const handleToolSelectionChange = useCallback((toolSlug: string, selected: boolean) => {
|
||||||
setSelectedTools(prev => {
|
setSelectedTools(prev => {
|
||||||
|
|
@ -116,239 +131,195 @@ export function ComposioToolsPanel({
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSaveTools = useCallback(async () => {
|
const handleAddSelectedTools = useCallback(() => {
|
||||||
// Convert selected tool slugs to actual tool objects
|
// Convert selected tool slugs to actual tool objects and add them
|
||||||
const selectedToolObjects = tools.filter(tool => selectedTools.has(tool.slug));
|
const selectedToolObjects = tools.filter(tool => selectedTools.has(tool.slug));
|
||||||
await onUpdateToolsSelection(selectedToolObjects);
|
|
||||||
setHasChanges(false);
|
|
||||||
}, [onUpdateToolsSelection, selectedTools, tools]);
|
|
||||||
|
|
||||||
const handleConnect = useCallback(() => {
|
|
||||||
setShowAuthModal(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDisconnect = useCallback(async () => {
|
|
||||||
if (!toolkit) return;
|
|
||||||
|
|
||||||
const connectedAccountId = projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.id;
|
selectedToolObjects.forEach(tool => {
|
||||||
|
const toolToAdd = {
|
||||||
|
name: tool.name,
|
||||||
|
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: toolkit.no_auth || false,
|
||||||
|
toolkitName: toolkit.name,
|
||||||
|
toolkitSlug: toolkit.slug,
|
||||||
|
logo: toolkit.meta.logo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddTool(toolToAdd);
|
||||||
|
});
|
||||||
|
|
||||||
setIsProcessingAuth(true);
|
onClose();
|
||||||
try {
|
}, [selectedTools, tools, toolkit, onAddTool, onClose]);
|
||||||
if (connectedAccountId) {
|
|
||||||
await deleteConnectedAccount(projectId, toolkit.slug, connectedAccountId);
|
|
||||||
onProjectConfigUpdate();
|
|
||||||
onRemoveToolkitTools(toolkit.slug);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Disconnect failed:', err);
|
|
||||||
} finally {
|
|
||||||
setIsProcessingAuth(false);
|
|
||||||
}
|
|
||||||
}, [projectId, toolkit, projectConfig, onProjectConfigUpdate, onRemoveToolkitTools]);
|
|
||||||
|
|
||||||
const handleAuthComplete = useCallback(() => {
|
|
||||||
setShowAuthModal(false);
|
|
||||||
onProjectConfigUpdate();
|
|
||||||
}, [onProjectConfigUpdate]);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setTools([]);
|
setTools([]);
|
||||||
setSelectedTools(new Set());
|
setSelectedTools(new Set());
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
if (hasChanges) {
|
setSearchQuery('');
|
||||||
if (window.confirm('You have unsaved changes. Are you sure you want to close?')) {
|
setDebouncedSearchQuery('');
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}, [onClose]);
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}, [onClose, hasChanges]);
|
|
||||||
|
|
||||||
// Initialize selected tools from project config when opening the panel
|
const handleClearSearch = useCallback(() => {
|
||||||
useEffect(() => {
|
setSearchQuery('');
|
||||||
if (toolkit && isOpen && projectConfig?.composioSelectedTools) {
|
}, []);
|
||||||
const toolSlugs = new Set(projectConfig.composioSelectedTools.map(tool => tool.slug));
|
|
||||||
setSelectedTools(toolSlugs);
|
|
||||||
setHasChanges(false);
|
|
||||||
}
|
|
||||||
}, [toolkit, isOpen, projectConfig]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (toolkit && isOpen) {
|
|
||||||
loadToolsForToolkit(toolkit.slug, null);
|
|
||||||
}
|
|
||||||
}, [toolkit, isOpen, loadToolsForToolkit]);
|
|
||||||
|
|
||||||
if (!toolkit) return null;
|
if (!toolkit) return null;
|
||||||
|
|
||||||
// Check if the toolkit is connected (has an active connected account) or doesn't require auth
|
|
||||||
const isToolkitConnected = toolkit.no_auth || projectConfig?.composioConnectedAccounts?.[toolkit.slug]?.status === 'ACTIVE';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SlidePanel
|
||||||
<SlidePanel
|
isOpen={isOpen}
|
||||||
isOpen={isOpen}
|
onClose={handleClose}
|
||||||
onClose={handleClose}
|
title={
|
||||||
title={
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
{toolkit.meta.logo && (
|
||||||
{toolkit.meta.logo && (
|
<PictureImg
|
||||||
<PictureImg
|
src={toolkit.meta.logo}
|
||||||
src={toolkit.meta.logo}
|
alt={`${toolkit.name} logo`}
|
||||||
alt={`${toolkit.name} logo`}
|
width={24}
|
||||||
width={24}
|
height={24}
|
||||||
height={24}
|
className="rounded-md object-cover"
|
||||||
className="rounded-md object-cover"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span>{toolkit.name}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
{/* Connection Status Banner */}
|
|
||||||
{!toolkit.no_auth && (
|
|
||||||
<div className={`mb-6 p-4 rounded-lg border-2 ${
|
|
||||||
isToolkitConnected
|
|
||||||
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800'
|
|
||||||
: 'bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className={`w-3 h-3 rounded-full ${
|
|
||||||
isToolkitConnected ? 'bg-emerald-500' : 'bg-orange-500'
|
|
||||||
}`}></div>
|
|
||||||
<div>
|
|
||||||
<h3 className={`font-semibold text-sm ${
|
|
||||||
isToolkitConnected
|
|
||||||
? 'text-emerald-800 dark:text-emerald-200'
|
|
||||||
: 'text-orange-800 dark:text-orange-200'
|
|
||||||
}`}>
|
|
||||||
{isToolkitConnected ? 'Toolkit Connected' : 'Authentication Required'}
|
|
||||||
</h3>
|
|
||||||
<p className={`text-xs mt-0.5 ${
|
|
||||||
isToolkitConnected
|
|
||||||
? 'text-emerald-700 dark:text-emerald-300'
|
|
||||||
: 'text-orange-700 dark:text-orange-300'
|
|
||||||
}`}>
|
|
||||||
{isToolkitConnected
|
|
||||||
? 'You can select and use tools from this toolkit'
|
|
||||||
: 'Connect your account to access and use tools'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
size="sm"
|
|
||||||
onPress={isToolkitConnected ? handleDisconnect : handleConnect}
|
|
||||||
disabled={isProcessingAuth}
|
|
||||||
color={isToolkitConnected ? "danger" : "primary"}
|
|
||||||
isLoading={isProcessingAuth}
|
|
||||||
startContent={isToolkitConnected ? <UnlinkIcon className="h-4 w-4" /> : <LinkIcon className="h-4 w-4" />}
|
|
||||||
>
|
|
||||||
{isToolkitConnected ? 'Disconnect' : 'Connect Now'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<span>{toolkit.name}</span>
|
||||||
{/* Header */}
|
</div>
|
||||||
<div className="mb-6">
|
}
|
||||||
<div className="flex items-center justify-between">
|
>
|
||||||
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Available Tools</h4>
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-center gap-2">
|
{/* Header */}
|
||||||
{hasChanges && (
|
<div className="mb-6">
|
||||||
<Button
|
<div className="flex items-center justify-between mb-4">
|
||||||
variant="solid"
|
<div>
|
||||||
size="sm"
|
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Select Tools</h4>
|
||||||
color="primary"
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
onPress={handleSaveTools}
|
Check the tools you want to add to your workflow
|
||||||
disabled={isSaving || !isToolkitConnected}
|
</p>
|
||||||
isLoading={isSaving}
|
|
||||||
>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{hasChanges && (
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
onPress={handleAddSelectedTools}
|
||||||
|
>
|
||||||
|
Add Selected ({selectedTools.size})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Tools List */}
|
{/* Search Box */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="relative">
|
||||||
{toolsLoading ? (
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<div className="text-center py-8">
|
<Search className="h-4 w-4 text-gray-400" />
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
|
</div>
|
||||||
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading tools...</p>
|
<Input
|
||||||
</div>
|
type="text"
|
||||||
) : (
|
placeholder="Search tools..."
|
||||||
<div className="space-y-4">
|
value={searchQuery}
|
||||||
{tools.map((tool) => (
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<div key={tool.slug} className={`group p-4 rounded-lg transition-all duration-200 border border-transparent ${
|
className="pl-10 pr-10"
|
||||||
isToolkitConnected
|
size="sm"
|
||||||
? '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'
|
{searchQuery && (
|
||||||
}`}>
|
<button
|
||||||
<div className="flex items-start gap-3">
|
onClick={handleClearSearch}
|
||||||
<Checkbox
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
isSelected={selectedTools.has(tool.slug)}
|
>
|
||||||
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
|
<X className="h-4 w-4" />
|
||||||
size="sm"
|
</button>
|
||||||
isDisabled={!isToolkitConnected}
|
)}
|
||||||
/>
|
</div>
|
||||||
<div className="flex-1">
|
</div>
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
|
|
||||||
{tool.name}
|
{/* Scrollable Tools List */}
|
||||||
</h4>
|
<div className="flex-1 overflow-y-auto">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
{toolsLoading ? (
|
||||||
{tool.description}
|
<div className="text-center py-8">
|
||||||
</p>
|
<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">
|
||||||
|
{searchQuery ? 'Searching tools...' : 'Loading tools...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : tools.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{searchQuery ? 'No tools found matching your search.' : 'No tools available.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{availableTools.map((tool) => (
|
||||||
|
<div
|
||||||
|
key={tool.slug}
|
||||||
|
className="group p-4 rounded-lg transition-all duration-200 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
isSelected={selectedTools.has(tool.slug)}
|
||||||
|
onValueChange={(selected) => handleToolSelectionChange(tool.slug, selected)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 text-left flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors text-left">
|
||||||
|
{tool.name}
|
||||||
|
</h4>
|
||||||
|
<div className="font-mono text-xs text-gray-500 dark:text-gray-400 text-left truncate max-w-[300px] bg-gray-100 dark:bg-gray-700 p-1 rounded-md" title={tool.slug}>
|
||||||
|
{tool.slug}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-left">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Fixed Pagination Controls */}
|
{/* Fixed Pagination Controls */}
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
<Button
|
{availableTools.length > 0 && (
|
||||||
variant="bordered"
|
<span>
|
||||||
size="sm"
|
{availableTools.length} tool{availableTools.length !== 1 ? 's' : ''} found
|
||||||
onClick={handlePreviousPage}
|
{searchQuery && ` for "${searchQuery}"`}
|
||||||
disabled={cursorHistory.length === 0 || toolsLoading}
|
</span>
|
||||||
>
|
)}
|
||||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
</div>
|
||||||
Previous
|
<div className="flex items-center gap-2">
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant="bordered"
|
||||||
variant="bordered"
|
size="sm"
|
||||||
size="sm"
|
onClick={handlePreviousPage}
|
||||||
onClick={handleNextPage}
|
disabled={cursorHistory.length === 0 || toolsLoading}
|
||||||
disabled={!nextCursor || toolsLoading}
|
>
|
||||||
>
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||||
Next
|
Previous
|
||||||
<ChevronRight className="h-4 w-4 ml-1" />
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
variant="bordered"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleNextPage}
|
||||||
|
disabled={!nextCursor || toolsLoading}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4 ml-1" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SlidePanel>
|
</div>
|
||||||
|
</SlidePanel>
|
||||||
{/* Auth Modal */}
|
|
||||||
{toolkit && (
|
|
||||||
<ToolkitAuthModal
|
|
||||||
key={toolkit.slug}
|
|
||||||
isOpen={showAuthModal}
|
|
||||||
onClose={() => setShowAuthModal(false)}
|
|
||||||
toolkitSlug={toolkit.slug}
|
|
||||||
projectId={projectId}
|
|
||||||
onComplete={handleAuthComplete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { Button } from '@heroui/react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Info, Plus, Trash2 } from 'lucide-react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
import { getProjectConfig } from '@/app/actions/project_actions';
|
||||||
|
import { addServer, removeServer } from '@/app/actions/custom_mcp_server_actions';
|
||||||
|
import { fetchTools } from "@/app/actions/custom_mcp_server_actions";
|
||||||
|
import { ServerCard } from './ServerCard';
|
||||||
|
import { McpToolsPanel } from './McpToolsPanel';
|
||||||
|
import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
const CustomMcpServerType = z.object({ serverUrl: z.string() });
|
||||||
|
type CustomMcpServer = z.infer<typeof CustomMcpServerType>;
|
||||||
|
|
||||||
|
type ServerList = Record<string, CustomMcpServer>;
|
||||||
|
|
||||||
|
type CustomMcpServersProps = {
|
||||||
|
tools: z.infer<typeof Workflow.shape.tools>;
|
||||||
|
onAddTool: (tool: z.infer<typeof WorkflowTool>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CustomMcpServers({ tools: workflowTools, onAddTool }: CustomMcpServersProps) {
|
||||||
|
const params = useParams();
|
||||||
|
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
|
||||||
|
if (!projectId) throw new Error('Project ID is required');
|
||||||
|
|
||||||
|
// State
|
||||||
|
const [servers, setServers] = useState<ServerList>({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [addName, setAddName] = useState('');
|
||||||
|
const [addUrl, setAddUrl] = useState('');
|
||||||
|
const [addLoading, setAddLoading] = useState(false);
|
||||||
|
const [addError, setAddError] = useState<string | null>(null);
|
||||||
|
const [panelServer, setPanelServer] = useState<{ name: string; url: string } | null>(null);
|
||||||
|
const [toolsLoading, setToolsLoading] = useState(false);
|
||||||
|
const [toolsError, setToolsError] = useState<string | null>(null);
|
||||||
|
const [serverTools, setServerTools] = useState<z.infer<typeof WorkflowTool>[]>([]);
|
||||||
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
|
const [serverToDelete, setServerToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Fetch servers on mount
|
||||||
|
const fetchServers = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const project = await getProjectConfig(projectId);
|
||||||
|
setServers(project.customMcpServers || {});
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err?.message || 'Failed to load servers');
|
||||||
|
setServers({});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchServers();
|
||||||
|
}, [fetchServers]);
|
||||||
|
|
||||||
|
// Add server
|
||||||
|
const handleAddServer = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!addName || !addUrl) return;
|
||||||
|
setAddLoading(true);
|
||||||
|
setAddError(null);
|
||||||
|
try {
|
||||||
|
await addServer(projectId, addName, { serverUrl: addUrl });
|
||||||
|
setAddName('');
|
||||||
|
setAddUrl('');
|
||||||
|
await fetchServers();
|
||||||
|
} catch (err: any) {
|
||||||
|
setAddError(err?.message || 'Failed to add server');
|
||||||
|
} finally {
|
||||||
|
setAddLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open delete modal
|
||||||
|
const handleDeleteClick = (name: string) => {
|
||||||
|
setServerToDelete(name);
|
||||||
|
setDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete server
|
||||||
|
const handleDeleteServer = async () => {
|
||||||
|
if (!serverToDelete) return;
|
||||||
|
try {
|
||||||
|
await removeServer(projectId, serverToDelete);
|
||||||
|
await fetchServers();
|
||||||
|
setDeleteModalOpen(false);
|
||||||
|
setServerToDelete(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err?.message || 'Failed to delete server');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open panel and fetch tools
|
||||||
|
const handleOpenPanel = async (name: string, url: string) => {
|
||||||
|
setPanelServer({ name, url });
|
||||||
|
setToolsLoading(true);
|
||||||
|
setToolsError(null);
|
||||||
|
setServerTools([]);
|
||||||
|
try {
|
||||||
|
const fetched = await fetchTools(url, name);
|
||||||
|
setServerTools(fetched);
|
||||||
|
} catch (err: any) {
|
||||||
|
setToolsError(err?.message || 'Failed to fetch tools');
|
||||||
|
} finally {
|
||||||
|
setToolsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close panel
|
||||||
|
const handleClosePanel = () => {
|
||||||
|
setPanelServer(null);
|
||||||
|
setServerTools([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="shrink-0">
|
||||||
|
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
Add your own MCP servers here. Enter the server details and select tools to add to your workflow.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add server form */}
|
||||||
|
<form onSubmit={handleAddServer} className="space-y-4">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={addName}
|
||||||
|
onChange={e => setAddName(e.target.value)}
|
||||||
|
placeholder="Server Name"
|
||||||
|
required
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={addUrl}
|
||||||
|
onChange={e => setAddUrl(e.target.value)}
|
||||||
|
placeholder="Server URL"
|
||||||
|
required
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!addName || !addUrl || addLoading}
|
||||||
|
startContent={<Plus className="h-4 w-4" />}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{addError && <div className="text-red-500 text-sm mt-1">{addError}</div>}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Server cards */}
|
||||||
|
{loading ? (
|
||||||
|
<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 servers...</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center py-8 text-red-500 dark:text-red-400">{error}</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{Object.entries(servers).length === 0 ? (
|
||||||
|
<div className="col-span-full text-gray-500 text-sm">No custom MCP servers added yet.</div>
|
||||||
|
) : (
|
||||||
|
Object.entries(servers).map(([name, { serverUrl }]) => (
|
||||||
|
<ServerCard
|
||||||
|
key={name}
|
||||||
|
serverName={name}
|
||||||
|
serverUrl={serverUrl}
|
||||||
|
workflowTools={workflowTools}
|
||||||
|
onSelectServer={() => handleOpenPanel(name, serverUrl)}
|
||||||
|
onDeleteServer={() => handleDeleteClick(name)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete confirmation modal */}
|
||||||
|
<ProjectWideChangeConfirmationModal
|
||||||
|
isOpen={deleteModalOpen}
|
||||||
|
onClose={() => setDeleteModalOpen(false)}
|
||||||
|
onConfirm={handleDeleteServer}
|
||||||
|
title="Delete Server"
|
||||||
|
confirmationQuestion={`Are you sure you want to delete "${serverToDelete}"? This will delete the server from the project.`}
|
||||||
|
confirmButtonText="Delete"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* MCP Tools Panel */}
|
||||||
|
<McpToolsPanel
|
||||||
|
server={panelServer}
|
||||||
|
isOpen={!!panelServer}
|
||||||
|
onClose={handleClosePanel}
|
||||||
|
tools={workflowTools}
|
||||||
|
onAddTool={onAddTool}
|
||||||
|
serverTools={serverTools}
|
||||||
|
toolsLoading={toolsLoading}
|
||||||
|
toolsError={toolsError}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,460 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Info, Plus, Search } from 'lucide-react';
|
|
||||||
import { clsx } from 'clsx';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { MCPServer } from '@/app/lib/types/types';
|
|
||||||
import {
|
|
||||||
ServerCard,
|
|
||||||
ToolManagementPanel
|
|
||||||
} from './MCPServersCommon';
|
|
||||||
import { fetchMcpToolsForServer } from '@/app/actions/mcp_actions';
|
|
||||||
import {
|
|
||||||
fetchCustomServers,
|
|
||||||
addCustomServer,
|
|
||||||
removeCustomServer,
|
|
||||||
toggleCustomServer,
|
|
||||||
updateCustomServerTools
|
|
||||||
} from '@/app/actions/custom_server_actions';
|
|
||||||
import { Modal } from '@/components/ui/modal';
|
|
||||||
|
|
||||||
type McpServerType = z.infer<typeof MCPServer>;
|
|
||||||
type McpToolType = z.infer<typeof MCPServer>['tools'][number];
|
|
||||||
|
|
||||||
export function CustomServers() {
|
|
||||||
const params = useParams();
|
|
||||||
const projectId = typeof params.projectId === 'string' ? params.projectId : params.projectId?.[0];
|
|
||||||
if (!projectId) throw new Error('Project ID is required');
|
|
||||||
|
|
||||||
const [servers, setServers] = useState<McpServerType[]>([]);
|
|
||||||
const [selectedServer, setSelectedServer] = useState<McpServerType | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [togglingServers, setTogglingServers] = useState<Set<string>>(new Set());
|
|
||||||
const [serverOperations, setServerOperations] = useState<Map<string, 'setup' | 'delete'>>(new Map());
|
|
||||||
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
|
||||||
const [hasToolChanges, setHasToolChanges] = useState(false);
|
|
||||||
const [savingTools, setSavingTools] = useState(false);
|
|
||||||
const [syncingServers, setSyncingServers] = useState<Set<string>>(new Set());
|
|
||||||
const [showAddServer, setShowAddServer] = useState(false);
|
|
||||||
const [newServerName, setNewServerName] = useState('');
|
|
||||||
const [newServerUrl, setNewServerUrl] = useState('');
|
|
||||||
|
|
||||||
const fetchServers = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const customServers = await fetchCustomServers(projectId);
|
|
||||||
setServers(customServers);
|
|
||||||
setError(null);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err?.message || 'Failed to load custom MCP servers');
|
|
||||||
console.error('Error fetching servers:', err);
|
|
||||||
setServers([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [projectId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchServers();
|
|
||||||
}, [fetchServers]);
|
|
||||||
|
|
||||||
const handleToggleServer = async (server: McpServerType) => {
|
|
||||||
try {
|
|
||||||
const serverKey = server.name;
|
|
||||||
setTogglingServers(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.add(serverKey);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
setServerOperations(prev => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.set(serverKey, server.isActive ? 'delete' : 'setup');
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
await toggleCustomServer(projectId, server.name, !server.isActive);
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setServers(prevServers => {
|
|
||||||
return prevServers.map(s => {
|
|
||||||
if (s.name === serverKey) {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
isActive: !s.isActive
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Toggle failed:', { server: server.name, error: err });
|
|
||||||
} finally {
|
|
||||||
const serverKey = server.name;
|
|
||||||
setTogglingServers(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.delete(serverKey);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
setServerOperations(prev => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
next.delete(serverKey);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSyncServer = async (server: McpServerType) => {
|
|
||||||
if (!projectId || !server.isActive) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setSyncingServers(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.add(server.name);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
const enrichedTools = await fetchMcpToolsForServer(projectId, server.name);
|
|
||||||
|
|
||||||
const updatedAvailableTools = enrichedTools.map(tool => ({
|
|
||||||
id: tool.name,
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
parameters: tool.parameters
|
|
||||||
}));
|
|
||||||
|
|
||||||
await updateCustomServerTools(
|
|
||||||
projectId,
|
|
||||||
server.name,
|
|
||||||
updatedAvailableTools, // Auto-select all tools for custom servers
|
|
||||||
updatedAvailableTools
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update servers state
|
|
||||||
setServers(prevServers => {
|
|
||||||
return prevServers.map(s => {
|
|
||||||
if (s.name === server.name) {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
availableTools: updatedAvailableTools,
|
|
||||||
tools: updatedAvailableTools
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// If this server is currently selected, update the selectedTools state
|
|
||||||
if (selectedServer?.name === server.name) {
|
|
||||||
setSelectedServer(prev => {
|
|
||||||
if (!prev) return null;
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
availableTools: updatedAvailableTools,
|
|
||||||
tools: updatedAvailableTools
|
|
||||||
};
|
|
||||||
});
|
|
||||||
// Update selectedTools to include all tools for the custom server
|
|
||||||
setSelectedTools(new Set(updatedAvailableTools.map(tool => tool.id)));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setSyncingServers(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
next.delete(server.name);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add effect to sync selectedTools when selectedServer changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedServer) {
|
|
||||||
setSelectedTools(new Set(selectedServer.tools.map(tool => tool.id)));
|
|
||||||
setHasToolChanges(false);
|
|
||||||
}
|
|
||||||
}, [selectedServer]);
|
|
||||||
|
|
||||||
const handleAddServer = async () => {
|
|
||||||
if (!newServerName || !newServerUrl) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newServer: McpServerType = {
|
|
||||||
id: `custom-${Date.now()}`,
|
|
||||||
name: newServerName,
|
|
||||||
description: `Custom MCP server at ${newServerUrl}`,
|
|
||||||
serverUrl: newServerUrl,
|
|
||||||
tools: [],
|
|
||||||
availableTools: [],
|
|
||||||
isActive: true,
|
|
||||||
isReady: true,
|
|
||||||
serverType: 'custom',
|
|
||||||
authNeeded: false,
|
|
||||||
isAuthenticated: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add to MongoDB and get back the formatted server
|
|
||||||
const formattedServer = await addCustomServer(projectId, newServer);
|
|
||||||
|
|
||||||
// Update local state with the formatted server
|
|
||||||
setServers(prev => [...prev, formattedServer]);
|
|
||||||
setShowAddServer(false);
|
|
||||||
setNewServerName('');
|
|
||||||
setNewServerUrl('');
|
|
||||||
|
|
||||||
// Fetch tools for the new server using the formatted URL
|
|
||||||
await handleSyncServer(formattedServer);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error adding server:', err);
|
|
||||||
setError('Failed to add server. Please try again.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveServer = async (server: McpServerType) => {
|
|
||||||
// Show confirmation dialog
|
|
||||||
const shouldRemove = window.confirm(
|
|
||||||
"Are you sure you want to delete this server? Alternatively, you can toggle it OFF if you'd like to retain the configuration but not make it available to agents."
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!shouldRemove) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeCustomServer(projectId, server.name);
|
|
||||||
// Update local state
|
|
||||||
setServers(prev => prev.filter(s => s.name !== server.name));
|
|
||||||
// If this server was selected, close the tool management panel
|
|
||||||
if (selectedServer?.name === server.name) {
|
|
||||||
setSelectedServer(null);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error removing server:', err);
|
|
||||||
setError('Failed to remove server. Please try again.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveToolSelection = async () => {
|
|
||||||
if (!selectedServer || !projectId) return;
|
|
||||||
|
|
||||||
setSavingTools(true);
|
|
||||||
try {
|
|
||||||
const availableTools = selectedServer.availableTools || [];
|
|
||||||
const selectedToolsList = availableTools.filter(tool => selectedTools.has(tool.id));
|
|
||||||
|
|
||||||
await updateCustomServerTools(
|
|
||||||
projectId,
|
|
||||||
selectedServer.name,
|
|
||||||
selectedToolsList,
|
|
||||||
availableTools
|
|
||||||
);
|
|
||||||
|
|
||||||
setServers(prevServers => {
|
|
||||||
return prevServers.map(s => {
|
|
||||||
if (s.name === selectedServer.name) {
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
tools: selectedToolsList
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setSelectedServer(prev => {
|
|
||||||
if (!prev) return null;
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
tools: selectedToolsList
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
setHasToolChanges(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving tool selection:', error);
|
|
||||||
} finally {
|
|
||||||
setSavingTools(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredServers = servers.filter(server => {
|
|
||||||
const searchLower = searchQuery.toLowerCase();
|
|
||||||
const serverTools = server.tools || [];
|
|
||||||
return (
|
|
||||||
server.name.toLowerCase().includes(searchLower) ||
|
|
||||||
server.description.toLowerCase().includes(searchLower) ||
|
|
||||||
serverTools.some(tool =>
|
|
||||||
tool.name.toLowerCase().includes(searchLower) ||
|
|
||||||
tool.description.toLowerCase().includes(searchLower)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-4">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
|
||||||
Add your own MCP servers here. These servers will be available to agents in the Build view once toggled ON.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => setShowAddServer(true)}
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
<span className="ml-2">Add Server</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<div className="flex-1 flex items-center gap-4">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<div className="absolute inset-y-0 left-2 flex items-center pointer-events-none">
|
|
||||||
<Search className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search servers or tools..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full pl-8 pr-4 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md
|
|
||||||
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
|
|
||||||
placeholder-gray-400 dark:placeholder-gray-500
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400
|
|
||||||
hover:border-gray-300 dark:hover:border-gray-600 transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
|
||||||
{filteredServers.length} {filteredServers.length === 1 ? 'server' : 'servers'} • {
|
|
||||||
filteredServers.reduce((total, server) => total + (server.availableTools?.length || 0), 0)
|
|
||||||
} tools
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
isOpen={showAddServer}
|
|
||||||
onClose={() => {
|
|
||||||
setShowAddServer(false);
|
|
||||||
setNewServerName('');
|
|
||||||
setNewServerUrl('');
|
|
||||||
}}
|
|
||||||
title="Add Custom MCP Server"
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
Server Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newServerName}
|
|
||||||
onChange={(e) => setNewServerName(e.target.value)}
|
|
||||||
placeholder="e.g., My Custom Server"
|
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md
|
|
||||||
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
Server URL
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newServerUrl}
|
|
||||||
onChange={(e) => setNewServerUrl(e.target.value)}
|
|
||||||
placeholder="e.g., http://localhost:3000"
|
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md
|
|
||||||
bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setShowAddServer(false);
|
|
||||||
setNewServerName('');
|
|
||||||
setNewServerUrl('');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleAddServer}
|
|
||||||
disabled={!newServerName || !newServerUrl}
|
|
||||||
>
|
|
||||||
Add Server
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<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 servers...</p>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="text-center py-8 text-red-500 dark:text-red-400">{error}</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{filteredServers.map((server) => (
|
|
||||||
<ServerCard
|
|
||||||
key={server.id}
|
|
||||||
server={server}
|
|
||||||
onToggle={() => handleToggleServer(server)}
|
|
||||||
onManageTools={() => setSelectedServer(server)}
|
|
||||||
onSync={() => handleSyncServer(server)}
|
|
||||||
onRemove={() => handleRemoveServer(server)}
|
|
||||||
isToggling={togglingServers.has(server.name)}
|
|
||||||
isSyncing={syncingServers.has(server.name)}
|
|
||||||
operation={serverOperations.get(server.name)}
|
|
||||||
error={error && error.includes(server.name) ? { message: error } : undefined}
|
|
||||||
showAuth={false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ToolManagementPanel
|
|
||||||
server={selectedServer}
|
|
||||||
onClose={() => {
|
|
||||||
setSelectedServer(null);
|
|
||||||
setSelectedTools(new Set());
|
|
||||||
setHasToolChanges(false);
|
|
||||||
}}
|
|
||||||
selectedTools={selectedTools}
|
|
||||||
onToolSelectionChange={(toolId, selected) => {
|
|
||||||
setSelectedTools(prev => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (selected) {
|
|
||||||
next.add(toolId);
|
|
||||||
} else {
|
|
||||||
next.delete(toolId);
|
|
||||||
}
|
|
||||||
setHasToolChanges(true);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onSaveTools={handleSaveToolSelection}
|
|
||||||
onSyncTools={selectedServer ? () => handleSyncServer(selectedServer) : undefined}
|
|
||||||
hasChanges={hasToolChanges}
|
|
||||||
isSaving={savingTools}
|
|
||||||
isSyncing={selectedServer ? syncingServers.has(selectedServer.name) : false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import { Button, Checkbox, Input } from '@heroui/react';
|
||||||
|
import { Search, X } from 'lucide-react';
|
||||||
|
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { SlidePanel } from '@/components/ui/slide-panel';
|
||||||
|
|
||||||
|
interface McpToolsPanelProps {
|
||||||
|
server: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
} | null;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
tools: z.infer<typeof Workflow.shape.tools>;
|
||||||
|
onAddTool: (tool: z.infer<typeof WorkflowTool>) => void;
|
||||||
|
serverTools: z.infer<typeof WorkflowTool>[];
|
||||||
|
toolsLoading: boolean;
|
||||||
|
toolsError: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function McpToolsPanel({
|
||||||
|
server,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
tools: workflowTools,
|
||||||
|
onAddTool,
|
||||||
|
serverTools,
|
||||||
|
toolsLoading,
|
||||||
|
toolsError,
|
||||||
|
}: McpToolsPanelProps) {
|
||||||
|
const [selectedTools, setSelectedTools] = useState<Set<string>>(new Set());
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Filter out already selected tools
|
||||||
|
const selectedToolNames = workflowTools
|
||||||
|
.filter(tool => tool.isMcp && tool.mcpServerName === server?.name)
|
||||||
|
.map(tool => tool.name);
|
||||||
|
|
||||||
|
// Debounce search query
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchQuery(searchQuery);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
// Filter tools based on search query
|
||||||
|
const filteredTools = useMemo(() => {
|
||||||
|
if (!debouncedSearchQuery) return serverTools;
|
||||||
|
|
||||||
|
const query = debouncedSearchQuery.toLowerCase();
|
||||||
|
return serverTools.filter(tool =>
|
||||||
|
tool.name.toLowerCase().includes(query) ||
|
||||||
|
tool.description.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}, [serverTools, debouncedSearchQuery]);
|
||||||
|
|
||||||
|
// Filter out already added tools
|
||||||
|
const availableTools = filteredTools.filter(tool => !selectedToolNames.includes(tool.name));
|
||||||
|
|
||||||
|
const handleToolSelectionChange = useCallback((toolName: string, selected: boolean) => {
|
||||||
|
setSelectedTools(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (selected) {
|
||||||
|
next.add(toolName);
|
||||||
|
} else {
|
||||||
|
next.delete(toolName);
|
||||||
|
}
|
||||||
|
setHasChanges(true);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddSelectedTools = useCallback(() => {
|
||||||
|
// Convert selected tool names to actual tool objects and add them
|
||||||
|
const selectedToolObjects = serverTools.filter(tool => selectedTools.has(tool.name));
|
||||||
|
|
||||||
|
selectedToolObjects.forEach(tool => {
|
||||||
|
onAddTool(tool);
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}, [selectedTools, serverTools, onAddTool, onClose]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setSelectedTools(new Set());
|
||||||
|
setHasChanges(false);
|
||||||
|
setSearchQuery('');
|
||||||
|
setDebouncedSearchQuery('');
|
||||||
|
onClose();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleClearSearch = useCallback(() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!server) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlidePanel
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
title={
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-6 h-6 bg-blue-500 rounded-md flex items-center justify-center">
|
||||||
|
<span className="text-white text-xs font-bold">MCP</span>
|
||||||
|
</div>
|
||||||
|
<span>{server.name}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Select Tools</h4>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Check the tools you want to add to your workflow
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{hasChanges && (
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
onPress={handleAddSelectedTools}
|
||||||
|
>
|
||||||
|
Add Selected ({selectedTools.size})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Box */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Search className="h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search tools..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 pr-10"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={handleClearSearch}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{toolsError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<p className="text-sm text-red-700 dark:text-red-300">{toolsError}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Scrollable Tools List */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{toolsLoading ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800 dark:border-gray-200 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{searchQuery ? 'Searching tools...' : 'Loading tools...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : availableTools.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{searchQuery ? 'No tools found matching your search.' : 'No tools available.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{availableTools.map((tool) => (
|
||||||
|
<div
|
||||||
|
key={tool.name}
|
||||||
|
className="group p-4 rounded-lg transition-all duration-200 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
isSelected={selectedTools.has(tool.name)}
|
||||||
|
onValueChange={(selected) => handleToolSelectionChange(tool.name, selected)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 text-left flex flex-col gap-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors text-left">
|
||||||
|
{tool.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-left">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fixed Footer */}
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{availableTools.length > 0 && (
|
||||||
|
<span>
|
||||||
|
{availableTools.length} tool{availableTools.length !== 1 ? 's' : ''} found
|
||||||
|
{searchQuery && ` for "${searchQuery}"`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="sm"
|
||||||
|
onPress={handleAddSelectedTools}
|
||||||
|
disabled={selectedTools.size === 0}
|
||||||
|
>
|
||||||
|
Add Selected ({selectedTools.size})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SlidePanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { PictureImg } from '@/components/ui/picture-img';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Chip } from '@heroui/react';
|
||||||
|
import { Server, MoreVertical } from 'lucide-react';
|
||||||
|
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
import { fetchTools } from "@/app/actions/custom_mcp_server_actions";
|
||||||
|
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/react';
|
||||||
|
import { Button } from '@heroui/react';
|
||||||
|
|
||||||
|
type ServerCardProps = {
|
||||||
|
serverName: string;
|
||||||
|
serverUrl: string;
|
||||||
|
workflowTools: z.infer<typeof Workflow.shape.tools>;
|
||||||
|
onSelectServer: () => void;
|
||||||
|
onDeleteServer: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverCardStyles = {
|
||||||
|
base: clsx(
|
||||||
|
"group p-6 rounded-xl transition-all duration-200 cursor-pointer",
|
||||||
|
"bg-white dark:bg-gray-900",
|
||||||
|
"border border-gray-200 dark:border-gray-700",
|
||||||
|
"shadow-md dark:shadow-gray-900/20",
|
||||||
|
"hover:shadow-lg dark:hover:shadow-gray-900/30",
|
||||||
|
"hover:border-blue-300 dark:hover:border-blue-600",
|
||||||
|
"hover:bg-gray-50/50 dark:hover:bg-gray-800/50",
|
||||||
|
"hover:-translate-y-1",
|
||||||
|
"min-h-[200px] flex flex-col"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ServerCard({
|
||||||
|
serverName,
|
||||||
|
serverUrl,
|
||||||
|
workflowTools,
|
||||||
|
onSelectServer,
|
||||||
|
onDeleteServer,
|
||||||
|
}: ServerCardProps) {
|
||||||
|
const [tools, setTools] = useState<z.infer<typeof WorkflowTool>[]>([]);
|
||||||
|
const [toolsLoading, setToolsLoading] = useState(true);
|
||||||
|
const [toolsError, setToolsError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Fetch tools on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchServerTools = async () => {
|
||||||
|
setToolsLoading(true);
|
||||||
|
setToolsError(null);
|
||||||
|
try {
|
||||||
|
const fetched = await fetchTools(serverUrl, serverName);
|
||||||
|
setTools(fetched);
|
||||||
|
} catch (err: any) {
|
||||||
|
setToolsError(err?.message || 'Failed to fetch tools');
|
||||||
|
setTools([]);
|
||||||
|
} finally {
|
||||||
|
setToolsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchServerTools();
|
||||||
|
}, [serverUrl, serverName]);
|
||||||
|
|
||||||
|
const handleCardClick = useCallback(() => {
|
||||||
|
onSelectServer();
|
||||||
|
}, [onSelectServer]);
|
||||||
|
|
||||||
|
// Calculate selected tools count for this server
|
||||||
|
const selectedToolsCount = workflowTools
|
||||||
|
.filter(tool => tool.isMcp && tool.mcpServerName === serverName)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={serverCardStyles.base} onClick={handleCardClick}>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start gap-3 mb-4">
|
||||||
|
<div className="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center flex-shrink-0">
|
||||||
|
<Server className="w-4 h-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-lg text-gray-900 dark:text-gray-100 truncate">
|
||||||
|
{serverName}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||||
|
{toolsLoading ? (
|
||||||
|
<Chip
|
||||||
|
color="secondary"
|
||||||
|
variant="faded"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Loading tools...
|
||||||
|
</Chip>
|
||||||
|
) : toolsError ? (
|
||||||
|
<Chip
|
||||||
|
color="danger"
|
||||||
|
variant="faded"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Error loading tools
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<Chip
|
||||||
|
color="secondary"
|
||||||
|
variant="faded"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{selectedToolsCount > 0
|
||||||
|
? `${tools.length} tools, ${selectedToolsCount} selected`
|
||||||
|
: `${tools.length} tools`
|
||||||
|
}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
title="More options"
|
||||||
|
aria-label="More options"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="Server actions">
|
||||||
|
<DropdownItem
|
||||||
|
key="delete"
|
||||||
|
color="danger"
|
||||||
|
startContent={<MoreVertical className="h-4 w-4" />}
|
||||||
|
onPress={onDeleteServer}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-3">
|
||||||
|
Custom MCP server at {serverUrl}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Chip
|
||||||
|
color='success'
|
||||||
|
variant='flat'
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Custom Server
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,11 @@ import { PictureImg } from '@/components/ui/picture-img';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ZToolkit } from '@/app/lib/composio/composio';
|
import { ZToolkit } from '@/app/lib/composio/composio';
|
||||||
import { Project } from '@/app/lib/types/project_types';
|
|
||||||
import { Chip } from '@heroui/react';
|
import { Chip } from '@heroui/react';
|
||||||
import { LinkIcon } from 'lucide-react';
|
import { LinkIcon } from 'lucide-react';
|
||||||
|
import { Workflow } from '@/app/lib/types/workflow_types';
|
||||||
|
|
||||||
type ToolkitType = z.infer<typeof ZToolkit>;
|
type ToolkitType = z.infer<typeof ZToolkit>;
|
||||||
type ProjectType = z.infer<typeof Project>;
|
|
||||||
|
|
||||||
const toolkitCardStyles = {
|
const toolkitCardStyles = {
|
||||||
base: clsx(
|
base: clsx(
|
||||||
|
|
@ -28,33 +27,25 @@ const toolkitCardStyles = {
|
||||||
|
|
||||||
interface ToolkitCardProps {
|
interface ToolkitCardProps {
|
||||||
toolkit: ToolkitType;
|
toolkit: ToolkitType;
|
||||||
projectId: string;
|
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
connectedAccountId?: string;
|
onSelectToolkit: () => void;
|
||||||
projectConfig: ProjectType | null;
|
workflowTools: z.infer<typeof Workflow.shape.tools>;
|
||||||
onManageTools: () => void;
|
|
||||||
onProjectConfigUpdate: () => void;
|
|
||||||
onRemoveToolkitTools: (toolkitSlug: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolkitCard({
|
export function ToolkitCard({
|
||||||
toolkit,
|
toolkit,
|
||||||
projectId,
|
|
||||||
isConnected,
|
isConnected,
|
||||||
connectedAccountId,
|
onSelectToolkit,
|
||||||
projectConfig,
|
workflowTools,
|
||||||
onManageTools,
|
|
||||||
onProjectConfigUpdate,
|
|
||||||
onRemoveToolkitTools
|
|
||||||
}: ToolkitCardProps) {
|
}: ToolkitCardProps) {
|
||||||
const handleCardClick = useCallback(() => {
|
const handleCardClick = useCallback(() => {
|
||||||
onManageTools();
|
onSelectToolkit();
|
||||||
}, [onManageTools]);
|
}, [onSelectToolkit]);
|
||||||
|
|
||||||
// Calculate selected tools count for this toolkit
|
// Calculate selected tools count for this toolkit
|
||||||
const selectedToolsCount = projectConfig?.composioSelectedTools?.filter(tool =>
|
const selectedToolsCount = workflowTools
|
||||||
tool.toolkit.slug === toolkit.slug
|
.filter(tool => tool.isComposio && tool.composioData?.toolkitSlug === toolkit.slug)
|
||||||
).length || 0;
|
.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={toolkitCardStyles.base} onClick={handleCardClick}>
|
<div className={toolkitCardStyles.base} onClick={handleCardClick}>
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,25 @@
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Tabs, Tab } from '@/components/ui/tabs';
|
import { Tabs, Tab } from '@/components/ui/tabs';
|
||||||
import { HostedServers } from './HostedServers';
|
import { CustomMcpServers } from './CustomMcpServer';
|
||||||
import { CustomServers } from './CustomServers';
|
|
||||||
import { WebhookConfig } from './WebhookConfig';
|
|
||||||
import { Composio } from './Composio';
|
import { Composio } from './Composio';
|
||||||
|
import { AddWebhookTool } from './AddWebhookTool';
|
||||||
import type { Key } from 'react';
|
import type { Key } from 'react';
|
||||||
|
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export function ToolsConfig({
|
export function ToolsConfig({
|
||||||
|
projectId,
|
||||||
useComposioTools,
|
useComposioTools,
|
||||||
useKlavisTools
|
tools,
|
||||||
|
onAddTool,
|
||||||
}: {
|
}: {
|
||||||
|
projectId: string;
|
||||||
useComposioTools: boolean;
|
useComposioTools: boolean;
|
||||||
useKlavisTools: boolean;
|
tools: z.infer<typeof Workflow.shape.tools>;
|
||||||
|
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
||||||
}) {
|
}) {
|
||||||
let defaultActiveTab = 'custom';
|
let defaultActiveTab = 'mcp';
|
||||||
if (useKlavisTools) {
|
|
||||||
defaultActiveTab = 'hosted';
|
|
||||||
}
|
|
||||||
if (useComposioTools) {
|
if (useComposioTools) {
|
||||||
defaultActiveTab = 'composio';
|
defaultActiveTab = 'composio';
|
||||||
}
|
}
|
||||||
|
|
@ -40,32 +42,28 @@ export function ToolsConfig({
|
||||||
{useComposioTools && (
|
{useComposioTools && (
|
||||||
<Tab key="composio" title="Composio">
|
<Tab key="composio" title="Composio">
|
||||||
<div className="mt-4 p-6">
|
<div className="mt-4 p-6">
|
||||||
<Composio />
|
<Composio
|
||||||
|
projectId={projectId}
|
||||||
|
tools={tools}
|
||||||
|
onAddTool={onAddTool}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
{useKlavisTools && (
|
<Tab key="mcp" title="Custom MCP Servers">
|
||||||
<Tab key="hosted" title={
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>Klavis</span>
|
|
||||||
<span className="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>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<div className="mt-4 p-6">
|
|
||||||
<HostedServers onSwitchTab={key => setActiveTab(key)} />
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
)}
|
|
||||||
<Tab key="custom" title="Custom MCP Servers">
|
|
||||||
<div className="mt-4 p-6">
|
<div className="mt-4 p-6">
|
||||||
<CustomServers />
|
<CustomMcpServers
|
||||||
|
tools={tools}
|
||||||
|
onAddTool={onAddTool}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="webhook" title="Webhook">
|
<Tab key="webhook" title="Webhook">
|
||||||
<div className="mt-4 p-6">
|
<div className="mt-4 p-6">
|
||||||
<WebhookConfig />
|
<AddWebhookTool
|
||||||
|
projectId={projectId}
|
||||||
|
onAddTool={onAddTool}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,20 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams } from 'next/navigation';
|
import { Spinner, Button, Input } from "@heroui/react";
|
||||||
import { Spinner } from "@heroui/react";
|
|
||||||
import { getProjectConfig, updateWebhookUrl } from "@/app/actions/project_actions";
|
import { getProjectConfig, updateWebhookUrl } from "@/app/actions/project_actions";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
|
import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal';
|
||||||
|
|
||||||
const sectionHeaderStyles = "block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2";
|
export function WebhookConfig({ projectId }: { projectId: string }) {
|
||||||
const sectionDescriptionStyles = "text-sm text-gray-500 dark:text-gray-400 mb-4";
|
|
||||||
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500";
|
|
||||||
const inputStyles = "rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20";
|
|
||||||
|
|
||||||
function Section({ title, children, description }: {
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
description?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden">
|
|
||||||
<div className="px-6 pt-4">
|
|
||||||
<h2 className={sectionHeaderStyles}>{title}</h2>
|
|
||||||
{description && (
|
|
||||||
<p className={sectionDescriptionStyles}>{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="px-6 pb-6">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WebhookConfig() {
|
|
||||||
const params = useParams();
|
|
||||||
const projectId = params.projectId ? (typeof params.projectId === 'string' ? params.projectId : params.projectId[0]) : '';
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [webhookUrl, setWebhookUrl] = useState<string | null>(null);
|
const [webhookUrl, setWebhookUrl] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
const [editValue, setEditValue] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
@ -46,6 +24,7 @@ export function WebhookConfig() {
|
||||||
const project = await getProjectConfig(projectId);
|
const project = await getProjectConfig(projectId);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setWebhookUrl(project.webhookUrl || null);
|
setWebhookUrl(project.webhookUrl || null);
|
||||||
|
setEditValue(project.webhookUrl || '');
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -67,59 +46,138 @@ export function WebhookConfig() {
|
||||||
};
|
};
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
function validate(url: string) {
|
// validate on change in webhook
|
||||||
if (!url.trim()) {
|
useEffect(() => {
|
||||||
return { valid: true };
|
if (!isEditMode) return;
|
||||||
}
|
|
||||||
|
setError(null);
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(editValue || '');
|
||||||
setError(null);
|
|
||||||
return { valid: true };
|
|
||||||
} catch {
|
} catch {
|
||||||
setError('Please enter a valid URL');
|
setError('Please enter a valid URL');
|
||||||
return { valid: false, errorMessage: 'Please enter a valid URL' };
|
}
|
||||||
|
}, [editValue, isEditMode]);
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
setIsEditMode(true);
|
||||||
|
setEditValue(webhookUrl || '');
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsEditMode(false);
|
||||||
|
setEditValue(webhookUrl || '');
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await updateWebhookUrl(projectId, editValue);
|
||||||
|
setWebhookUrl(editValue);
|
||||||
|
setIsEditMode(false);
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update webhook URL:', err);
|
||||||
|
setError('Failed to update webhook URL');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (loading) {
|
||||||
<div className="space-y-6">
|
return (
|
||||||
<Section
|
<div className="space-y-6">
|
||||||
title="Webhook URL"
|
<div className="rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden">
|
||||||
description="In workflow editor, tool calls will be posted to this URL, unless they are mocked."
|
<div className="px-6 pt-4">
|
||||||
>
|
<h2 className="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">Webhook URL</h2>
|
||||||
<div className="space-y-2">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">In workflow editor, tool calls will be posted to this URL, unless they are mocked.</p>
|
||||||
<div className={clsx(
|
|
||||||
"border rounded-lg focus-within:ring-2",
|
|
||||||
error
|
|
||||||
? "border-red-500 focus-within:ring-red-500/20"
|
|
||||||
: "border-gray-200 dark:border-gray-700 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
|
|
||||||
)}>
|
|
||||||
<Textarea
|
|
||||||
value={webhookUrl || ''}
|
|
||||||
useValidation={true}
|
|
||||||
updateOnBlur={true}
|
|
||||||
validate={validate}
|
|
||||||
onValidatedChange={(value) => {
|
|
||||||
setWebhookUrl(value);
|
|
||||||
updateWebhookUrl(projectId, value);
|
|
||||||
}}
|
|
||||||
placeholder="Enter webhook URL..."
|
|
||||||
className="w-full text-sm bg-transparent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-4 py-3"
|
|
||||||
autoResize
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{loading && (
|
<div className="px-6 pb-6">
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{error && (
|
|
||||||
<p className="text-sm text-red-500">{error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden">
|
||||||
|
<div className="px-6 pt-4">
|
||||||
|
<h2 className="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">Webhook URL</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">Tool calls will be posted to this URL, unless they are mocked.</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{isEditMode ? (
|
||||||
|
<>
|
||||||
|
<div className={clsx(
|
||||||
|
"border rounded-lg focus-within:ring-2",
|
||||||
|
error
|
||||||
|
? "border-red-500 focus-within:ring-red-500/20"
|
||||||
|
: "border-gray-200 dark:border-gray-700 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
|
||||||
|
)}>
|
||||||
|
<Input
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
|
placeholder="Enter webhook URL..."
|
||||||
|
className="w-full text-sm bg-transparent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-4 py-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onPress={handleCancel}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onPress={() => setShowConfirmModal(true)}
|
||||||
|
disabled={!!error || saving}
|
||||||
|
>
|
||||||
|
Update Webhook URL
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{webhookUrl || 'No webhook URL configured'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onPress={handleEdit}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProjectWideChangeConfirmationModal
|
||||||
|
isOpen={showConfirmModal}
|
||||||
|
onClose={() => setShowConfirmModal(false)}
|
||||||
|
onConfirm={handleSave}
|
||||||
|
title="Update Webhook URL"
|
||||||
|
confirmationQuestion="Are you sure you want to update the webhook URL? This will affect all workflow tool calls."
|
||||||
|
confirmButtonText="Update"
|
||||||
|
isLoading={saving}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { Suspense } from 'react';
|
|
||||||
import { ToolsConfig } from './components/ToolsConfig';
|
|
||||||
import { PageHeader } from '@/components/ui/page-header';
|
|
||||||
import { requireActiveBillingSubscription } from '@/app/lib/billing';
|
|
||||||
import { USE_COMPOSIO_TOOLS, USE_KLAVIS_TOOLS } from '@/app/lib/feature_flags';
|
|
||||||
|
|
||||||
export default async function ToolsPage() {
|
|
||||||
await requireActiveBillingSubscription();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<PageHeader
|
|
||||||
title="Tools"
|
|
||||||
description="Configure and manage your project's tool integrations"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 p-6">
|
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<ToolsConfig
|
|
||||||
useComposioTools={USE_COMPOSIO_TOOLS}
|
|
||||||
useKlavisTools={USE_KLAVIS_TOOLS}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
"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";
|
||||||
import { Spinner } from "@heroui/react";
|
import { Spinner } from "@heroui/react";
|
||||||
import { listDataSources } from "../../../actions/datasource_actions";
|
import { listDataSources } from "../../../actions/datasource_actions";
|
||||||
import { collectProjectTools, revertToLiveWorkflow } from "@/app/actions/project_actions";
|
import { revertToLiveWorkflow } from "@/app/actions/project_actions";
|
||||||
import { getProjectConfig } from "@/app/actions/project_actions";
|
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,
|
||||||
|
|
@ -25,11 +25,10 @@ export function App({
|
||||||
const [mode, setMode] = useState<'draft' | 'live'>('draft');
|
const [mode, setMode] = useState<'draft' | 'live'>('draft');
|
||||||
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 [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>>>([]);
|
||||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
|
||||||
|
|
||||||
console.log('workflow app.tsx render');
|
console.log('workflow app.tsx render');
|
||||||
|
|
||||||
|
|
@ -44,28 +43,34 @@ export function App({
|
||||||
const [
|
const [
|
||||||
project,
|
project,
|
||||||
dataSources,
|
dataSources,
|
||||||
projectTools,
|
|
||||||
eligibleModels,
|
eligibleModels,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getProjectConfig(projectId),
|
getProjectConfig(projectId),
|
||||||
listDataSources(projectId),
|
listDataSources(projectId),
|
||||||
collectProjectTools(projectId),
|
|
||||||
getEligibleModels(),
|
getEligibleModels(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setProject(project);
|
setProject(project);
|
||||||
setDataSources(dataSources);
|
setDataSources(dataSources);
|
||||||
setProjectTools(projectTools);
|
|
||||||
setEligibleModels(eligibleModels);
|
setEligibleModels(eligibleModels);
|
||||||
if (project.mcpServers) {
|
if (project.mcpServers) {
|
||||||
setProjectMcpServers(project.mcpServers);
|
setProjectMcpServers(project.mcpServers);
|
||||||
}
|
}
|
||||||
if (project.webhookUrl) {
|
|
||||||
setWebhookUrl(project.webhookUrl);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
const handleProjectToolsUpdate = useCallback(async () => {
|
||||||
|
// Lightweight refresh for tool-only updates
|
||||||
|
const projectConfig = await getProjectConfig(projectId);
|
||||||
|
|
||||||
|
setProject(projectConfig);
|
||||||
|
setProjectConfig(projectConfig);
|
||||||
|
|
||||||
|
// Update MCP servers if they changed
|
||||||
|
if (projectConfig.mcpServers) {
|
||||||
|
setProjectMcpServers(projectConfig.mcpServers);
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
// Add this useEffect for initial load
|
// Add this useEffect for initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
|
|
@ -89,19 +94,19 @@ export function App({
|
||||||
<div>Loading workflow...</div>
|
<div>Loading workflow...</div>
|
||||||
</div>}
|
</div>}
|
||||||
{!loading && !workflow && <div>No workflow found!</div>}
|
{!loading && !workflow && <div>No workflow found!</div>}
|
||||||
{!loading && project && workflow && (dataSources !== null) && (projectTools !== null) && <WorkflowEditor
|
{!loading && project && workflow && (dataSources !== null) && <WorkflowEditor
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
isLive={mode == 'live'}
|
isLive={mode == 'live'}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
dataSources={dataSources}
|
dataSources={dataSources}
|
||||||
projectTools={projectTools}
|
projectConfig={projectConfig || project}
|
||||||
useRag={useRag}
|
useRag={useRag}
|
||||||
mcpServerUrls={projectMcpServers}
|
mcpServerUrls={projectMcpServers}
|
||||||
toolWebhookUrl={webhookUrl}
|
|
||||||
defaultModel={defaultModel}
|
defaultModel={defaultModel}
|
||||||
eligibleModels={eligibleModels}
|
eligibleModels={eligibleModels}
|
||||||
onChangeMode={handleSetMode}
|
onChangeMode={handleSetMode}
|
||||||
onRevertToLive={handleRevertToLive}
|
onRevertToLive={handleRevertToLive}
|
||||||
|
onProjectToolsUpdated={handleProjectToolsUpdate}
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal, ModalContent, ModalHeader, ModalBody } from '@heroui/react';
|
||||||
|
import { ToolsConfig } from '../../tools/components/ToolsConfig';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types';
|
||||||
|
|
||||||
|
interface ToolsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
projectId: string;
|
||||||
|
tools: z.infer<typeof Workflow.shape.tools>;
|
||||||
|
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToolsModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
projectId,
|
||||||
|
tools,
|
||||||
|
onAddTool
|
||||||
|
}: ToolsModalProps) {
|
||||||
|
function handleAddTool(tool: Partial<z.infer<typeof WorkflowTool>>) {
|
||||||
|
onAddTool(tool);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
size="5xl"
|
||||||
|
scrollBehavior="inside"
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<h3 className="text-lg font-semibold">
|
||||||
|
Add tools
|
||||||
|
</h3>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<ToolsConfig
|
||||||
|
useComposioTools={true}
|
||||||
|
projectId={projectId}
|
||||||
|
tools={tools}
|
||||||
|
onAddTool={handleAddTool}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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, 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,10 @@ 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 { ToolsModal } from './components/ToolsModal';
|
||||||
|
import { ToolkitAuthModal } from '../tools/components/ToolkitAuthModal';
|
||||||
|
import { deleteConnectedAccount } from '@/app/actions/composio_actions';
|
||||||
|
import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal';
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
@ -33,8 +39,8 @@ const buttonClasses = "text-sm px-3 py-1.5 bg-indigo-50 hover:bg-indigo-100 text
|
||||||
interface EntityListProps {
|
interface EntityListProps {
|
||||||
agents: z.infer<typeof WorkflowAgent>[];
|
agents: z.infer<typeof WorkflowAgent>[];
|
||||||
tools: z.infer<typeof WorkflowTool>[];
|
tools: 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 {
|
||||||
|
|
@ -80,6 +88,7 @@ const ListItemWithMenu = ({
|
||||||
iconClassName,
|
iconClassName,
|
||||||
mcpServerName,
|
mcpServerName,
|
||||||
dragHandle,
|
dragHandle,
|
||||||
|
isMocked,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
|
|
@ -92,10 +101,11 @@ const ListItemWithMenu = ({
|
||||||
iconClassName?: string;
|
iconClassName?: string;
|
||||||
mcpServerName?: string;
|
mcpServerName?: string;
|
||||||
dragHandle?: React.ReactNode;
|
dragHandle?: React.ReactNode;
|
||||||
|
isMocked?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
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 +126,27 @@ 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">
|
{isMocked && (
|
||||||
{menuContent}
|
<Tooltip content="Mocked" size="sm" delay={500}>
|
||||||
</div>
|
<div className="w-4 h-4 rounded-full bg-purple-500 flex items-center justify-center text-xs font-medium text-white">
|
||||||
|
M
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{menuContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -166,44 +181,58 @@ 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}
|
||||||
|
isMocked={tool.mockTool}
|
||||||
|
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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -220,8 +249,8 @@ type ComposioToolkit = {
|
||||||
export function EntityList({
|
export function EntityList({
|
||||||
agents,
|
agents,
|
||||||
tools,
|
tools,
|
||||||
projectTools,
|
|
||||||
prompts,
|
prompts,
|
||||||
|
workflow,
|
||||||
selectedEntity,
|
selectedEntity,
|
||||||
startAgentName,
|
startAgentName,
|
||||||
onSelectAgent,
|
onSelectAgent,
|
||||||
|
|
@ -235,7 +264,9 @@ export function EntityList({
|
||||||
onDeleteAgent,
|
onDeleteAgent,
|
||||||
onDeleteTool,
|
onDeleteTool,
|
||||||
onDeletePrompt,
|
onDeletePrompt,
|
||||||
|
onProjectToolsUpdated,
|
||||||
projectId,
|
projectId,
|
||||||
|
projectConfig,
|
||||||
onReorderAgents,
|
onReorderAgents,
|
||||||
onShowVisualise,
|
onShowVisualise,
|
||||||
}: EntityListProps & {
|
}: EntityListProps & {
|
||||||
|
|
@ -243,21 +274,20 @@ 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 [showToolsModal, setShowToolsModal] = useState(false);
|
||||||
|
|
||||||
const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => {
|
const handleAddAgentWithType = (agentType: 'internal' | 'user_facing') => {
|
||||||
onAddAgent({
|
onAddAgent({
|
||||||
outputVisibility: agentType
|
outputVisibility: agentType
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// Merge workflow tools with project tools
|
|
||||||
const mergedTools = [...tools, ...projectTools];
|
|
||||||
const selectedRef = useRef<HTMLButtonElement>(null);
|
const selectedRef = useRef<HTMLButtonElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [containerHeight, setContainerHeight] = useState<number>(0);
|
const [containerHeight, setContainerHeight] = useState<number>(0);
|
||||||
|
|
||||||
// collect composio tools
|
// collect composio tools
|
||||||
const composioTools: Record<string, ComposioToolkit> = {};
|
const composioTools: Record<string, ComposioToolkit> = {};
|
||||||
for (const tool of mergedTools) {
|
for (const tool of tools) {
|
||||||
if (tool.isComposio) {
|
if (tool.isComposio) {
|
||||||
if (!composioTools[tool.composioData?.toolkitSlug || '']) {
|
if (!composioTools[tool.composioData?.toolkitSlug || '']) {
|
||||||
composioTools[tool.composioData?.toolkitSlug || ''] = {
|
composioTools[tool.composioData?.toolkitSlug || ''] = {
|
||||||
|
|
@ -496,35 +526,37 @@ 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-1">
|
||||||
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 }));
|
||||||
}}
|
setShowToolsModal(true);
|
||||||
className={`group ${buttonClasses}`}
|
}}
|
||||||
showHoverContent={true}
|
className={`group ${buttonClasses}`}
|
||||||
hoverContent="Add Tool"
|
showHoverContent={true}
|
||||||
>
|
hoverContent="Add Tool"
|
||||||
<PlusIcon className="w-4 h-4" />
|
>
|
||||||
</Button>
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{expandedPanels.tools && (
|
{expandedPanels.tools && (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{mergedTools.length > 0 ? (
|
{tools.length > 0 ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{/* Group tools by server */}
|
{/* Group tools by server */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// Get custom tools (non-MCP tools)
|
// Get custom tools (non-MCP tools)
|
||||||
const customTools = mergedTools.filter(tool => !tool.isMcp && !tool.isComposio);
|
const customTools = tools.filter(tool => !tool.isMcp && !tool.isComposio);
|
||||||
|
|
||||||
// Group MCP tools by server
|
// Group MCP tools by server
|
||||||
const serverTools = mergedTools.reduce((acc, tool) => {
|
const serverTools = tools.reduce((acc, tool) => {
|
||||||
if (tool.isMcp && tool.mcpServerName) {
|
if (tool.isMcp && tool.mcpServerName) {
|
||||||
if (!acc[tool.mcpServerName]) {
|
if (!acc[tool.mcpServerName]) {
|
||||||
acc[tool.mcpServerName] = [];
|
acc[tool.mcpServerName] = [];
|
||||||
|
|
@ -532,21 +564,26 @@ export function EntityList({
|
||||||
acc[tool.mcpServerName].push(tool);
|
acc[tool.mcpServerName].push(tool);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, typeof mergedTools>);
|
}, {} as Record<string, typeof tools>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Show composio cards */}
|
{/* Show composio cards - ordered by status */}
|
||||||
{Object.values(composioTools).map((card) => (
|
{Object.values(composioTools)
|
||||||
<ComposioCard
|
.map((card) => (
|
||||||
key={card.slug}
|
<ComposioCard
|
||||||
card={card}
|
key={card.slug}
|
||||||
selectedEntity={selectedEntity}
|
card={card}
|
||||||
onSelectTool={handleToolSelection}
|
selectedEntity={selectedEntity}
|
||||||
onDeleteTool={onDeleteTool}
|
onSelectTool={handleToolSelection}
|
||||||
selectedRef={selectedRef}
|
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]) => (
|
||||||
|
|
@ -564,23 +601,24 @@ export function EntityList({
|
||||||
{/* Show custom tools */}
|
{/* Show custom tools */}
|
||||||
{customTools.length > 0 && (
|
{customTools.length > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{customTools.map((tool, index) => (
|
{customTools.map((tool, index) => (
|
||||||
<ListItemWithMenu
|
<ListItemWithMenu
|
||||||
key={`custom-tool-${index}`}
|
key={`custom-tool-${index}`}
|
||||||
name={tool.name}
|
name={tool.name}
|
||||||
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
|
isSelected={selectedEntity?.type === "tool" && selectedEntity.name === tool.name}
|
||||||
onClick={() => handleToolSelection(tool.name)}
|
onClick={() => handleToolSelection(tool.name)}
|
||||||
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
||||||
icon={<Boxes className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />}
|
icon={<Boxes className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />}
|
||||||
menuContent={
|
isMocked={tool.mockTool}
|
||||||
<EntityDropdown
|
menuContent={
|
||||||
name={tool.name}
|
<EntityDropdown
|
||||||
onDelete={onDeleteTool}
|
name={tool.name}
|
||||||
isLocked={tool.isLibrary}
|
onDelete={onDeleteTool}
|
||||||
/>
|
isLocked={tool.isLibrary}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -682,6 +720,13 @@ export function EntityList({
|
||||||
onClose={() => setShowAgentTypeModal(false)}
|
onClose={() => setShowAgentTypeModal(false)}
|
||||||
onConfirm={handleAddAgentWithType}
|
onConfirm={handleAddAgentWithType}
|
||||||
/>
|
/>
|
||||||
|
<ToolsModal
|
||||||
|
isOpen={showToolsModal}
|
||||||
|
onClose={() => setShowToolsModal(false)}
|
||||||
|
projectId={projectId}
|
||||||
|
tools={tools}
|
||||||
|
onAddTool={onAddTool}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -769,6 +814,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 +826,215 @@ 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 [showDisconnectModal, setShowDisconnectModal] = useState(false);
|
||||||
|
const [isProcessingAuth, setIsProcessingAuth] = 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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleConnect = () => {
|
||||||
|
setShowAuthModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
setShowDisconnectModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDisconnect = 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);
|
||||||
|
setShowDisconnectModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthComplete = () => {
|
||||||
|
setShowAuthModal(false);
|
||||||
|
onProjectToolsUpdated?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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 ? (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Status Badge - only show orange when requires auth and not connected */}
|
||||||
|
{hasToolkitWithAuth && !isToolkitConnected && (
|
||||||
|
<Tooltip
|
||||||
|
content="Disconnected"
|
||||||
|
size="sm"
|
||||||
|
delay={500}
|
||||||
|
>
|
||||||
|
<div className="w-5 h-5 rounded-full bg-orange-500 flex items-center justify-center text-xs font-medium text-white">
|
||||||
|
○
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions Dropdown - only show when requires auth */}
|
||||||
|
{hasToolkitWithAuth && (
|
||||||
|
<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 'connect':
|
||||||
|
handleConnect();
|
||||||
|
break;
|
||||||
|
case 'disconnect':
|
||||||
|
handleDisconnect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabledKeys={[
|
||||||
|
...(isProcessingAuth ? ['connect', 'disconnect'] : []),
|
||||||
|
...(hasToolkitWithAuth && isToolkitConnected ? [] : ['disconnect']),
|
||||||
|
...(hasToolkitWithAuth && !isToolkitConnected ? [] : ['connect'])
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
<span>{card.name}</span>
|
|
||||||
</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
|
isMocked={tool.mockTool}
|
||||||
src={card.logo}
|
menuContent={
|
||||||
alt={`${card.name} logo`}
|
<div className="opacity-0 group-hover/tool:opacity-100 transition-opacity">
|
||||||
className="w-full h-full object-contain rounded"
|
<EntityDropdown
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Disconnect Confirmation Modal */}
|
||||||
|
<ProjectWideChangeConfirmationModal
|
||||||
|
isOpen={showDisconnectModal}
|
||||||
|
onClose={() => setShowDisconnectModal(false)}
|
||||||
|
onConfirm={handleConfirmDisconnect}
|
||||||
|
title={`Disconnect ${card.name}`}
|
||||||
|
confirmationQuestion={`Are you sure you want to disconnect the ${card.name} toolkit?`}
|
||||||
|
confirmButtonText="Disconnect"
|
||||||
|
isLoading={isProcessingAuth}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
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";
|
||||||
|
|
@ -315,7 +316,6 @@ function reducer(state: State, action: Action): State {
|
||||||
required: []
|
required: []
|
||||||
},
|
},
|
||||||
mockTool: true,
|
mockTool: true,
|
||||||
autoSubmitMockedResponse: true,
|
|
||||||
...action.tool
|
...action.tool
|
||||||
});
|
});
|
||||||
draft.selection = {
|
draft.selection = {
|
||||||
|
|
@ -576,26 +576,26 @@ export function WorkflowEditor({
|
||||||
workflow,
|
workflow,
|
||||||
useRag,
|
useRag,
|
||||||
mcpServerUrls,
|
mcpServerUrls,
|
||||||
toolWebhookUrl,
|
|
||||||
defaultModel,
|
defaultModel,
|
||||||
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>>[];
|
||||||
workflow: z.infer<typeof Workflow>;
|
workflow: z.infer<typeof Workflow>;
|
||||||
useRag: boolean;
|
useRag: boolean;
|
||||||
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||||
toolWebhookUrl: string;
|
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
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 +615,7 @@ export function WorkflowEditor({
|
||||||
isLive,
|
isLive,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -978,8 +979,8 @@ export function WorkflowEditor({
|
||||||
<EntityList
|
<EntityList
|
||||||
agents={state.present.workflow.agents}
|
agents={state.present.workflow.agents}
|
||||||
tools={state.present.workflow.tools}
|
tools={state.present.workflow.tools}
|
||||||
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 +1003,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>
|
||||||
|
|
@ -1019,10 +1022,8 @@ export function WorkflowEditor({
|
||||||
workflow={state.present.workflow}
|
workflow={state.present.workflow}
|
||||||
messageSubscriber={updateChatMessages}
|
messageSubscriber={updateChatMessages}
|
||||||
mcpServerUrls={mcpServerUrls}
|
mcpServerUrls={mcpServerUrls}
|
||||||
toolWebhookUrl={toolWebhookUrl}
|
|
||||||
isInitialState={isInitialState}
|
isInitialState={isInitialState}
|
||||||
onPanelClick={handlePlaygroundClick}
|
onPanelClick={handlePlaygroundClick}
|
||||||
projectTools={projectTools}
|
|
||||||
triggerCopilotChat={triggerCopilotChat}
|
triggerCopilotChat={triggerCopilotChat}
|
||||||
/>
|
/>
|
||||||
{state.present.selection?.type === "agent" && <AgentConfig
|
{state.present.selection?.type === "agent" && <AgentConfig
|
||||||
|
|
@ -1033,7 +1034,6 @@ export function WorkflowEditor({
|
||||||
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
|
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
|
||||||
agents={state.present.workflow.agents}
|
agents={state.present.workflow.agents}
|
||||||
tools={state.present.workflow.tools}
|
tools={state.present.workflow.tools}
|
||||||
projectTools={projectTools}
|
|
||||||
prompts={state.present.workflow.prompts}
|
prompts={state.present.workflow.prompts}
|
||||||
dataSources={dataSources}
|
dataSources={dataSources}
|
||||||
handleUpdate={handleUpdateAgent.bind(null, state.present.selection.name)}
|
handleUpdate={handleUpdateAgent.bind(null, state.present.selection.name)}
|
||||||
|
|
@ -1045,15 +1045,12 @@ export function WorkflowEditor({
|
||||||
{state.present.selection?.type === "tool" && (() => {
|
{state.present.selection?.type === "tool" && (() => {
|
||||||
const selectedTool = state.present.workflow.tools.find(
|
const selectedTool = state.present.workflow.tools.find(
|
||||||
(tool) => tool.name === state.present.selection!.name
|
(tool) => tool.name === state.present.selection!.name
|
||||||
) || projectTools.find(
|
|
||||||
(tool) => tool.name === state.present.selection!.name
|
|
||||||
);
|
);
|
||||||
return <ToolConfig
|
return <ToolConfig
|
||||||
key={state.present.selection.name}
|
key={state.present.selection.name}
|
||||||
tool={selectedTool!}
|
tool={selectedTool!}
|
||||||
usedToolNames={new Set([
|
usedToolNames={new Set([
|
||||||
...state.present.workflow.tools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name),
|
...state.present.workflow.tools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name),
|
||||||
...projectTools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name)
|
|
||||||
])}
|
])}
|
||||||
handleUpdate={handleUpdateTool.bind(null, state.present.selection.name)}
|
handleUpdate={handleUpdateTool.bind(null, state.present.selection.name)}
|
||||||
handleClose={handleUnselectTool}
|
handleClose={handleUnselectTool}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from "@heroui/react";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
|
||||||
|
export interface ProjectWideChangeConfirmationModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title: string;
|
||||||
|
confirmationQuestion: string;
|
||||||
|
confirmButtonText?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectWideChangeConfirmationModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
title,
|
||||||
|
confirmationQuestion,
|
||||||
|
confirmButtonText = "Confirm",
|
||||||
|
isLoading = false,
|
||||||
|
disabled = false,
|
||||||
|
}: ProjectWideChangeConfirmationModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="md">
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-orange-600" />
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{confirmationQuestion}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-orange-50 dark:bg-orange-950/30 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-5 h-5 rounded-full bg-orange-500 flex items-center justify-center text-xs font-medium text-white mt-0.5">
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium text-orange-800 dark:text-orange-200 mb-1">
|
||||||
|
This change will affect the deployed (Live) workflow as well!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onPress={onClose}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onPress={onConfirm}
|
||||||
|
disabled={disabled || isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{confirmButtonText}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
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