decision_agent 0.2.0 → 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 +3 -0
- data/lib/decision_agent/dsl/schema_validator.rb +2 -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/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 +3 -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/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 +1846 -1570
- 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 +65 -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">
|
|
@@ -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;
|