diff --git a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/agent/model/enums/AgentEnum.java b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/agent/model/enums/AgentEnum.java index e0692dcaaa..d8136b168e 100644 --- a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/agent/model/enums/AgentEnum.java +++ b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/dynamic/agent/model/enums/AgentEnum.java @@ -27,7 +27,8 @@ public enum AgentEnum { MAPREDUCE_FIN_AGENT("MAPREDUCE_FIN_AGENT", "mapreduce_fin_agent"), MAPREDUCE_MAP_TASK_AGENT("MAPREDUCE_MAP_TASK_AGENT", "mapreduce_map_task_agent"), MAPREDUCE_REDUCE_TASK_AGENT("MAPREDUCE_REDUCE_TASK_AGENT", "mapreduce_reduce_task_agent"), - PPT_GENERATOR_AGENT("PPT_GENERATOR_AGENT", "ppt_generator_agent"); + PPT_GENERATOR_AGENT("PPT_GENERATOR_AGENT", "ppt_generator_agent"), + JSX_GENERATOR_AGENT("JSX_GENERATOR_AGENT", "jsx_generator_agent"); private String agentName; diff --git a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/planning/PlanningFactory.java b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/planning/PlanningFactory.java index 6cf67f8eda..6ca785d45c 100644 --- a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/planning/PlanningFactory.java +++ b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/planning/PlanningFactory.java @@ -84,6 +84,7 @@ import com.alibaba.cloud.ai.example.manus.tool.textOperator.TextFileOperator; import com.alibaba.cloud.ai.example.manus.tool.textOperator.TextFileService; import com.alibaba.cloud.ai.example.manus.tool.pptGenerator.PptGeneratorOperator; +import com.alibaba.cloud.ai.example.manus.tool.jsxGenerator.JsxGeneratorOperator; import com.alibaba.cloud.ai.example.manus.workflow.SummaryWorkflow; import com.fasterxml.jackson.databind.ObjectMapper; @@ -153,6 +154,9 @@ public class PlanningFactory implements IPlanningFactory { @Autowired private PptGeneratorOperator pptGeneratorOperator; + @Autowired + private JsxGeneratorOperator jsxGeneratorOperator; + public PlanningFactory(ChromeDriverService chromeDriverService, PlanExecutionRecorder recorder, ManusProperties manusProperties, TextFileService textFileService, McpService mcpService, SmartContentSavingService innerStorageService, UnifiedDirectoryManager unifiedDirectoryManager, @@ -230,6 +234,7 @@ public Map toolCallbackMap(String planId, String ro toolDefinitions.add(new TextFileOperator(textFileService, innerStorageService, objectMapper)); // toolDefinitions.add(new InnerStorageTool(unifiedDirectoryManager)); // toolDefinitions.add(pptGeneratorOperator); + // toolDefinitions.add(jsxGeneratorOperator); toolDefinitions.add(new InnerStorageContentTool(unifiedDirectoryManager, summaryWorkflow, recorder)); toolDefinitions.add(new FileMergeTool(unifiedDirectoryManager)); // toolDefinitions.add(new GoogleSearch()); diff --git a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/ComponentState.java b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/ComponentState.java new file mode 100644 index 0000000000..f4e4ae1b8f --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/ComponentState.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.example.manus.tool.jsxGenerator; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Component state class for storing current component file path, last operation result, + * and component metadata + */ +public class ComponentState { + + private String currentFilePath = ""; + + private String lastOperationResult = ""; + + private String componentType = ""; + + private Map componentMetadata = new ConcurrentHashMap<>(); + + private String lastGeneratedCode = ""; + + private final Object componentLock = new Object(); + + public String getCurrentFilePath() { + return currentFilePath; + } + + public void setCurrentFilePath(String currentFilePath) { + this.currentFilePath = currentFilePath; + } + + public String getLastOperationResult() { + return lastOperationResult; + } + + public void setLastOperationResult(String lastOperationResult) { + this.lastOperationResult = lastOperationResult; + } + + public String getComponentType() { + return componentType; + } + + public void setComponentType(String componentType) { + this.componentType = componentType; + } + + public Map getComponentMetadata() { + return componentMetadata; + } + + public void setComponentMetadata(Map componentMetadata) { + this.componentMetadata = componentMetadata; + } + + public void addComponentMetadata(String key, Object value) { + this.componentMetadata.put(key, value); + } + + public String getLastGeneratedCode() { + return lastGeneratedCode; + } + + public void setLastGeneratedCode(String lastGeneratedCode) { + this.lastGeneratedCode = lastGeneratedCode; + } + + public Object getComponentLock() { + return componentLock; + } + +} diff --git a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/IJsxGeneratorService.java b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/IJsxGeneratorService.java new file mode 100644 index 0000000000..2e4ac0745b --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/IJsxGeneratorService.java @@ -0,0 +1,171 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.example.manus.tool.jsxGenerator; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import com.alibaba.cloud.ai.example.manus.config.ManusProperties; + +/** + * Interface for JSX/Vue component generation operations. Provides methods for creating, + * saving, and previewing Vue SFC components with Handlebars template support. + */ +public interface IJsxGeneratorService { + + /** + * Generate Vue Single File Component based on component specifications + * @param componentType Type of the component (e.g., 'button', 'form', 'chart') + * @param componentData Component data including props, data, methods, computed, etc. + * @return Generated Vue SFC code + */ + String generateVueSFC(String componentType, Map componentData); + + /** + * Generate Vue template section + * @param componentType Component type + * @param templateData Template data + * @return Generated template HTML + */ + String generateVueTemplate(String componentType, Map templateData); + + /** + * Generate Vue script section + * @param componentData Component data including data, methods, computed, etc. + * @return Generated script section + */ + String generateVueScript(Map componentData); + + /** + * Generate Vue style section + * @param styleData Style specifications + * @return Generated style section + */ + String generateVueStyle(Map styleData); + + /** + * Apply Handlebars template for quick code generation + * @param templateName Template name (e.g., 'counter-button', 'data-form') + * @param templateData Data to apply to template + * @return Generated code from template + */ + String applyHandlebarsTemplate(String templateName, Map templateData); + + /** + * Register a new Handlebars template + * @param templateName Template name + * @param templateContent Template content + */ + void registerTemplate(String templateName, String templateContent); + + /** + * Get available template names + * @return Set of available template names + */ + Set getAvailableTemplates(); + + /** + * Save Vue SFC code to a file + * @param planId Plan ID + * @param filePath File path to save the Vue SFC code + * @param vueSfcCode Vue SFC code to save + * @return Absolute path of the saved file + * @throws IOException if saving fails + */ + String saveVueSfcToFile(String planId, String filePath, String vueSfcCode) throws IOException; + + /** + * Update existing Vue component file + * @param planId Plan ID + * @param filePath File path + * @param sectionType Section to update ('template', 'script', 'style') + * @param newContent New content for the section + * @throws IOException if update fails + */ + void updateVueComponent(String planId, String filePath, String sectionType, String newContent) throws IOException; + + /** + * Generate Sandpack preview configuration + * @param planId Plan ID + * @param filePath Path to the Vue file + * @param dependencies Additional dependencies needed + * @return Sandpack configuration JSON + */ + String generateSandpackConfig(String planId, String filePath, Map dependencies); + + /** + * Generate a preview URL for the Vue component in Sandpack + * @param planId Plan ID + * @param filePath Path to the Vue file + * @return Preview URL + */ + String generatePreviewUrl(String planId, String filePath); + + /** + * Validate Vue SFC syntax + * @param vueSfcCode Vue SFC code to validate + * @return Validation result with errors if any + */ + String validateVueSfc(String vueSfcCode); + + /** + * Get component state for specified plan + * @param planId Plan ID + * @return Component state + */ + Object getComponentState(String planId); + + /** + * Close components for specified plan + * @param planId Plan ID + */ + void closeComponentForPlan(String planId); + + /** + * Update component state + * @param planId Plan ID + * @param filePath File path + * @param operationResult Operation result + */ + void updateComponentState(String planId, String filePath, String operationResult); + + /** + * Get current component file path + * @param planId Plan ID + * @return Current component file path + */ + String getCurrentFilePath(String planId); + + /** + * Get last operation result for a plan + * @param planId Plan ID + * @return Last operation result + */ + String getLastOperationResult(String planId); + + /** + * Get Manus properties + * @return Manus properties + */ + ManusProperties getManusProperties(); + + /** + * Clean up resources + */ + void cleanup(); + +} diff --git a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorOperator.java b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorOperator.java new file mode 100644 index 0000000000..049954d063 --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorOperator.java @@ -0,0 +1,275 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.example.manus.tool.jsxGenerator; + +import com.alibaba.cloud.ai.example.manus.tool.AbstractBaseTool; +import com.alibaba.cloud.ai.example.manus.tool.code.ToolExecuteResult; +import com.alibaba.cloud.ai.example.manus.tool.filesystem.UnifiedDirectoryManager; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class JsxGeneratorOperator extends AbstractBaseTool { + + private static final Logger log = LoggerFactory.getLogger(JsxGeneratorOperator.class); + + private final IJsxGeneratorService jsxGeneratorService; + + private final ObjectMapper objectMapper; + + private final UnifiedDirectoryManager unifiedDirectoryManager; + + private static final String TOOL_NAME = "jsx_generator_operator"; + + public JsxGeneratorOperator(IJsxGeneratorService jsxGeneratorService, ObjectMapper objectMapper, + UnifiedDirectoryManager unifiedDirectoryManager) { + this.jsxGeneratorService = jsxGeneratorService; + this.objectMapper = objectMapper; + this.unifiedDirectoryManager = unifiedDirectoryManager; + } + + /** + * Helper method to create detailed error messages + */ + private String createDetailedErrorMessage(String action, String missingParam, JsxGeneratorTool.JsxInput input) { + StringBuilder sb = new StringBuilder(); + sb.append("Error: ") + .append(missingParam) + .append(" parameter is required for ") + .append(action) + .append(" operation.\n"); + sb.append("Received parameters: ").append(input.toString()).append("\n"); + sb.append("Expected format for ").append(action).append(":\n"); + + switch (action) { + case "generate_vue": + sb.append("{\n"); + sb.append(" \"action\": \"generate_vue\",\n"); + sb.append(" \"component_type\": \"\", // Required: button, form, chart, counter, etc.\n"); + sb.append(" \"component_data\": { // Optional: component specifications\n"); + sb.append(" \"name\": \"\",\n"); + sb.append(" \"data\": {},\n"); + sb.append(" \"methods\": {},\n"); + sb.append(" \"template\": \"\",\n"); + sb.append(" \"style\": \"\"\n"); + sb.append(" }\n"); + sb.append("}"); + break; + case "apply_template": + sb.append("{\n"); + sb.append(" \"action\": \"apply_template\",\n"); + sb.append(" \"template_name\": \"\", // Required: template name\n"); + sb.append(" \"template_data\": {} // Optional: data for template\n"); + sb.append("}"); + break; + case "save": + sb.append("{\n"); + sb.append(" \"action\": \"save\",\n"); + sb.append(" \"file_path\": \"\", // Required: path to save file\n"); + sb.append(" \"vue_sfc_code\": \"\" // Required: Vue SFC code\n"); + sb.append("}"); + break; + case "validate": + sb.append("{\n"); + sb.append(" \"action\": \"validate\",\n"); + sb.append(" \"vue_sfc_code\": \"\" // Required: Vue SFC code to validate\n"); + sb.append("}"); + break; + } + + return sb.toString(); + } + + /** + * Run the tool (accepts JsxInput object input) + */ + @Override + public ToolExecuteResult run(JsxGeneratorTool.JsxInput input) { + log.info("JsxGeneratorOperator input: {}", input); + try { + String planId = this.currentPlanId; + + if ("list_templates".equals(input.getAction())) { + // Handle list templates operation + var templates = jsxGeneratorService.getAvailableTemplates(); + return new ToolExecuteResult("Available templates: " + String.join(", ", templates)); + } + else if ("generate_vue".equals(input.getAction())) { + // Handle generate Vue SFC operation + String componentType = input.getComponentType(); + var componentData = input.getComponentData(); + + if (componentType == null || componentType.trim().isEmpty()) { + return new ToolExecuteResult(createDetailedErrorMessage("generate_vue", "component_type", input)); + } + if (componentData == null) { + componentData = new java.util.HashMap<>(); + log.info("No component_data provided, using empty map for component generation"); + } + + log.info("JsxGeneratorOperator - Generating Vue SFC with componentType: {}, componentData keys: {}", + componentType, componentData.keySet()); + String vueSfcCode = jsxGeneratorService.generateVueSFC(componentType, componentData); + jsxGeneratorService.updateComponentState(planId, null, "Vue SFC generated successfully"); + return new ToolExecuteResult( + "Vue SFC generated successfully for component type '" + componentType + "':\n" + vueSfcCode); + } + else if ("apply_template".equals(input.getAction())) { + // Handle apply template operation + String templateName = input.getTemplateName(); + var templateData = input.getTemplateData(); + + if (templateName == null || templateName.trim().isEmpty()) { + return new ToolExecuteResult(createDetailedErrorMessage("apply_template", "template_name", input)); + } + if (templateData == null) { + templateData = new java.util.HashMap<>(); + log.info("No template_data provided, using empty map for template application"); + } + + log.info("JsxGeneratorOperator - Applying template: {}, with data keys: {}", templateName, + templateData.keySet()); + String generatedCode = jsxGeneratorService.applyHandlebarsTemplate(templateName, templateData); + jsxGeneratorService.updateComponentState(planId, null, "Template applied successfully"); + return new ToolExecuteResult("Template '" + templateName + "' applied successfully:\n" + generatedCode); + } + else if ("save".equals(input.getAction())) { + // Handle save operation + String filePath = input.getFilePath(); + String vueSfcCode = input.getVueSfcCode(); + + if (filePath == null || filePath.trim().isEmpty()) { + return new ToolExecuteResult(createDetailedErrorMessage("save", "file_path", input)); + } + if (vueSfcCode == null || vueSfcCode.trim().isEmpty()) { + return new ToolExecuteResult(createDetailedErrorMessage("save", "vue_sfc_code", input)); + } + + log.info("JsxGeneratorOperator - Saving Vue SFC to file: {}, code length: {}", filePath, + vueSfcCode.length()); + String savedPath = jsxGeneratorService.saveVueSfcToFile(planId, filePath, vueSfcCode); + return new ToolExecuteResult("Vue SFC file saved successfully to: " + savedPath); + } + else if ("validate".equals(input.getAction())) { + // Handle validate operation + String vueSfcCode = input.getVueSfcCode(); + + if (vueSfcCode == null || vueSfcCode.trim().isEmpty()) { + return new ToolExecuteResult(createDetailedErrorMessage("validate", "vue_sfc_code", input)); + } + + log.info("JsxGeneratorOperator - Validating Vue SFC code, length: {}", vueSfcCode.length()); + String validationResult = jsxGeneratorService.validateVueSfc(vueSfcCode); + return new ToolExecuteResult("Validation result: " + validationResult); + } + else { + return new ToolExecuteResult("Unsupported operation: " + input.getAction() + + ". Supported operations: generate_vue, apply_template, save, validate, list_templates"); + } + } + catch (IllegalArgumentException e) { + String planId = this.currentPlanId; + jsxGeneratorService.updateComponentState(planId, null, + "Error: Parameter validation failed: " + e.getMessage()); + return new ToolExecuteResult("Parameter validation failed: " + e.getMessage()); + } + catch (Exception e) { + log.error("JSX generation failed", e); + String planId = this.currentPlanId; + jsxGeneratorService.updateComponentState(planId, null, "Error: JSX generation failed: " + e.getMessage()); + return new ToolExecuteResult("JSX generation failed: " + e.getMessage()); + } + } + + @Override + public String getName() { + return TOOL_NAME; + } + + @Override + public String getDescription() { + return "Tool for generating Vue SFC components with Handlebars templates and preview functionality. " + + "Supports operations: generate_vue (Generate Vue component), apply_template (Apply Handlebars template), " + + "save (Save Vue SFC to file), validate (Validate Vue SFC syntax), list_templates (List available templates). " + + "Generated Vue files will be saved in the project directory structure."; + } + + @Override + public String getParameters() { + return """ + { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Action to perform", + "enum": ["generate_vue", "apply_template", "save", "validate", "list_templates"] + }, + "component_type": { + "type": "string", + "description": "Type of component to generate (e.g., button, form, chart)" + }, + "component_data": { + "type": "object", + "description": "Component data including name, data, methods, computed, template, style" + }, + "template_name": { + "type": "string", + "description": "Handlebars template name (e.g., counter-button, data-form)" + }, + "template_data": { + "type": "object", + "description": "Data to apply to Handlebars template" + }, + "file_path": { + "type": "string", + "description": "File path to save the Vue SFC code" + }, + "vue_sfc_code": { + "type": "string", + "description": "Vue SFC code to save or validate" + } + }, + "required": ["action"] + } + """; + } + + @Override + public Class getInputType() { + return JsxGeneratorTool.JsxInput.class; + } + + @Override + public String getServiceGroup() { + return "frontend-tools"; + } + + @Override + public String getCurrentToolStateString() { + return "JSX Generator Operator is ready"; + } + + @Override + public void cleanup(String planId) { + if (jsxGeneratorService != null) { + jsxGeneratorService.closeComponentForPlan(planId); + } + } + +} diff --git a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorService.java b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorService.java new file mode 100644 index 0000000000..f45756c9df --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorService.java @@ -0,0 +1,598 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.example.manus.tool.jsxGenerator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +import com.alibaba.cloud.ai.example.manus.config.ManusProperties; +import com.alibaba.cloud.ai.example.manus.tool.filesystem.UnifiedDirectoryManager; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.annotation.PreDestroy; + +@Service +@Primary +public class JsxGeneratorService implements ApplicationRunner, IJsxGeneratorService { + + private static final Logger log = LoggerFactory.getLogger(JsxGeneratorService.class); + + @Autowired + private ManusProperties manusProperties; + + @Autowired + private UnifiedDirectoryManager unifiedDirectoryManager; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + // Store component states for each plan + private final ConcurrentHashMap componentStates = new ConcurrentHashMap<>(); + + // Store Handlebars templates + private final Map handlebarsTemplates = new ConcurrentHashMap<>(); + + // Supported Vue SFC file extensions (for future validation use) + @SuppressWarnings("unused") + private static final Set SUPPORTED_EXTENSIONS = new HashSet<>(Set.of(".vue", ".js", ".ts")); + + @Override + public void run(ApplicationArguments args) { + log.info("JsxGeneratorService initialized"); + initializeDefaultTemplates(); + } + + private void initializeDefaultTemplates() { + // Initialize default Handlebars templates + registerTemplate("counter-button", """ + + + + + + """); + + registerTemplate("data-form", """ + + + + + + """); + + log.info("Default templates initialized: {}", handlebarsTemplates.keySet()); + } + + private Object getComponentLock(String planId) { + return getComponentState(planId).getComponentLock(); + } + + @Override + public ComponentState getComponentState(String planId) { + return componentStates.computeIfAbsent(planId, k -> new ComponentState()); + } + + @Override + public void closeComponentForPlan(String planId) { + synchronized (getComponentLock(planId)) { + componentStates.remove(planId); + log.info("Closed component state for plan: {}", planId); + } + } + + @Override + public String generateVueSFC(String componentType, Map componentData) { + log.info("Generating Vue SFC for component: {}", componentType); + + StringBuilder sfcBuilder = new StringBuilder(); + + // Generate template section + String template; + Object templateObj = componentData.get("template"); + if (templateObj instanceof String) { + // If template is provided as a string, wrap it in template tags + template = ""; + } + else if (templateObj instanceof Map) { + @SuppressWarnings("unchecked") + Map templateData = (Map) templateObj; + template = generateVueTemplate(componentType, templateData); + } + else { + // Use default template generation + template = generateVueTemplate(componentType, new HashMap<>()); + } + sfcBuilder.append(template).append("\n\n"); + + // Generate script section + String script = generateVueScript(componentData); + sfcBuilder.append(script).append("\n\n"); + + // Generate style section + String style; + Object styleObj = componentData.get("style"); + if (styleObj instanceof String) { + // If style is provided as a string, wrap it in style tags + style = ""; + } + else if (styleObj instanceof Map) { + @SuppressWarnings("unchecked") + Map styleData = (Map) styleObj; + style = generateVueStyle(styleData); + } + else { + // Use default style generation + style = generateVueStyle(new HashMap<>()); + } + sfcBuilder.append(style); + + return sfcBuilder.toString(); + } + + @Override + public String generateVueTemplate(String componentType, Map templateData) { + StringBuilder template = new StringBuilder(); + template.append(""); + return template.toString(); + } + + @Override + public String generateVueScript(Map componentData) { + StringBuilder script = new StringBuilder(); + script.append(""); + return script.toString(); + } + + @Override + public String generateVueStyle(Map styleData) { + StringBuilder style = new StringBuilder(); + style.append(""); + return style.toString(); + } + + @Override + public String applyHandlebarsTemplate(String templateName, Map templateData) { + String template = handlebarsTemplates.get(templateName); + if (template == null) { + throw new IllegalArgumentException("Template not found: " + templateName); + } + + log.info("Applying Handlebars template: {}", templateName); + + // Simple template replacement (in real implementation, use Handlebars library) + String result = template; + for (Map.Entry entry : templateData.entrySet()) { + String placeholder = "{{" + entry.getKey() + "}}"; + result = result.replace(placeholder, String.valueOf(entry.getValue())); + } + + return result; + } + + @Override + public void registerTemplate(String templateName, String templateContent) { + handlebarsTemplates.put(templateName, templateContent); + log.info("Registered template: {}", templateName); + } + + @Override + public Set getAvailableTemplates() { + return new HashSet<>(handlebarsTemplates.keySet()); + } + + @Override + public String saveVueSfcToFile(String planId, String filePath, String vueSfcCode) throws IOException { + log.info("Saving Vue SFC to file: {}", filePath); + + // Get absolute path + Path absolutePath = getAbsolutePath(planId, filePath); + + // Ensure parent directory exists + Files.createDirectories(absolutePath.getParent()); + + // Write Vue SFC code to file + Files.writeString(absolutePath, vueSfcCode); + + // Update component state + updateComponentState(planId, filePath, "Vue SFC file saved successfully"); + + return absolutePath.toString(); + } + + @Override + public void updateVueComponent(String planId, String filePath, String sectionType, String newContent) + throws IOException { + log.info("Updating Vue component section: {} in file: {}", sectionType, filePath); + + Path absolutePath = getAbsolutePath(planId, filePath); + if (!Files.exists(absolutePath)) { + throw new IOException("File does not exist: " + filePath); + } + + String existingContent = Files.readString(absolutePath); + String updatedContent = updateVueSfcSection(existingContent, sectionType, newContent); + + Files.writeString(absolutePath, updatedContent); + updateComponentState(planId, filePath, "Updated " + sectionType + " section"); + } + + private String updateVueSfcSection(String sfcContent, String sectionType, String newContent) { + Pattern pattern = Pattern.compile("(<" + sectionType + "[^>]*>)(.*?)()", Pattern.DOTALL); + Matcher matcher = pattern.matcher(sfcContent); + + if (matcher.find()) { + return matcher.replaceFirst("$1\n" + newContent + "\n$3"); + } + else { + // If section doesn't exist, append it + return sfcContent + "\n\n<" + sectionType + ">\n" + newContent + "\n"; + } + } + + @Override + public String generateSandpackConfig(String planId, String filePath, Map dependencies) { + try { + Map config = new HashMap<>(); + config.put("template", "vue"); + config.put("files", + Map.of("/src/App.vue", Map.of("code", Files.readString(getAbsolutePath(planId, filePath))))); + + if (dependencies != null && !dependencies.isEmpty()) { + config.put("dependencies", dependencies); + } + + return objectMapper.writeValueAsString(config); + } + catch (Exception e) { + log.error("Error generating Sandpack config", e); + return "{}"; + } + } + + @Override + public String generatePreviewUrl(String planId, String filePath) { + // In a real implementation, this would generate a URL to preview in Sandpack + String absolutePath = getAbsolutePath(planId, filePath).toString(); + String previewUrl = "http://localhost:3000/sandpack?file=" + absolutePath; + log.info("Generated preview URL: {}", previewUrl); + return previewUrl; + } + + @Override + public String validateVueSfc(String vueSfcCode) { + // Basic validation - check for required sections + if (!vueSfcCode.contains("")) { + return "Error: Missing template section"; + } + if (!vueSfcCode.contains("")) { + return "Error: Missing script section"; + } + return "Valid Vue SFC"; + } + + @Override + public void updateComponentState(String planId, String filePath, String operationResult) { + ComponentState state = getComponentState(planId); + synchronized (getComponentLock(planId)) { + state.setCurrentFilePath(filePath); + state.setLastOperationResult(operationResult); + } + } + + @Override + public String getCurrentFilePath(String planId) { + return getComponentState(planId).getCurrentFilePath(); + } + + @Override + public String getLastOperationResult(String planId) { + return getComponentState(planId).getLastOperationResult(); + } + + @Override + public ManusProperties getManusProperties() { + return manusProperties; + } + + @PreDestroy + @Override + public void cleanup() { + log.info("Cleaning up JsxGeneratorService resources"); + componentStates.clear(); + handlebarsTemplates.clear(); + } + + /** + * Get absolute path for a given relative path + * @param planId Plan ID + * @param filePath File path + * @return Absolute Path + */ + private Path getAbsolutePath(String planId, String filePath) { + return unifiedDirectoryManager.getRootPlanDirectory(planId).resolve(filePath); + } + + /** + * Clean up component state for specified plan + * @param planId Plan ID + */ + public void cleanupPlanComponents(String planId) { + synchronized (getComponentLock(planId)) { + try { + componentStates.remove(planId); + log.info("Cleaned up component resources for plan: {}", planId); + } + catch (Exception e) { + log.error("Error cleaning up plan components: {}", planId, e); + } + } + } + +} diff --git a/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorTool.java b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorTool.java new file mode 100644 index 0000000000..3cb4ebc281 --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/main/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorTool.java @@ -0,0 +1,407 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.example.manus.tool.jsxGenerator; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.cloud.ai.example.manus.tool.AbstractBaseTool; +import com.alibaba.cloud.ai.example.manus.tool.code.ToolExecuteResult; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class JsxGeneratorTool extends AbstractBaseTool { + + private static final Logger log = LoggerFactory.getLogger(JsxGeneratorTool.class); + + private static final String TOOL_NAME = "vue_component_generator"; + + private static final String TOOL_DESCRIPTION = "Tool for generating Vue SFC components with Handlebars templates and Sandpack preview"; + + private static final String PARAMETERS = "{\"type\":\"object\",\"properties\":{\"action\":{\"type\":\"string\",\"description\":\"Action to perform: generate_vue, apply_template, save, update, preview, validate\"},\"component_type\":{\"type\":\"string\",\"description\":\"Type of component to generate (e.g., button, form, chart)\"},\"component_data\":{\"type\":\"object\",\"description\":\"Component data including name, data, methods, computed, template, style\"},\"template_name\":{\"type\":\"string\",\"description\":\"Handlebars template name (e.g., counter-button, data-form)\"},\"template_data\":{\"type\":\"object\",\"description\":\"Data to apply to Handlebars template\"},\"file_path\":{\"type\":\"string\",\"description\":\"File path to save the Vue SFC code\"},\"vue_sfc_code\":{\"type\":\"string\",\"description\":\"Vue SFC code to save or validate\"},\"section_type\":{\"type\":\"string\",\"description\":\"Section type for update operation (template, script, style)\"},\"new_content\":{\"type\":\"string\",\"description\":\"New content for update operation\"},\"dependencies\":{\"type\":\"object\",\"description\":\"Additional dependencies for Sandpack preview\"}},\"required\":[\"action\"]}"; + + private final IJsxGeneratorService vueGeneratorService; + + /** + * Internal input class for defining input parameters of Vue component generator tool + */ + public static class JsxInput { + + private String action; + + @JsonProperty("component_type") + private String componentType; + + @JsonProperty("component_data") + private Map componentData; + + @JsonProperty("template_name") + private String templateName; + + @JsonProperty("template_data") + private Map templateData; + + @JsonProperty("file_path") + private String filePath; + + @JsonProperty("vue_sfc_code") + private String vueSfcCode; + + @JsonProperty("section_type") + private String sectionType; + + @JsonProperty("new_content") + private String newContent; + + private Map dependencies; + + // Getters and setters + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getComponentType() { + return componentType; + } + + public void setComponentType(String componentType) { + this.componentType = componentType; + } + + public Map getComponentData() { + return componentData; + } + + public void setComponentData(Map componentData) { + this.componentData = componentData; + } + + public String getTemplateName() { + return templateName; + } + + public void setTemplateName(String templateName) { + this.templateName = templateName; + } + + public Map getTemplateData() { + return templateData; + } + + public void setTemplateData(Map templateData) { + this.templateData = templateData; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public String getVueSfcCode() { + return vueSfcCode; + } + + public void setVueSfcCode(String vueSfcCode) { + this.vueSfcCode = vueSfcCode; + } + + public String getSectionType() { + return sectionType; + } + + public void setSectionType(String sectionType) { + this.sectionType = sectionType; + } + + public String getNewContent() { + return newContent; + } + + public void setNewContent(String newContent) { + this.newContent = newContent; + } + + public Map getDependencies() { + return dependencies; + } + + public void setDependencies(Map dependencies) { + this.dependencies = dependencies; + } + + @Override + public String toString() { + return "JsxInput{" + "action='" + action + '\'' + ", componentType='" + componentType + '\'' + + ", componentData=" + componentData + ", templateName='" + templateName + '\'' + ", templateData=" + + templateData + ", filePath='" + filePath + '\'' + ", vueSfcCode='" + + (vueSfcCode != null ? vueSfcCode.substring(0, Math.min(100, vueSfcCode.length())) + "..." : null) + + '\'' + ", sectionType='" + sectionType + '\'' + ", newContent='" + newContent + '\'' + + ", dependencies=" + dependencies + '}'; + } + + } + + public JsxGeneratorTool(IJsxGeneratorService vueGeneratorService) { + this.vueGeneratorService = vueGeneratorService; + } + + @Override + public String getName() { + return TOOL_NAME; + } + + @Override + public String getDescription() { + return TOOL_DESCRIPTION; + } + + @Override + public String getParameters() { + return PARAMETERS; + } + + @Override + public Class getInputType() { + return JsxInput.class; + } + + @Override + public String getServiceGroup() { + return "frontend-tools"; + } + + @Override + public String getCurrentToolStateString() { + return "Vue Component Generator Tool is ready"; + } + + @Override + public void cleanup(String planId) { + // Cleanup logic if needed + } + + /** + * Helper method to create detailed error messages + */ + private String createDetailedErrorMessage(String action, String missingParam, JsxInput input) { + StringBuilder sb = new StringBuilder(); + sb.append("Error: ") + .append(missingParam) + .append(" parameter is required for ") + .append(action) + .append(" operation.\n"); + sb.append("Received parameters: ").append(input.toString()).append("\n"); + sb.append("Expected format for ").append(action).append(":\n"); + + switch (action) { + case "generate_vue": + sb.append("{\n"); + sb.append(" \"action\": \"generate_vue\",\n"); + sb.append(" \"component_type\": \"\", // Required: button, form, chart, counter, etc.\n"); + sb.append(" \"component_data\": { // Optional: component specifications\n"); + sb.append(" \"name\": \"\",\n"); + sb.append(" \"data\": {},\n"); + sb.append(" \"methods\": {},\n"); + sb.append(" \"template\": \"\",\n"); + sb.append(" \"style\": \"\"\n"); + sb.append(" }\n"); + sb.append("}"); + break; + case "apply_template": + sb.append("{\n"); + sb.append(" \"action\": \"apply_template\",\n"); + sb.append(" \"template_name\": \"\", // Required: template name\n"); + sb.append(" \"template_data\": {} // Optional: data for template\n"); + sb.append("}"); + break; + case "save": + sb.append("{\n"); + sb.append(" \"action\": \"save\",\n"); + sb.append(" \"file_path\": \"\", // Required: path to save file\n"); + sb.append(" \"vue_sfc_code\": \"\" // Required: Vue SFC code\n"); + sb.append("}"); + break; + case "update": + sb.append("{\n"); + sb.append(" \"action\": \"update\",\n"); + sb.append(" \"file_path\": \"\", // Required: path to file\n"); + sb.append(" \"section_type\": \"\", // Required: template, script, or style\n"); + sb.append(" \"new_content\": \"\" // Required: new content\n"); + sb.append("}"); + break; + case "preview": + sb.append("{\n"); + sb.append(" \"action\": \"preview\",\n"); + sb.append(" \"file_path\": \"\", // Required: path to Vue file\n"); + sb.append(" \"dependencies\": {} // Optional: additional dependencies\n"); + sb.append("}"); + break; + case "validate": + sb.append("{\n"); + sb.append(" \"action\": \"validate\",\n"); + sb.append(" \"vue_sfc_code\": \"\" // Required: Vue SFC code to validate\n"); + sb.append("}"); + break; + } + + return sb.toString(); + } + + /** + * Execute Vue component generation operations with strongly typed input object + */ + @Override + public ToolExecuteResult run(JsxInput input) { + log.info("VueGeneratorTool input: {}", input); + try { + String planId = this.currentPlanId; + String action = input.getAction(); + + // Basic parameter validation + if (action == null) { + return new ToolExecuteResult(createDetailedErrorMessage("unknown", "action", input)); + } + + return switch (action) { + case "generate_vue" -> { + String componentType = input.getComponentType(); + Map componentData = input.getComponentData(); + + if (componentType == null || componentType.trim().isEmpty()) { + yield new ToolExecuteResult( + createDetailedErrorMessage("generate_vue", "component_type", input)); + } + if (componentData == null) { + componentData = new HashMap<>(); + log.info("No component_data provided, using empty map for component generation"); + } + + log.info("Generating Vue SFC with componentType: {}, componentData keys: {}", componentType, + componentData.keySet()); + String vueSfcCode = vueGeneratorService.generateVueSFC(componentType, componentData); + yield new ToolExecuteResult("Successfully generated Vue SFC code for component type '" + + componentType + "':\n" + vueSfcCode); + } + case "apply_template" -> { + String templateName = input.getTemplateName(); + Map templateData = input.getTemplateData(); + + if (templateName == null || templateName.trim().isEmpty()) { + yield new ToolExecuteResult( + createDetailedErrorMessage("apply_template", "template_name", input)); + } + if (templateData == null) { + templateData = new HashMap<>(); + log.info("No template_data provided, using empty map for template application"); + } + + log.info("Applying template: {}, with data keys: {}", templateName, templateData.keySet()); + String generatedCode = vueGeneratorService.applyHandlebarsTemplate(templateName, templateData); + yield new ToolExecuteResult( + "Successfully applied template '" + templateName + "':\n" + generatedCode); + } + case "save" -> { + String filePath = input.getFilePath(); + String vueSfcCode = input.getVueSfcCode(); + + if (filePath == null || filePath.trim().isEmpty()) { + yield new ToolExecuteResult(createDetailedErrorMessage("save", "file_path", input)); + } + if (vueSfcCode == null || vueSfcCode.trim().isEmpty()) { + yield new ToolExecuteResult(createDetailedErrorMessage("save", "vue_sfc_code", input)); + } + + log.info("Saving Vue SFC to file: {}, code length: {}", filePath, vueSfcCode.length()); + String savedPath = vueGeneratorService.saveVueSfcToFile(planId, filePath, vueSfcCode); + yield new ToolExecuteResult("Successfully saved Vue SFC to file: " + savedPath); + } + case "update" -> { + String filePath = input.getFilePath(); + String sectionType = input.getSectionType(); + String newContent = input.getNewContent(); + + if (filePath == null || filePath.trim().isEmpty()) { + yield new ToolExecuteResult(createDetailedErrorMessage("update", "file_path", input)); + } + if (sectionType == null || sectionType.trim().isEmpty()) { + yield new ToolExecuteResult(createDetailedErrorMessage("update", "section_type", input)); + } + if (newContent == null) { + yield new ToolExecuteResult(createDetailedErrorMessage("update", "new_content", input)); + } + + log.info("Updating Vue component: {}, section: {}, new content length: {}", filePath, sectionType, + newContent.length()); + vueGeneratorService.updateVueComponent(planId, filePath, sectionType, newContent); + yield new ToolExecuteResult("Successfully updated " + sectionType + " section in: " + filePath); + } + case "preview" -> { + String filePath = input.getFilePath(); + Map dependencies = input.getDependencies(); + + if (filePath == null || filePath.trim().isEmpty()) { + yield new ToolExecuteResult(createDetailedErrorMessage("preview", "file_path", input)); + } + + log.info("Generating preview for Vue component: {}, dependencies: {}", filePath, dependencies); + String sandpackConfig = vueGeneratorService.generateSandpackConfig(planId, filePath, dependencies); + String previewUrl = vueGeneratorService.generatePreviewUrl(planId, filePath); + yield new ToolExecuteResult("Successfully generated Sandpack preview:\nURL: " + previewUrl + + "\nConfig: " + sandpackConfig); + } + case "validate" -> { + String vueSfcCode = input.getVueSfcCode(); + + if (vueSfcCode == null || vueSfcCode.trim().isEmpty()) { + yield new ToolExecuteResult(createDetailedErrorMessage("validate", "vue_sfc_code", input)); + } + + log.info("Validating Vue SFC code, length: {}", vueSfcCode.length()); + String validationResult = vueGeneratorService.validateVueSfc(vueSfcCode); + yield new ToolExecuteResult("Validation result: " + validationResult); + } + case "list_templates" -> { + Set templates = vueGeneratorService.getAvailableTemplates(); + yield new ToolExecuteResult("Available templates: " + String.join(", ", templates)); + } + default -> new ToolExecuteResult("Unknown operation: " + action + + ". Supported operations: generate_vue, apply_template, save, update, preview, validate, list_templates\n" + + "Received input: " + input); + }; + + } + catch (IOException e) { + log.error("VueGeneratorTool execution failed", e); + return new ToolExecuteResult("Tool execution failed: " + e.getMessage() + "\nInput was: " + input); + } + catch (Exception e) { + log.error("Unexpected error in VueGeneratorTool", e); + return new ToolExecuteResult("Unexpected error: " + e.getMessage() + "\nInput was: " + input); + } + } + +} diff --git a/spring-ai-alibaba-jmanus/src/main/resources/prompts/startup-agents/en/jsx_generator_agent/agent-config.yml b/spring-ai-alibaba-jmanus/src/main/resources/prompts/startup-agents/en/jsx_generator_agent/agent-config.yml new file mode 100644 index 0000000000..1ce122f1f3 --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/main/resources/prompts/startup-agents/en/jsx_generator_agent/agent-config.yml @@ -0,0 +1,161 @@ +# JSX/Vue Component Generation Agent Configuration +agentName: JSX_GENERATOR_AGENT +agentDescription: A professional JSX/Vue component generation agent capable of automatically creating Vue Single File Components (SFC) with Handlebars templates, supporting component generation, template application, and Sandpack preview. +builtIn: false +availableToolKeys: + - jsx_generator_operator + - inner_storage_content_tool + - text_file_operator + - terminate + +# Next Step Prompt Configuration +nextStepPrompt: | + You are a professional JSX/Vue component generation operator. + + IMPORTANT: Always use the correct JSON format with underscores in field names (e.g., "component_type", not "componentType"). + + Please follow these steps to use the jsx_generator_operator tool to create Vue Single File Components: + + Step 1: Use the generate_vue action to create a new Vue component + + Required JSON format example: + ```json + + "action": "generate_vue", + "component_type": "counter", + "component_data": + "name": "CounterButton", + "data": + "count": 0 + , + "methods": + "increment": "function() this.count++; " + , + "template": "

Clicked: times

", + "style": "button padding: 10px 20px; font-size: 16px; p margin-top: 10px; font-size: 18px; color: #333; .redWhenOver5 color: white; background-color: red; " +
+
+ ``` + + Note: Replace the following placeholders with correct characters when using: + - and with opening and closing curly braces + - and with opening and closing curly braces + - and with opening and closing curly braces + - and with opening and closing curly braces + - and with opening and closing curly braces + - and with opening and closing curly braces + - with Vue interpolation syntax (double curly braces around count) + - onClick with @click + - className with :class + + Parameters: + - component_type: REQUIRED. Type of component (button, form, chart, counter, table, etc.) + - component_data: OPTIONAL. Object containing component specifications: + - name: Component name (string) + - data: Vue data object (Map format) + - methods: Vue methods object (Map format, values can be string functions) + - computed: Vue computed properties (Map format) + - template: HTML template (can be string or Map format) + - style: CSS style (can be string or Map format) + + Important Notes: + - template and style fields support two formats: + 1. String format: Direct HTML/CSS code + 2. Map format: Structured configuration data + - methods field values should be string-formatted JavaScript functions + + Step 2: Use the apply_template action to apply Handlebars templates + + Required JSON format example: + ```json + + "action": "apply_template", + "template_name": "counter-button", + "template_data": + "componentName": "MyCounter", + "initialValue": 0, + "buttonText": "Click Count", + "threshold": 5 + + + ``` + + Parameters: + - template_name: REQUIRED. Name of the Handlebars template + - template_data: OPTIONAL. Data object to populate the template + + Step 3: Use the save action to save the generated Vue SFC code + + Required JSON format example: + ```json + + "action": "save", + "file_path": "components/CounterButton.vue", + "vue_sfc_code": "" + + ``` + + Parameters: + - file_path: REQUIRED. Path where the Vue component file should be saved + - vue_sfc_code: REQUIRED. The complete Vue SFC code to save + + Step 4: Use the preview action to generate a Sandpack preview + + Required JSON format example: + ```json + + "action": "preview", + "file_path": "components/CounterButton.vue", + "dependencies": + "vue": "^3.0.0" + + + ``` + + Parameters: + - file_path: REQUIRED. Path to the Vue component file + - dependencies: OPTIONAL. Additional npm dependencies object + + Step 5: Use the validate action to validate Vue SFC code syntax + + Required JSON format example: + ```json + + "action": "validate", + "vue_sfc_code": "" + + ``` + + Parameters: + - vue_sfc_code: REQUIRED. The Vue SFC code to validate + + Available actions: + - generate_vue: Create a new Vue component + - apply_template: Apply a Handlebars template + - save: Save Vue SFC code to file + - update: Update specific sections of a Vue component + - preview: Generate Sandpack preview + - validate: Validate Vue SFC code syntax + - list_templates: List available Handlebars templates + + ERROR HANDLING: + If you receive an error message, it will include: + 1. The specific missing or invalid parameter + 2. The received parameters for debugging + 3. The expected JSON format for that operation + + Common issues and solutions: + - "component_type parameter is required": Ensure you include "component_type" field in your JSON + - JSON parsing errors: Check that your JSON syntax is valid with double quotes + - Type casting errors: Ensure template and style fields are in correct format (string or Map) + - Missing required fields: Refer to the required parameters for each action above + + BEST PRACTICES: + - Always include the "action" parameter + - Use proper Vue 3 Composition API syntax when generating components + - Ensure proper CSS scoping using the 'scoped' attribute in style blocks + - Test your JSON format before sending to avoid errors + - Use meaningful component names and file paths + - Functions in methods field should use complete JavaScript syntax + + To extract content from existing text or documents, please use text_file_operator or inner_storage_content_tool. diff --git a/spring-ai-alibaba-jmanus/src/main/resources/prompts/startup-agents/zh/jsx_generator_agent/agent-config.yml b/spring-ai-alibaba-jmanus/src/main/resources/prompts/startup-agents/zh/jsx_generator_agent/agent-config.yml new file mode 100644 index 0000000000..858477e8dd --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/main/resources/prompts/startup-agents/zh/jsx_generator_agent/agent-config.yml @@ -0,0 +1,157 @@ +# JSX/Vue组件生成代理配置 +agentName: JSX_GENERATOR_AGENT +agentDescription: 专业的JSX Vue组件生成代理,可以自动创建Vue单文件组件,支持Handlebars模板,组件生成,模板应用,Sandpack预览功能 +builtIn: false +availableToolKeys: + - jsx_generator_operator + - inner_storage_content_tool + - text_file_operator + - terminate + +# 下一步操作提示配置 +nextStepPrompt: | + 您是一名专业的JSX/Vue组件生成操作员。 + + 重要提示:请始终使用正确的JSON格式,字段名使用下划线(如:"component_type",而不是"componentType")。 + + 请按照以下步骤使用 jsx_generator_operator 工具创建Vue单文件组件: + + 步骤1: 使用 generate_vue 操作创建新的Vue组件 + + 必需的JSON格式示例: + ```json + + "action": "generate_vue", + "component_type": "counter", + "component_data": + "name": "CounterButton", + "data": + "count": 0 + , + "methods": + "increment": "function() this.count++; " + , + "template": "

已点击:

", + "style": "button padding: 10px 20px; font-size: 16px; p margin-top: 10px; font-size: 18px; color: #333; .redWhenOver5 color: white; background-color: red; " + + + ``` + + 占位符说明(请在实际使用时替换): + - BRACE 替换为左花括号 + - SLASH_BRACE 替换为右花括号 + - COUNT_VAR 替换为Vue插值语法(双花括号包围count) + - onClick 替换为 @click + - className 替换为 :class + + 参数说明: + - component_type: 必需。组件类型(button、form、chart、counter、table等) + - component_data: 可选。包含组件规格的对象: + - name: 组件名称(字符串) + - data: Vue数据对象(Map格式) + - methods: Vue方法对象(Map格式,值可以是字符串形式的函数) + - computed: Vue计算属性(Map格式) + - template: HTML模板(可以是字符串或Map格式) + - style: CSS样式(可以是字符串或Map格式) + + 重要提示: + - template字段支持两种格式:字符串格式直接提供HTML代码,Map格式提供结构化配置数据 + - style字段支持两种格式:字符串格式直接提供CSS代码,Map格式提供结构化配置数据 + - methods字段的值应为字符串格式的JavaScript函数 + + 步骤2: 使用 apply_template 操作应用Handlebars模板 + + 必需的JSON格式示例: + ```json + + "action": "apply_template", + "template_name": "counter-button", + "template_data": + "componentName": "MyCounter", + "initialValue": 0, + "buttonText": "点击计数", + "threshold": 5 + + + ``` + + 参数说明: + - template_name: 必需。Handlebars模板名称 + - template_data: 可选。用于填充模板的数据对象 + + 步骤3: 使用 save 操作保存生成的Vue SFC代码 + + 必需的JSON格式示例: + ```json + + "action": "save", + "file_path": "components/CounterButton.vue", + "vue_sfc_code": "" + + ``` + + 参数说明: + - file_path: 必需。Vue组件文件应保存的路径 + - vue_sfc_code: 必需。要保存的完整Vue SFC代码 + + 步骤4: 使用 preview 操作生成Sandpack预览 + + 必需的JSON格式示例: + ```json + + "action": "preview", + "file_path": "components/CounterButton.vue", + "dependencies": + "vue": "^3.0.0" + + + ``` + + 参数说明: + - file_path: 必需。Vue组件文件的路径 + - dependencies: 可选。额外的npm依赖对象 + + 步骤5: 使用 validate 操作验证Vue SFC代码语法 + + 必需的JSON格式示例: + ```json + + "action": "validate", + "vue_sfc_code": "" + + ``` + + 参数说明: + - vue_sfc_code: 必需。要验证的Vue SFC代码 + + 可用操作: + - generate_vue: 创建新的Vue组件 + - apply_template: 应用Handlebars模板 + - save: 将Vue SFC代码保存到文件 + - update: 更新Vue组件的特定部分 + - preview: 生成Sandpack预览 + - validate: 验证Vue SFC代码语法 + - list_templates: 列出可用的Handlebars模板 + + 错误处理: + 如果您收到错误消息,它将包括: + 1. 特定的缺失或无效参数 + 2. 接收到的参数用于调试 + 3. 该操作的预期JSON格式 + + 常见问题说明: + - "component_type parameter is required":确保在JSON中包含"component_type"字段 + - JSON解析错误:检查JSON语法是否有效,确保使用双引号 + - 类型转换错误:确保template字段格式正确(字符串或Map) + - 类型转换错误:确保style字段格式正确(字符串或Map) + - 缺少必需字段:参考上述每个操作的必需参数 + + 最佳实践: + - 始终包含"action"参数 + - 生成组件时请使用正确的Vue 3 Composition API语法 + - 确保在style块中使用'scoped'属性进行CSS作用域限制 + - 发送前测试JSON格式以避免错误 + - 使用有意义的组件名称路径 + - methods字段中的函数应使用完整的JavaScript语法 + + 如需从已有文本或文档中提取内容,请使用 text_file_operator 或 inner_storage_content_tool。 diff --git a/spring-ai-alibaba-jmanus/src/test/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorIntegrationTest.java b/spring-ai-alibaba-jmanus/src/test/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorIntegrationTest.java new file mode 100644 index 0000000000..8913d9b1ee --- /dev/null +++ b/spring-ai-alibaba-jmanus/src/test/java/com/alibaba/cloud/ai/example/manus/tool/jsxGenerator/JsxGeneratorIntegrationTest.java @@ -0,0 +1,451 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.example.manus.tool.jsxGenerator; + +import com.alibaba.cloud.ai.example.manus.config.ManusProperties; +import com.alibaba.cloud.ai.example.manus.tool.code.ToolExecuteResult; +import com.alibaba.cloud.ai.example.manus.tool.filesystem.UnifiedDirectoryManager; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JSX Generator Integration Test Class + */ +public class JsxGeneratorIntegrationTest { + + private static final Logger log = LoggerFactory.getLogger(JsxGeneratorIntegrationTest.class); + + private JsxGeneratorService jsxGeneratorService; + + private ObjectMapper objectMapper; + + private UnifiedDirectoryManager unifiedDirectoryManager; + + private ManusProperties manusProperties; + + private JsxGeneratorOperator jsxGeneratorOperator; + + Path tempDir; + + @BeforeEach + void setUp() { + log.info("===== Starting Test Setup ====="); + + // Initialize mock dependencies + manusProperties = mock(ManusProperties.class); + unifiedDirectoryManager = mock(UnifiedDirectoryManager.class); + objectMapper = new ObjectMapper(); + + // Create JsxGeneratorService instance + jsxGeneratorService = new JsxGeneratorService(); + log.info("JsxGeneratorService initialization completed"); + + // Inject dependencies using reflection + try { + Field manusField = JsxGeneratorService.class.getDeclaredField("manusProperties"); + manusField.setAccessible(true); + manusField.set(jsxGeneratorService, manusProperties); + + Field unifiedDirField = JsxGeneratorService.class.getDeclaredField("unifiedDirectoryManager"); + unifiedDirField.setAccessible(true); + unifiedDirField.set(jsxGeneratorService, unifiedDirectoryManager); + + log.info("Successfully injected dependencies to JsxGeneratorService"); + } + catch (Exception e) { + log.error("Failed to inject dependencies", e); + throw new RuntimeException("Failed to inject dependencies", e); + } + + // Manually call run method to initialize default templates + try { + jsxGeneratorService.run(null); + log.info("Successfully initialized default templates"); + } + catch (Exception e) { + log.error("Failed to initialize default templates", e); + throw new RuntimeException("Failed to initialize default templates", e); + } + + // Initialize operator + jsxGeneratorOperator = new JsxGeneratorOperator((IJsxGeneratorService) jsxGeneratorService, objectMapper, + unifiedDirectoryManager); + log.info("JsxGeneratorOperator initialization completed"); + + // Set test directory + tempDir = Path.of("./extensions"); + log.info("Set test output directory: {}", tempDir); + + // Set mock behavior + try { + when(unifiedDirectoryManager.getRootPlanDirectory(anyString())).thenAnswer(invocation -> { + String planId = invocation.getArgument(0); + return tempDir.resolve("test-jsx-output").resolve(planId); + }); + log.info("UnifiedDirectoryManager mock behavior setup completed"); + } + catch (Exception e) { + log.error("Error occurred while setting UnifiedDirectoryManager mock", e); + throw new RuntimeException(e); + } + + log.info("===== Test Setup Completed ====="); + } + + @Test + void testGenerateBasicVueComponent() throws Exception { + log.info("\n===== Starting Test: Generate Basic Vue Component ====="); + + String planId = "test-plan-vue"; + log.info("Prepare test data: planId={}", planId); + + // Prepare input data + JsxGeneratorTool.JsxInput input = new JsxGeneratorTool.JsxInput(); + input.setAction("generate_vue"); + input.setComponentType("button"); + + Map componentData = new HashMap<>(); + componentData.put("name", "TestButton"); + Map data = new HashMap<>(); + data.put("count", 0); + componentData.put("data", data); + input.setComponentData(componentData); + + log.info("Vue component data setup completed: componentType={}, name={}", input.getComponentType(), + componentData.get("name")); + + // Set current plan ID + jsxGeneratorOperator.setCurrentPlanId(planId); + log.info("Set current plan ID: {}", planId); + + // Execute test + log.info("Starting Vue component generation operation..."); + ToolExecuteResult result = jsxGeneratorOperator.run(input); + log.info("Vue component generation operation completed"); + + // Verify results + log.info("Starting result verification..."); + assertNotNull(result); + log.info("Verify result is not null: success"); + assertTrue(result.getOutput().contains("successfully")); + log.info("Verify result contains success message: success"); + assertTrue(result.getOutput().contains("