diff --git a/cofounder/api/server.js b/cofounder/api/server.js index f1d7828..49936bf 100644 --- a/cofounder/api/server.js +++ b/cofounder/api/server.js @@ -647,6 +647,175 @@ const stream_to_client = async ({ project, key, meta }) => { }; // ---------------------------------------------------------------------------------------------------- +// Helper function to safely access nested properties from an object +function getValueFromPath(obj, path) { + if (path === "" || typeof path === "undefined" || path === null) return obj; // Return the object itself if path is empty or undefined + if (!obj) return null; + + const parts = path.split('.'); + let current = obj; + for (const part of parts) { + if (current && typeof current === 'object' && part in current) { + current = current[part]; + } else { + console.warn(`getValueFromPath: Path [${path}] not found in object part [${part}]`, { object: obj } ); + return null; + } + } + return current; +} + +// Helper function for placeholder resolution +function resolvePlaceholders(parameters, projectData, workflowInputs) { + console.log("resolvePlaceholders: Starting resolution with parameters:", JSON.stringify(parameters, null, 2)); + console.log("resolvePlaceholders: projectData keys:", Object.keys(projectData || {})); + console.log("resolvePlaceholders: workflowInputs:", JSON.stringify(workflowInputs, null, 2)); + + const resolvedParameters = {}; + + for (const key in parameters) { + if (Object.hasOwnProperty.call(parameters, key)) { + const value = parameters[key]; + let resolvedValue = value; + + if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) { + const placeholder = value.substring(2, value.length - 2).trim(); // e.g., "workflow.input.X" or "data.node_id.path" + console.log(`resolvePlaceholders: Found placeholder for key [${key}]: ${placeholder}`); + + if (placeholder.startsWith('workflow.input.')) { + const workflowInputKey = placeholder.substring('workflow.input.'.length); + if (workflowInputs && Object.hasOwnProperty.call(workflowInputs, workflowInputKey)) { + resolvedValue = workflowInputs[workflowInputKey]; + console.log(`resolvePlaceholders: Resolved {{${placeholder}}} from workflowInputs to:`, resolvedValue); + } else { + resolvedValue = null; + console.warn(`resolvePlaceholders: Workflow input key [${workflowInputKey}] not found in workflowInputs. Setting to null.`); + } + } else if (placeholder.startsWith('data.')) { + const fullPath = placeholder.substring('data.'.length); // e.g., "node_id.path.to.value" or "node_id" + const pathParts = fullPath.split('.'); + const nodeKey = pathParts.shift(); // First part is node_key + const actualPathInNodeOutput = pathParts.join('.'); // Rest is the path within that node's data + + if (projectData && Object.hasOwnProperty.call(projectData, nodeKey)) { + const nodeData = projectData[nodeKey]; + console.log(`resolvePlaceholders: Accessing projectData for nodeKey [${nodeKey}]. Path to resolve: [${actualPathInNodeOutput}]`); + resolvedValue = getValueFromPath(nodeData, actualPathInNodeOutput); + if (resolvedValue === null) { + console.warn(`resolvePlaceholders: Path [${actualPathInNodeOutput}] for nodeKey [${nodeKey}] resolved to null.`); + } else { + console.log(`resolvePlaceholders: Resolved {{${placeholder}}} from projectData[${nodeKey}] to:`, JSON.stringify(resolvedValue, null, 2)); + } + } else { + resolvedValue = null; + console.warn(`resolvePlaceholders: Node key [${nodeKey}] not found in projectData. Setting to null.`); + } + } else { + console.warn(`resolvePlaceholders: Unrecognized placeholder format: {{${placeholder}}}. Keeping original.`); + // Keep original value if format is not recognized but matches {{...}} + } + } + resolvedParameters[key] = resolvedValue; + } + } + console.log("resolvePlaceholders: Finished resolution. Resolved parameters:", JSON.stringify(resolvedParameters, null, 2)); + return resolvedParameters; +} + +async function _executeNode({ request, data }) { + try { + const project_id = request.project; + // Assuming request.query.data contains { node_key: "...", input_parameters: { ... } } + const { node_key, input_parameters } = request.query.data; + + console.log("Executing _executeNode with:"); + console.dir({ project_id, node_key, input_parameters }, { depth: null }); + + // Placeholder Resolution + const resolved_parameters = resolvePlaceholders(input_parameters, data, {}); // Passing empty object for workflowInputs for now + console.log("Resolved parameters:"); + console.dir({ resolved_parameters }, { depth: null }); + + // --- Determine System Function ID --- + if (!data || !data.keymap) { + console.error("_executeNode: Error - data.keymap is missing. Cannot determine node metadata or operationId."); + throw new Error("Project keymap is not loaded or missing, cannot execute node."); + } + // const keymap = data.keymap; // Not directly used for operationId in current strategy but good for future. + + // Default operationId is the node_key itself. + let operationId = node_key; + console.log(`_executeNode: Default operationId set to node_key: [${operationId}]`); + + // Check for operation_id_override in resolved_parameters + // Attempt to parse 'messages' if it's a string + if (resolved_parameters.messages && typeof resolved_parameters.messages === 'string') { + try { + resolved_parameters.messages = JSON.parse(resolved_parameters.messages); + console.log(`Successfully parsed 'messages' parameter for node ${node_key}.`); + } catch (e) { + console.warn(`Failed to parse 'messages' input string for node ${node_key}: ${e.message}. Passing as string.`); + } + } + + let final_params_for_system_run = { ...resolved_parameters }; + + if (resolved_parameters.use_operation_id_from_params === true) { + if (typeof resolved_parameters.operation_id_override === 'string' && resolved_parameters.operation_id_override.length > 0) { + operationId = resolved_parameters.operation_id_override; + console.log(`_executeNode: Overriding operationId with value from params: [${operationId}]`); + } else { + console.warn("_executeNode: use_operation_id_from_params was true, but operation_id_override was missing or invalid. Using default operationId."); + } + } + // Clean up override parameters so they are not passed to the system function + delete final_params_for_system_run.use_operation_id_from_params; + delete final_params_for_system_run.operation_id_override; + + if (!operationId) { + console.error("_executeNode: Error - operationId could not be determined."); + throw new Error("Could not determine the system function (operationId) to execute."); + } + console.log(`_executeNode: Final operationId: [${operationId}]`); + + // --- Prepare Data for System Function --- + // resolved_parameters are used directly. + // We also merge the full project data 'data' and add node_key for context. + const system_run_data = { + ...data, // Full project data + ...final_params_for_system_run, // Resolved input parameters for the node + node_key_being_executed: node_key // Explicitly pass which node is being executed + }; + + // --- Call System Function --- + console.log(`_executeNode: Calling cofounder.system.run with id: [${operationId}]`); + console.dir({ system_run_data_payload: system_run_data }, { depth: null }); + + const execution_result = await cofounder.system.run({ + id: operationId, + context: { ...context, project: project_id }, // Global context + project_id + data: system_run_data, + }); + + console.log("_executeNode: cofounder.system.run execution result:"); + console.dir({ execution_result }, { depth: null }); + + return { + success: true, + message: `Node [${node_key}] execution successful with operation [${operationId}].`, + operation_id: operationId, + result: execution_result, // This might be a summary or confirmation + }; + + } catch (error) { + console.error(`Error in _executeNode for node_key [${request.query?.data?.node_key || 'unknown'}]:`, error.message); + // The error will be caught by the main actions route handler's try...catch, + // which will then send a 500 response. + // We re-throw to ensure it's handled there. + throw error; + } +} + // -------------------------------------------------------- SERVER REST API FUNCTION CALLS ------------------------ async function _updateProjectPreferences({ request }) { /* diff --git a/cofounder/dashboard/src/components/flow/keymap.tsx b/cofounder/dashboard/src/components/flow/keymap.tsx index 46b5f1e..c6a4bcf 100644 --- a/cofounder/dashboard/src/components/flow/keymap.tsx +++ b/cofounder/dashboard/src/components/flow/keymap.tsx @@ -4,9 +4,75 @@ export default { type: "pm", name: "Details", desc: "User-submitted Project Details", + "inputs": [ + { + "name": "project_description", + "label": "Project Description", + "type": "textarea", + "defaultValue": "Please describe your project in detail.", + "placeholder": "Enter all relevant details about your project...", + "rows": 5, + "required": true + }, + { + "name": "llm_model", + "label": "LLM Model (for generation assistance, if applicable)", + "type": "text", + "defaultValue": "gpt-4o-mini", + "placeholder": "e.g., gpt-4o-mini, gpt-4" + }, + { + "name": "llm_messages", + "label": "LLM Messages (JSON string, for advanced use)", + "type": "json", + "defaultValue": "[{\"role\": \"user\", \"content\": \"Provide a summary of the project description.\"}]", + "rows": 3, + "placeholder": "Enter messages in JSON format" + }, + { + "name": "max_tokens", + "label": "Max Tokens (for LLM)", + "type": "number", + "defaultValue": 256, + "placeholder": "e.g., 256" + }, + { + "name": "use_streaming", + "label": "Use Streaming (for LLM)", + "type": "boolean", + "defaultValue": true + }, + { + "name": "project_priority", + "label": "Project Priority", + "type": "select", + "defaultValue": "medium", + "options": [ + { "value": "low", "label": "Low" }, + { "value": "medium", "label": "Medium" }, + { "value": "high", "label": "High" } + ], + "required": false + } + ] }, - "pm.prd": { type: "pm", name: "PRD", desc: "Product Requirements Document" }, + "pm.prd": { + type: "pm", + name: "PRD", + desc: "Product Requirements Document", + "inputs": [ + { + "name": "prd_content", + "label": "Product Requirements", + "type": "textarea", + "defaultValue": "# Product Requirements Document\n\n## 1. Introduction\n\n## 2. Goals\n\n## 3. Target Audience\n\n## 4. Features\n", + "rows": 10, + "placeholder": "Define the product requirements...", + "required": true + } + ] + }, "pm.frd": { type: "pm", name: "FRD", desc: "Features Requirements Document" }, "pm.drd": { type: "pm", name: "DRD", desc: "Database Requirements Document" }, "pm.brd": { type: "pm", name: "BRD", desc: "Backend Requirements Document" }, @@ -17,6 +83,17 @@ export default { type: "db", name: "DB/schemas", desc: "Database Tables Schemas", + "inputs": [ + { + "name": "schema_yaml", + "label": "Database Schemas (YAML format)", + "type": "textarea", + "defaultValue": "tables:\n - name: users\n columns:\n - name: id\n type: integer\n primary_key: true\n - name: username\n type: varchar(255)\n unique: true\n - name: email\n type: varchar(255)\n unique: true\n - name: created_at\n type: timestamp\n default: current_timestamp", + "rows": 10, + "placeholder": "Define your table schemas in YAML...", + "required": true + } + ] }, "db.postgres": { type: "db", diff --git a/cofounder/dashboard/src/components/flow/nodes/cofounder-node.tsx b/cofounder/dashboard/src/components/flow/nodes/cofounder-node.tsx index 4fc9e29..8120da8 100644 --- a/cofounder/dashboard/src/components/flow/nodes/cofounder-node.tsx +++ b/cofounder/dashboard/src/components/flow/nodes/cofounder-node.tsx @@ -20,6 +20,16 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; // Import Select components +import { toast } from "sonner"; import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; import yaml_syntax from "react-syntax-highlighter/dist/esm/languages/prism/yaml"; @@ -72,6 +82,21 @@ export default memo(({ data, isConnectable }) => { const streamContainerRef = useRef(null); const [metaHeaderClass, setMetaHeaderClass] = useState(""); const [refresh, setRefresh] = useState(Date.now()); + const [inputParams, setInputParams] = useState({}); + const [isApiLoading, setIsApiLoading] = useState(false); // Local loading state for API call + + // Initialize inputParams with defaultValues when data.meta.inputs changes + useEffect(() => { + if (data.meta?.inputs && Array.isArray(data.meta.inputs)) { + const initialParams = {}; + for (const inputDef of data.meta.inputs) { + initialParams[inputDef.name] = inputDef.defaultValue !== undefined ? inputDef.defaultValue : ''; + } + setInputParams(initialParams); + } else { + setInputParams({}); // Reset if no inputs defined + } + }, [data.meta]); // Dependency array: run when data.meta changes. function getColor() { return data?.meta?.type && color_map[data.meta.type] @@ -99,12 +124,6 @@ export default memo(({ data, isConnectable }) => { } }, [node_data]); - /* - useEffect(() => { - setRefresh(Date.now()) - }, [node_data , node_stream , node_extra]); - */ - function getMinifiedContent() { // webapp component with versionning case if ( @@ -451,6 +470,58 @@ export default memo(({ data, isConnectable }) => { } }, [node_stream]); + const project_id = useSelector((state: any) => state.project.project); + + const handleRunNode = async () => { + console.log(`Run button clicked for node: ${data.key}`); + setIsApiLoading(true); // Set loading true + + if (!project_id) { + console.error("Project ID not found. Cannot run node."); + toast.error("Project ID not found. Cannot run node."); + setIsApiLoading(false); // Reset loading + return; + } + console.log(`Project ID: ${project_id}`); + + const node_key = data.key; + const payload = { + project: project_id, + query: { + action: "execute:node", + data: { + node_key: node_key, + input_parameters: inputParams, + }, + }, + }; + + console.log("Payload for execute:node:", payload); + toast.info(`Executing node: ${node_key}...`); + + try { + const response = await fetch('/api/project/actions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + const result = await response.json(); + + if (response.ok && result && result.end) { + console.log('Node execution request successful:', result); + toast.success(`Node [${node_key}] execution request acknowledged.`); + } else { + console.error('Node execution request failed:', result); + toast.error(`Error executing node ${data.key}: ${result.error || response.statusText || 'Unknown server error'}`); + } + } catch (error) { + console.error('Error calling execute:node API:', error); + toast.error(`API call failed for node ${data.key}: ${error.message || 'Network error or invalid response'}`); + } finally { + setIsApiLoading(false); // Reset loading in finally block + } + }; + return ( <>
@@ -515,53 +586,139 @@ export default memo(({ data, isConnectable }) => { {getMinifiedContent()} - {(node_data && ( - -
+ {/* Input Parameters Section */} +
+

Node Inputs:

+ {Array.isArray(data.meta.inputs) && data.meta.inputs.length > 0 ? ( + data.meta.inputs.map((inputDef) => ( +
+ + {inputDef.type === 'text' && ( + setInputParams(prev => ({ ...prev, [inputDef.name]: e.target.value }))} + placeholder={inputDef.placeholder} + className="bg-black/30 border-[#333] text-white text-xs" + /> + )} + {(inputDef.type === 'textarea' || inputDef.type === 'json') && ( +