Developing Visual Editors for Backend Type Elements
After completing the Extend Your Own Element Families tutorial, you can create and run element instances. However, configuration parameters must be manually modified in files, which presents a challenge for business developers unfamiliar with code.
This guide demonstrates how to develop a visual configuration editor for a DingTalk robot, delivering the same graphical configuration experience as official JitAi elements.
Effect preview
Editor architecture
| Element Level | fullName | Main Responsibilities |
|---|---|---|
| Editor Element | imRobots.dingTalkStreamType.Editor | Type points to editors.React; provides visual configuration interface for DingTalk robot |
| Target Element | imRobots.dingTalkStreamType | The backend Type element being edited, completed in previous chapters |
Editor directory structure
imRobots/
└── dingTalkStreamType/ # Backend Type element (completed)
├── e.json # Type element definition
├── config.json # Configuration template
├── loader.py # Loader implementation
├── client_manager.py # Client management
├── handler.py # Message handler
└── Editor/ # Editor directory (new)
├── e.json # Editor element definition
├── index.ts # Editor entry file
├── Editor.tsx # Editor implementation file
├── Editor.style.ts # Style file
└── utils.ts # Utility functions
Step-by-step guide
Creating the editor directory
Create an Editor subdirectory under the dingTalkStreamType directory:
# Execute in dingTalkStreamType directory
mkdir -p Editor
Installing dependencies
Add the Monaco Editor dependency to your project's package.json:
{
"dependencies": {
"@monaco-editor/react": "^4.7.0"
}
}
Implementing editor files
- Element Definition File
- Entry File
- Editor Implementation
- Style File
- Utility Functions
Create the editor element definition file Editor/e.json:
In JitAi, editors are elements with their own e.json definition files.
{
"frontBundleEntry": "./index.ts",
"outputName": "index",
"tag": "editor",
"targetType": [
"imRobots.dingTalkStreamType"
],
"title": "DingTalk Robot Configuration Editor",
"type": "editors.React"
}
Editor element configuration:
title: Display name for the editor elementtype: Must beeditors.Reactto indicate a React editor elementtag: Must beeditorto indicate a backend element editortargetType: Full name of the target backend elementfrontBundleEntry: Entry point for the editor
Create the editor entry file Editor/index.ts:
export { Editor } from './Editor';
Backend element editors must export a component named Editor. This is a fixed convention that enables JitAi tools to recognize the editor.
Create the main editor file Editor/Editor.tsx:
import type { FC } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Form, Input, Select, Button, message, Space, Card } from 'antd';
import { Editor as MonacoEditor } from '@monaco-editor/react';
import { editorStyles } from './Editor.style';
import { validateConfig, formatConfigJson } from './utils';
interface DingTalkEditorProps {
elementInfo: {
fullName: string;
config: Record<string, any>;
title: string;
};
onSave: (config: Record<string, any>) => Promise<void>;
onCancel: () => void;
}
const DingTalkEditor: FC<DingTalkEditorProps> = ({ elementInfo, onSave, onCancel }) => {
const [form] = Form.useForm();
const [configJson, setConfigJson] = useState('');
const [loading, setLoading] = useState(false);
const didMountRef = useRef(false);
// Initialize form data
useEffect(() => {
if (!didMountRef.current) {
didMountRef.current = true;
const config = elementInfo.config || {};
// Set form field values
form.setFieldsValue({
agent: config.agent || '',
clientId: config.clientId || '',
clientSecret: config.clientSecret || ''
});
// Set JSON editor content
setConfigJson(formatConfigJson(config));
}
}, [elementInfo, form]);
// Handle form submission
const handleSave = async () => {
try {
setLoading(true);
// Validate form fields
const formValues = await form.validateFields();
// Validate JSON configuration
const jsonConfig = validateConfig(configJson);
// Merge form data and JSON configuration
const finalConfig = {
...jsonConfig,
...formValues
};
await onSave(finalConfig);
message.success('Configuration saved successfully');
} catch (error) {
console.error('Save failed:', error);
message.error('Save failed, please check configuration');
} finally {
setLoading(false);
}
};
// Handle JSON editor changes
const handleJsonChange = (value: string | undefined) => {
setConfigJson(value || '');
};
return (
<div style={editorStyles.container}>
<Card title="DingTalk Robot Configuration" style={editorStyles.card}>
<Form
form={form}
layout="vertical"
style={editorStyles.form}
>
<Form.Item
label="AI Agent"
name="agent"
rules={[{ required: true, message: 'Please select AI Agent' }]}
>
<Select
placeholder="Select AI Agent"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={[
{ value: 'aiagents.ragTest', label: 'RAG Test Agent' },
{ value: 'aiagents.chatBot', label: 'Chat Bot Agent' },
// Add more agent options as needed
]}
/>
</Form.Item>
<Form.Item
label="Client ID"
name="clientId"
rules={[{ required: true, message: 'Please enter Client ID' }]}
>
<Input placeholder="Enter DingTalk application Client ID" />
</Form.Item>
<Form.Item
label="Client Secret"
name="clientSecret"
rules={[{ required: true, message: 'Please enter Client Secret' }]}
>
<Input.Password placeholder="Enter DingTalk application Client Secret" />
</Form.Item>
</Form>
<div style={editorStyles.jsonSection}>
<h4>Advanced Configuration (JSON)</h4>
<MonacoEditor
height="200px"
language="json"
theme="vs-dark"
value={configJson}
onChange={handleJsonChange}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
wordWrap: 'on'
}}
/>
</div>
<div style={editorStyles.actions}>
<Space>
<Button onClick={onCancel}>
Cancel
</Button>
<Button
type="primary"
loading={loading}
onClick={handleSave}
>
Save Configuration
</Button>
</Space>
</div>
</Card>
</div>
);
};
export { DingTalkEditor as Editor };
Create the style file Editor/Editor.style.ts:
import type { CSSProperties } from 'react';
export const editorStyles: Record<string, CSSProperties> = {
container: {
padding: '20px',
height: '100%',
overflow: 'auto'
},
card: {
maxWidth: '800px',
margin: '0 auto'
},
form: {
marginBottom: '24px'
},
jsonSection: {
marginBottom: '24px'
},
actions: {
textAlign: 'right' as const,
borderTop: '1px solid #f0f0f0',
paddingTop: '16px'
}
};
Create the utility functions file Editor/utils.ts:
/**
* Validate JSON configuration
*/
export function validateConfig(configJson: string): Record<string, any> {
try {
if (!configJson.trim()) {
return {};
}
const parsed = JSON.parse(configJson);
if (typeof parsed !== 'object' || parsed === null) {
throw new Error('Configuration must be a JSON object');
}
return parsed;
} catch (error) {
throw new Error(`JSON configuration format error: ${error.message}`);
}
}
/**
* Format configuration as JSON string
*/
export function formatConfigJson(config: Record<string, any>): string {
try {
// Remove form fields, only keep advanced configuration
const { agent, clientId, clientSecret, ...advancedConfig } = config;
if (Object.keys(advancedConfig).length === 0) {
return '{\n \n}';
}
return JSON.stringify(advancedConfig, null, 2);
} catch (error) {
console.error('Format JSON failed:', error);
return '{\n \n}';
}
}
/**
* Get available AI Agent list
*/
export function getAvailableAgents(): Array<{ value: string; label: string }> {
// In actual implementation, this would fetch from API
return [
{ value: 'aiagents.ragTest', label: 'RAG Test Agent' },
{ value: 'aiagents.chatBot', label: 'Chat Bot Agent' },
{ value: 'aiagents.knowledgeBase', label: 'Knowledge Base Agent' },
];
}
Editor working principles
Backend element editor vs frontend component editor
| Aspect | Backend Element Editor | Frontend Component Editor |
|---|---|---|
| Target | Backend Type elements (like DingTalk robot) | Frontend UI components (like Counter) |
| Configuration Scope | Instance-level configuration files | Component runtime parameters |
| Interface | elementInfo + onSave/onCancel | CompEditorProps + onChangeCompConfig |
| Save Method | Asynchronous save via onSave callback | Real-time sync via onChangeCompConfig |
Data flow
- Configuration Reception: Editor receives current element configuration through
elementInfo - Form Initialization: Initialize form fields and JSON editor content
- User Interaction: User modifies form fields or JSON configuration
- Validation: Validate form data and JSON format
- Save: Call
onSavecallback to persist configuration
Testing the editor
Making the editor take effect
- Clear cache: Delete the
distdirectory in the application directory - Restart service: Restart the desktop client
- Trigger packaging: Access the application page, the system will automatically repackage
Verifying editor functionality
- Open element management: Enter JitAi development tool's element management interface
- Find target element: Locate the DingTalk robot element instance
- Open editor: Click edit button, the visual editor should open
- Test configuration: Try modifying AI Agent, Client ID and other configurations
- Save verification: Save configuration and restart element, confirm configuration takes effect
Troubleshooting common issues
- Editor not opening: Check if
targetTypeine.jsoncorrectly points to the target element - Configuration not saving: Confirm if
onSavecallback is called correctly and handles errors properly - JSON format error: Check if JSON editor content format is correct
Advanced features
Dynamic agent list
You can enhance the editor by dynamically loading available AI Agent lists:
const [agentOptions, setAgentOptions] = useState([]);
useEffect(() => {
// Load available agents
const loadAgents = async () => {
try {
const agents = await fetchAvailableAgents(); // API call
setAgentOptions(agents);
} catch (error) {
console.error('Failed to load agents:', error);
}
};
loadAgents();
}, []);
Configuration validation
Add more comprehensive configuration validation:
export function validateDingTalkConfig(config: any): string[] {
const errors: string[] = [];
if (!config.agent) {
errors.push('AI Agent is required');
}
if (!config.clientId) {
errors.push('Client ID is required');
}
if (!config.clientSecret) {
errors.push('Client Secret is required');
}
if (config.clientId && !/^[a-zA-Z0-9]+$/.test(config.clientId)) {
errors.push('Client ID format is invalid');
}
return errors;
}
Summary
Core steps for developing visual editors for backend elements:
- Create Editor directory + configure editor element definition file
e.json(type: "editors.React") - Implement Editor component: Receive
elementInfo+ render configuration form - Correct export: Export component named
Editor - Configuration persistence: Save configuration through
onSavecallback
Key differences from frontend component editors:
- Use
elementInfointerface instead ofCompEditorProps - Asynchronous save via
onSavecallback instead of real-time sync - Target backend Type elements instead of frontend UI components
- Handle instance-level configuration instead of component runtime parameters
Key points:
- Export name
Editorcannot be changed - Must handle asynchronous save operations and error handling
- JSON editor is suitable for complex configuration scenarios
- Form validation ensures configuration correctness