decision_agent 0.1.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -1
- data/bin/decision_agent +104 -0
- data/lib/decision_agent/dmn/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +1132 -12
- data/lib/decision_agent/dsl/schema_validator.rb +12 -1
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/web/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/app.js +119 -1
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +71 -0
- data/lib/decision_agent/web/public/styles.css +21 -0
- data/lib/decision_agent/web/server.rb +465 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
- data/spec/advanced_operators_spec.rb +2147 -0
- data/spec/auth/rbac_adapter_spec.rb +228 -0
- data/spec/dmn/decision_graph_spec.rb +282 -0
- data/spec/dmn/decision_tree_spec.rb +203 -0
- data/spec/dmn/feel/errors_spec.rb +18 -0
- data/spec/dmn/feel/functions_spec.rb +400 -0
- data/spec/dmn/feel/simple_parser_spec.rb +274 -0
- data/spec/dmn/feel/types_spec.rb +176 -0
- data/spec/dmn/feel_parser_spec.rb +489 -0
- data/spec/dmn/hit_policy_spec.rb +202 -0
- data/spec/dmn/integration_spec.rb +226 -0
- data/spec/examples.txt +1909 -0
- data/spec/fixtures/dmn/complex_decision.dmn +81 -0
- data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
- data/spec/fixtures/dmn/simple_decision.dmn +40 -0
- data/spec/monitoring/metrics_collector_spec.rb +37 -35
- data/spec/monitoring/monitored_agent_spec.rb +14 -11
- data/spec/performance_optimizations_spec.rb +10 -3
- data/spec/thread_safety_spec.rb +10 -2
- data/spec/web_ui_rack_spec.rb +294 -0
- metadata +66 -1
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// DMN Editor JavaScript
|
|
2
|
+
|
|
3
|
+
// State
|
|
4
|
+
const state = {
|
|
5
|
+
currentModel: null,
|
|
6
|
+
currentDecision: null,
|
|
7
|
+
models: [],
|
|
8
|
+
baseUrl: window.location.origin
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Initialize
|
|
12
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
13
|
+
initializeEventListeners();
|
|
14
|
+
loadModels();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Event Listeners
|
|
18
|
+
function initializeEventListeners() {
|
|
19
|
+
// Header actions
|
|
20
|
+
document.getElementById('new-model-btn').addEventListener('click', () => {
|
|
21
|
+
openModal('new-model-modal');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
document.getElementById('import-btn').addEventListener('click', () => {
|
|
25
|
+
openModal('import-modal');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
document.getElementById('export-btn').addEventListener('click', exportModel);
|
|
29
|
+
document.getElementById('validate-btn').addEventListener('click', validateModel);
|
|
30
|
+
|
|
31
|
+
// Forms
|
|
32
|
+
document.getElementById('new-model-form').addEventListener('submit', createNewModel);
|
|
33
|
+
document.getElementById('add-decision-form').addEventListener('submit', addDecision);
|
|
34
|
+
document.getElementById('add-column-form').addEventListener('submit', addColumn);
|
|
35
|
+
document.getElementById('import-form').addEventListener('submit', importModel);
|
|
36
|
+
|
|
37
|
+
// Decision actions
|
|
38
|
+
document.getElementById('add-decision-btn').addEventListener('click', () => {
|
|
39
|
+
openModal('add-decision-modal');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
document.getElementById('decision-select').addEventListener('change', (e) => {
|
|
43
|
+
loadDecision(e.target.value);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Table controls
|
|
47
|
+
document.getElementById('add-input-btn').addEventListener('click', () => {
|
|
48
|
+
document.getElementById('column-type').value = 'input';
|
|
49
|
+
document.getElementById('column-modal-title').textContent = 'Add Input Column';
|
|
50
|
+
document.getElementById('expression-group').style.display = 'block';
|
|
51
|
+
openModal('add-column-modal');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
document.getElementById('add-output-btn').addEventListener('click', () => {
|
|
55
|
+
document.getElementById('column-type').value = 'output';
|
|
56
|
+
document.getElementById('column-modal-title').textContent = 'Add Output Column';
|
|
57
|
+
document.getElementById('expression-group').style.display = 'none';
|
|
58
|
+
openModal('add-column-modal');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
document.getElementById('add-rule-btn').addEventListener('click', addRule);
|
|
62
|
+
document.getElementById('hit-policy-select').addEventListener('change', updateHitPolicy);
|
|
63
|
+
|
|
64
|
+
// Tabs
|
|
65
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
66
|
+
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Visualization
|
|
70
|
+
document.getElementById('visualize-graph-btn')?.addEventListener('click', visualizeGraph);
|
|
71
|
+
document.getElementById('visualize-tree-btn')?.addEventListener('click', visualizeTree);
|
|
72
|
+
|
|
73
|
+
// XML actions
|
|
74
|
+
document.getElementById('copy-xml-btn')?.addEventListener('click', copyXml);
|
|
75
|
+
document.getElementById('download-xml-btn')?.addEventListener('click', downloadXml);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// API Functions
|
|
79
|
+
async function apiCall(endpoint, method = 'GET', data = null) {
|
|
80
|
+
const options = {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (data) {
|
|
88
|
+
options.body = JSON.stringify(data);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const response = await fetch(`${state.baseUrl}/api/dmn${endpoint}`, options);
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const error = await response.json();
|
|
95
|
+
throw new Error(error.error || 'API request failed');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Model Management
|
|
102
|
+
async function loadModels() {
|
|
103
|
+
try {
|
|
104
|
+
const models = await apiCall('/models');
|
|
105
|
+
state.models = models;
|
|
106
|
+
renderModelList();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
showNotification('Failed to load models: ' + error.message, 'error');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderModelList() {
|
|
113
|
+
const list = document.getElementById('model-list');
|
|
114
|
+
|
|
115
|
+
if (state.models.length === 0) {
|
|
116
|
+
list.innerHTML = '<p class="empty-state">No models yet. Create one to get started.</p>';
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
list.innerHTML = state.models.map(model => `
|
|
121
|
+
<div class="model-item ${model.id === state.currentModel?.id ? 'active' : ''}"
|
|
122
|
+
onclick="loadModel('${model.id}')">
|
|
123
|
+
<h4>${escapeHtml(model.name)}</h4>
|
|
124
|
+
<p>${model.decision_count} decision(s)</p>
|
|
125
|
+
</div>
|
|
126
|
+
`).join('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function loadModel(modelId) {
|
|
130
|
+
try {
|
|
131
|
+
const model = await apiCall(`/models/${modelId}`);
|
|
132
|
+
state.currentModel = model;
|
|
133
|
+
renderModelEditor();
|
|
134
|
+
renderModelList();
|
|
135
|
+
|
|
136
|
+
// Hide welcome screen, show editor
|
|
137
|
+
document.getElementById('welcome-screen').style.display = 'none';
|
|
138
|
+
document.getElementById('editor-container').style.display = 'block';
|
|
139
|
+
|
|
140
|
+
// Load first decision if available
|
|
141
|
+
if (model.decisions && model.decisions.length > 0) {
|
|
142
|
+
loadDecision(model.decisions[0].id);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
showNotification('Failed to load model: ' + error.message, 'error');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function createNewModel(e) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
|
|
152
|
+
const name = document.getElementById('model-name-input').value;
|
|
153
|
+
const namespace = document.getElementById('model-namespace-input').value;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const model = await apiCall('/models', 'POST', { name, namespace });
|
|
157
|
+
state.models.push(model);
|
|
158
|
+
await loadModel(model.id);
|
|
159
|
+
closeModal('new-model-modal');
|
|
160
|
+
document.getElementById('new-model-form').reset();
|
|
161
|
+
showNotification('Model created successfully', 'success');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
showNotification('Failed to create model: ' + error.message, 'error');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderModelEditor() {
|
|
168
|
+
if (!state.currentModel) return;
|
|
169
|
+
|
|
170
|
+
document.getElementById('model-name').textContent = state.currentModel.name;
|
|
171
|
+
document.getElementById('model-id').textContent = `ID: ${state.currentModel.id}`;
|
|
172
|
+
|
|
173
|
+
// Populate decision selector
|
|
174
|
+
const select = document.getElementById('decision-select');
|
|
175
|
+
if (state.currentModel.decisions && state.currentModel.decisions.length > 0) {
|
|
176
|
+
select.innerHTML = state.currentModel.decisions.map(d =>
|
|
177
|
+
`<option value="${d.id}">${escapeHtml(d.name)}</option>`
|
|
178
|
+
).join('');
|
|
179
|
+
} else {
|
|
180
|
+
select.innerHTML = '<option value="">No decisions yet</option>';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Decision Management
|
|
185
|
+
async function addDecision(e) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
|
|
188
|
+
const decisionId = document.getElementById('decision-id-input').value;
|
|
189
|
+
const name = document.getElementById('decision-name-input').value;
|
|
190
|
+
const type = document.getElementById('decision-type-select').value;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const decision = await apiCall(`/models/${state.currentModel.id}/decisions`, 'POST', {
|
|
194
|
+
decision_id: decisionId,
|
|
195
|
+
name,
|
|
196
|
+
type
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
state.currentModel.decisions.push(decision);
|
|
200
|
+
renderModelEditor();
|
|
201
|
+
loadDecision(decision.id);
|
|
202
|
+
closeModal('add-decision-modal');
|
|
203
|
+
document.getElementById('add-decision-form').reset();
|
|
204
|
+
showNotification('Decision added successfully', 'success');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
showNotification('Failed to add decision: ' + error.message, 'error');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function loadDecision(decisionId) {
|
|
211
|
+
const decision = state.currentModel.decisions.find(d => d.id === decisionId);
|
|
212
|
+
if (!decision) return;
|
|
213
|
+
|
|
214
|
+
state.currentDecision = decision;
|
|
215
|
+
document.getElementById('decision-select').value = decisionId;
|
|
216
|
+
|
|
217
|
+
if (decision.decision_table) {
|
|
218
|
+
renderDecisionTable(decision.decision_table);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Decision Table
|
|
223
|
+
function renderDecisionTable(table) {
|
|
224
|
+
const headers = document.getElementById('table-headers');
|
|
225
|
+
const types = document.getElementById('table-types');
|
|
226
|
+
const body = document.getElementById('table-body');
|
|
227
|
+
|
|
228
|
+
// Set hit policy
|
|
229
|
+
document.getElementById('hit-policy-select').value = table.hit_policy;
|
|
230
|
+
|
|
231
|
+
// Render headers
|
|
232
|
+
let headerHtml = '<th class="rule-number">#</th>';
|
|
233
|
+
let typeHtml = '<th></th>';
|
|
234
|
+
|
|
235
|
+
table.inputs.forEach(input => {
|
|
236
|
+
headerHtml += `<th class="input-col">${escapeHtml(input.label)} <span class="col-type">(Input)</span></th>`;
|
|
237
|
+
typeHtml += `<th class="type-annotation">${input.type_ref || 'string'}</th>`;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
table.outputs.forEach(output => {
|
|
241
|
+
headerHtml += `<th class="output-col">${escapeHtml(output.label)} <span class="col-type">(Output)</span></th>`;
|
|
242
|
+
typeHtml += `<th class="type-annotation">${output.type_ref || 'string'}</th>`;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
headerHtml += '<th class="actions-col">Actions</th>';
|
|
246
|
+
typeHtml += '<th></th>';
|
|
247
|
+
|
|
248
|
+
headers.innerHTML = headerHtml;
|
|
249
|
+
types.innerHTML = typeHtml;
|
|
250
|
+
|
|
251
|
+
// Render rules
|
|
252
|
+
if (table.rules.length === 0) {
|
|
253
|
+
body.innerHTML = `<tr class="empty-row"><td colspan="${table.inputs.length + table.outputs.length + 2}">
|
|
254
|
+
No rules defined. Click "Add Rule" to create one.</td></tr>`;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
body.innerHTML = table.rules.map((rule, index) => {
|
|
259
|
+
let rowHtml = `<tr data-rule-id="${rule.id}">`;
|
|
260
|
+
rowHtml += `<td class="rule-number">${index + 1}</td>`;
|
|
261
|
+
|
|
262
|
+
rule.input_entries.forEach((entry, i) => {
|
|
263
|
+
rowHtml += `<td><input type="text" value="${escapeHtml(entry)}"
|
|
264
|
+
onchange="updateRuleEntry('${rule.id}', 'input', ${i}, this.value)"></td>`;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
rule.output_entries.forEach((entry, i) => {
|
|
268
|
+
rowHtml += `<td><input type="text" value="${escapeHtml(entry)}"
|
|
269
|
+
onchange="updateRuleEntry('${rule.id}', 'output', ${i}, this.value)"></td>`;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
rowHtml += `<td class="actions-col">
|
|
273
|
+
<button class="btn btn-sm btn-danger" onclick="deleteRule('${rule.id}')">Delete</button>
|
|
274
|
+
</td>`;
|
|
275
|
+
rowHtml += '</tr>';
|
|
276
|
+
|
|
277
|
+
return rowHtml;
|
|
278
|
+
}).join('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function addColumn(e) {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
|
|
284
|
+
const columnType = document.getElementById('column-type').value;
|
|
285
|
+
const id = document.getElementById('column-id-input').value;
|
|
286
|
+
const label = document.getElementById('column-label-input').value;
|
|
287
|
+
const typeRef = document.getElementById('column-type-input').value;
|
|
288
|
+
const expression = document.getElementById('column-expression-input').value;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const endpoint = `/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/${columnType}s`;
|
|
292
|
+
|
|
293
|
+
const data = {
|
|
294
|
+
[`${columnType}_id`]: id,
|
|
295
|
+
label,
|
|
296
|
+
type_ref: typeRef
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (columnType === 'input' && expression) {
|
|
300
|
+
data.expression = expression;
|
|
301
|
+
} else if (columnType === 'output') {
|
|
302
|
+
data.name = id;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await apiCall(endpoint, 'POST', data);
|
|
306
|
+
|
|
307
|
+
// Reload model to get updated state
|
|
308
|
+
await loadModel(state.currentModel.id);
|
|
309
|
+
loadDecision(state.currentDecision.id);
|
|
310
|
+
|
|
311
|
+
closeModal('add-column-modal');
|
|
312
|
+
document.getElementById('add-column-form').reset();
|
|
313
|
+
showNotification(`${columnType === 'input' ? 'Input' : 'Output'} column added successfully`, 'success');
|
|
314
|
+
} catch (error) {
|
|
315
|
+
showNotification('Failed to add column: ' + error.message, 'error');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function addRule() {
|
|
320
|
+
if (!state.currentDecision || !state.currentDecision.decision_table) return;
|
|
321
|
+
|
|
322
|
+
const table = state.currentDecision.decision_table;
|
|
323
|
+
const ruleId = `rule_${Date.now()}`;
|
|
324
|
+
const inputEntries = table.inputs.map(() => '-');
|
|
325
|
+
const outputEntries = table.outputs.map(() => '');
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules`, 'POST', {
|
|
329
|
+
rule_id: ruleId,
|
|
330
|
+
input_entries: inputEntries,
|
|
331
|
+
output_entries: outputEntries
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Reload model
|
|
335
|
+
await loadModel(state.currentModel.id);
|
|
336
|
+
loadDecision(state.currentDecision.id);
|
|
337
|
+
showNotification('Rule added successfully', 'success');
|
|
338
|
+
} catch (error) {
|
|
339
|
+
showNotification('Failed to add rule: ' + error.message, 'error');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function updateRuleEntry(ruleId, type, index, value) {
|
|
344
|
+
try {
|
|
345
|
+
const rule = state.currentDecision.decision_table.rules.find(r => r.id === ruleId);
|
|
346
|
+
if (!rule) return;
|
|
347
|
+
|
|
348
|
+
if (type === 'input') {
|
|
349
|
+
rule.input_entries[index] = value;
|
|
350
|
+
} else {
|
|
351
|
+
rule.output_entries[index] = value;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules/${ruleId}`, 'PUT', {
|
|
355
|
+
input_entries: rule.input_entries,
|
|
356
|
+
output_entries: rule.output_entries
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
showNotification('Failed to update rule: ' + error.message, 'error');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function deleteRule(ruleId) {
|
|
364
|
+
if (!confirm('Are you sure you want to delete this rule?')) return;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules/${ruleId}`, 'DELETE');
|
|
368
|
+
|
|
369
|
+
// Reload model
|
|
370
|
+
await loadModel(state.currentModel.id);
|
|
371
|
+
loadDecision(state.currentDecision.id);
|
|
372
|
+
showNotification('Rule deleted successfully', 'success');
|
|
373
|
+
} catch (error) {
|
|
374
|
+
showNotification('Failed to delete rule: ' + error.message, 'error');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function updateHitPolicy(e) {
|
|
379
|
+
if (!state.currentDecision || !state.currentDecision.decision_table) return;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}`, 'PUT', {
|
|
383
|
+
logic: { hit_policy: e.target.value }
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
showNotification('Hit policy updated successfully', 'success');
|
|
387
|
+
} catch (error) {
|
|
388
|
+
showNotification('Failed to update hit policy: ' + error.message, 'error');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Import/Export
|
|
393
|
+
async function exportModel() {
|
|
394
|
+
if (!state.currentModel) {
|
|
395
|
+
showNotification('No model selected', 'warning');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const xml = await apiCall(`/models/${state.currentModel.id}/export`);
|
|
401
|
+
|
|
402
|
+
// Update XML tab
|
|
403
|
+
document.getElementById('xml-content').innerHTML = `<code>${escapeHtml(xml)}</code>`;
|
|
404
|
+
|
|
405
|
+
showNotification('Model exported successfully', 'success');
|
|
406
|
+
} catch (error) {
|
|
407
|
+
showNotification('Failed to export model: ' + error.message, 'error');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function importModel(e) {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
|
|
414
|
+
const fileInput = document.getElementById('import-file-input');
|
|
415
|
+
const file = fileInput.files[0];
|
|
416
|
+
|
|
417
|
+
if (!file) {
|
|
418
|
+
showNotification('Please select a file', 'warning');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const xmlContent = await file.text();
|
|
424
|
+
const model = await apiCall('/models/import', 'POST', { xml: xmlContent });
|
|
425
|
+
|
|
426
|
+
state.models.push(model);
|
|
427
|
+
await loadModel(model.id);
|
|
428
|
+
closeModal('import-modal');
|
|
429
|
+
document.getElementById('import-form').reset();
|
|
430
|
+
showNotification('Model imported successfully', 'success');
|
|
431
|
+
} catch (error) {
|
|
432
|
+
showNotification('Failed to import model: ' + error.message, 'error');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function validateModel() {
|
|
437
|
+
if (!state.currentModel) {
|
|
438
|
+
showNotification('No model selected', 'warning');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const result = await apiCall(`/models/${state.currentModel.id}/validate`);
|
|
444
|
+
|
|
445
|
+
if (result.valid) {
|
|
446
|
+
showNotification('Model is valid!', 'success');
|
|
447
|
+
} else {
|
|
448
|
+
showNotification(`Validation failed: ${result.errors.join(', ')}`, 'error');
|
|
449
|
+
}
|
|
450
|
+
} catch (error) {
|
|
451
|
+
showNotification('Failed to validate model: ' + error.message, 'error');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Visualization
|
|
456
|
+
async function visualizeGraph() {
|
|
457
|
+
if (!state.currentModel) return;
|
|
458
|
+
|
|
459
|
+
const format = document.getElementById('graph-format-select').value;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const visualization = await apiCall(`/models/${state.currentModel.id}/visualize/graph?format=${format}`);
|
|
463
|
+
const container = document.getElementById('graph-visualization');
|
|
464
|
+
|
|
465
|
+
if (format === 'svg') {
|
|
466
|
+
container.innerHTML = visualization;
|
|
467
|
+
} else {
|
|
468
|
+
container.innerHTML = `<pre><code>${escapeHtml(visualization)}</code></pre>`;
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
showNotification('Failed to generate visualization: ' + error.message, 'error');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function visualizeTree() {
|
|
476
|
+
if (!state.currentDecision) return;
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
const svg = await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/visualize/tree`);
|
|
480
|
+
document.getElementById('tree-visualization').innerHTML = svg;
|
|
481
|
+
} catch (error) {
|
|
482
|
+
showNotification('Failed to generate tree visualization: ' + error.message, 'error');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function copyXml() {
|
|
487
|
+
const xmlContent = document.getElementById('xml-content').textContent;
|
|
488
|
+
navigator.clipboard.writeText(xmlContent);
|
|
489
|
+
showNotification('XML copied to clipboard', 'success');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function downloadXml() {
|
|
493
|
+
const xmlContent = document.getElementById('xml-content').textContent;
|
|
494
|
+
const blob = new Blob([xmlContent], { type: 'text/xml' });
|
|
495
|
+
const url = URL.createObjectURL(blob);
|
|
496
|
+
const a = document.createElement('a');
|
|
497
|
+
a.href = url;
|
|
498
|
+
a.download = `${state.currentModel.name}.dmn`;
|
|
499
|
+
a.click();
|
|
500
|
+
URL.revokeObjectURL(url);
|
|
501
|
+
showNotification('DMN file downloaded', 'success');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// UI Helpers
|
|
505
|
+
function switchTab(tabName) {
|
|
506
|
+
// Update tab buttons
|
|
507
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
508
|
+
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Update tab panes
|
|
512
|
+
document.querySelectorAll('.tab-pane').forEach(pane => {
|
|
513
|
+
pane.classList.toggle('active', pane.id === `${tabName}-tab`);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Load content based on tab
|
|
517
|
+
if (tabName === 'xml') {
|
|
518
|
+
exportModel();
|
|
519
|
+
} else if (tabName === 'graph') {
|
|
520
|
+
visualizeGraph();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function openModal(modalId) {
|
|
525
|
+
document.getElementById(modalId).classList.add('active');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function closeModal(modalId) {
|
|
529
|
+
document.getElementById(modalId).classList.remove('active');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function showNotification(message, type = 'info') {
|
|
533
|
+
const notification = document.getElementById('notification');
|
|
534
|
+
notification.textContent = message;
|
|
535
|
+
notification.className = `notification ${type} show`;
|
|
536
|
+
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
notification.classList.remove('show');
|
|
539
|
+
}, 3000);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function escapeHtml(text) {
|
|
543
|
+
const div = document.createElement('div');
|
|
544
|
+
div.textContent = text;
|
|
545
|
+
return div.innerHTML;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Close modals when clicking outside
|
|
549
|
+
window.addEventListener('click', (e) => {
|
|
550
|
+
if (e.target.classList.contains('modal')) {
|
|
551
|
+
e.target.classList.remove('active');
|
|
552
|
+
}
|
|
553
|
+
});
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
<header>
|
|
12
12
|
<h1>🎯 DecisionAgent Rule Builder</h1>
|
|
13
13
|
<p class="subtitle">Visual rule editor for non-technical users</p>
|
|
14
|
+
<div class="header-links">
|
|
15
|
+
<a href="/dmn/editor" class="btn btn-link">📊 DMN Editor</a>
|
|
16
|
+
</div>
|
|
14
17
|
</header>
|
|
15
18
|
|
|
16
19
|
<div class="main-layout">
|
|
@@ -181,12 +184,80 @@
|
|
|
181
184
|
<option value="between">between (range)</option>
|
|
182
185
|
<option value="modulo">modulo equals</option>
|
|
183
186
|
</optgroup>
|
|
187
|
+
<optgroup label="Mathematical Functions">
|
|
188
|
+
<option value="sin">sin (trigonometric)</option>
|
|
189
|
+
<option value="cos">cos (trigonometric)</option>
|
|
190
|
+
<option value="tan">tan (trigonometric)</option>
|
|
191
|
+
<option value="sqrt">sqrt (square root)</option>
|
|
192
|
+
<option value="power">power (exponentiation)</option>
|
|
193
|
+
<option value="exp">exp (exponential)</option>
|
|
194
|
+
<option value="log">log (natural logarithm)</option>
|
|
195
|
+
<option value="round">round</option>
|
|
196
|
+
<option value="floor">floor</option>
|
|
197
|
+
<option value="ceil">ceil</option>
|
|
198
|
+
<option value="abs">abs (absolute value)</option>
|
|
199
|
+
<option value="min">min (array minimum)</option>
|
|
200
|
+
<option value="max">max (array maximum)</option>
|
|
201
|
+
</optgroup>
|
|
202
|
+
<optgroup label="Statistical Aggregations">
|
|
203
|
+
<option value="sum">sum (array sum)</option>
|
|
204
|
+
<option value="average">average (array mean)</option>
|
|
205
|
+
<option value="mean">mean (array average)</option>
|
|
206
|
+
<option value="median">median (array median)</option>
|
|
207
|
+
<option value="stddev">stddev (standard deviation)</option>
|
|
208
|
+
<option value="standard_deviation">standard_deviation (stddev)</option>
|
|
209
|
+
<option value="variance">variance (array variance)</option>
|
|
210
|
+
<option value="percentile">percentile (Nth percentile)</option>
|
|
211
|
+
<option value="count">count (array length)</option>
|
|
212
|
+
</optgroup>
|
|
184
213
|
<optgroup label="Date/Time Operators">
|
|
185
214
|
<option value="before_date">before date</option>
|
|
186
215
|
<option value="after_date">after date</option>
|
|
187
216
|
<option value="within_days">within N days</option>
|
|
188
217
|
<option value="day_of_week">day of week</option>
|
|
189
218
|
</optgroup>
|
|
219
|
+
<optgroup label="Duration Calculations">
|
|
220
|
+
<option value="duration_seconds">duration (seconds)</option>
|
|
221
|
+
<option value="duration_minutes">duration (minutes)</option>
|
|
222
|
+
<option value="duration_hours">duration (hours)</option>
|
|
223
|
+
<option value="duration_days">duration (days)</option>
|
|
224
|
+
</optgroup>
|
|
225
|
+
<optgroup label="Date Arithmetic">
|
|
226
|
+
<option value="add_days">add days</option>
|
|
227
|
+
<option value="subtract_days">subtract days</option>
|
|
228
|
+
<option value="add_hours">add hours</option>
|
|
229
|
+
<option value="subtract_hours">subtract hours</option>
|
|
230
|
+
<option value="add_minutes">add minutes</option>
|
|
231
|
+
<option value="subtract_minutes">subtract minutes</option>
|
|
232
|
+
</optgroup>
|
|
233
|
+
<optgroup label="Time Components">
|
|
234
|
+
<option value="hour_of_day">hour of day (0-23)</option>
|
|
235
|
+
<option value="day_of_month">day of month (1-31)</option>
|
|
236
|
+
<option value="month">month (1-12)</option>
|
|
237
|
+
<option value="year">year</option>
|
|
238
|
+
<option value="week_of_year">week of year (1-52)</option>
|
|
239
|
+
</optgroup>
|
|
240
|
+
<optgroup label="Rate Calculations">
|
|
241
|
+
<option value="rate_per_second">rate per second</option>
|
|
242
|
+
<option value="rate_per_minute">rate per minute</option>
|
|
243
|
+
<option value="rate_per_hour">rate per hour</option>
|
|
244
|
+
</optgroup>
|
|
245
|
+
<optgroup label="Moving Window">
|
|
246
|
+
<option value="moving_average">moving average</option>
|
|
247
|
+
<option value="moving_sum">moving sum</option>
|
|
248
|
+
<option value="moving_max">moving max</option>
|
|
249
|
+
<option value="moving_min">moving min</option>
|
|
250
|
+
</optgroup>
|
|
251
|
+
<optgroup label="Financial Calculations">
|
|
252
|
+
<option value="compound_interest">compound interest</option>
|
|
253
|
+
<option value="present_value">present value</option>
|
|
254
|
+
<option value="future_value">future value</option>
|
|
255
|
+
<option value="payment">payment (loan PMT)</option>
|
|
256
|
+
</optgroup>
|
|
257
|
+
<optgroup label="String Aggregations">
|
|
258
|
+
<option value="join">join (array to string)</option>
|
|
259
|
+
<option value="length">length (string/array length)</option>
|
|
260
|
+
</optgroup>
|
|
190
261
|
<optgroup label="Collection Operators">
|
|
191
262
|
<option value="contains_all">contains all</option>
|
|
192
263
|
<option value="contains_any">contains any</option>
|
|
@@ -42,6 +42,7 @@ header {
|
|
|
42
42
|
background: linear-gradient(135deg, var(--primary-color), #6366f1);
|
|
43
43
|
color: white;
|
|
44
44
|
border-radius: 10px;
|
|
45
|
+
position: relative;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
header h1 {
|
|
@@ -49,6 +50,26 @@ header h1 {
|
|
|
49
50
|
margin-bottom: 10px;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
header .header-links {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 20px;
|
|
56
|
+
right: 20px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
header .header-links .btn-link {
|
|
60
|
+
color: white;
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
padding: 8px 16px;
|
|
63
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
background: rgba(255, 255, 255, 0.1);
|
|
66
|
+
transition: background 0.2s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
header .header-links .btn-link:hover {
|
|
70
|
+
background: rgba(255, 255, 255, 0.2);
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
.subtitle {
|
|
53
74
|
font-size: 1.1rem;
|
|
54
75
|
opacity: 0.9;
|