diff --git a/static/index.html b/static/index.html index 11fd3c22..6f8adf94 100644 --- a/static/index.html +++ b/static/index.html @@ -89,6 +89,10 @@ Models + @@ -164,7 +168,7 @@ - + @@ -177,5 +181,6 @@ + diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 53a84b24..7420b681 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -119,6 +119,7 @@ class Dashboard { 'settings': 'Settings', 'logs': 'Logs', 'models': 'Models', + 'model-groups': 'Model Groups', 'users': 'User Management' }; if (titleElement) titleElement.textContent = titles[page] || 'Dashboard'; @@ -130,6 +131,11 @@ class Dashboard { if (content) { content.innerHTML = await this.getPageTemplate(page); await this.initializePageScript(page); + + // Model Groups page uses its own render method + if (page === 'model-groups' && typeof modelGroupsPage !== 'undefined') { + await modelGroupsPage.render(); + } } } catch (error) { console.error(`Error loading page ${page}:`, error); diff --git a/static/js/pages/model_groups.js b/static/js/pages/model_groups.js new file mode 100644 index 00000000..d8502986 --- /dev/null +++ b/static/js/pages/model_groups.js @@ -0,0 +1,168 @@ +// Model Groups Management Page + +class ModelGroupsPage { + constructor() { + this.container = document.getElementById('page-content'); + } + + async render() { + this.container.innerHTML = ` + +
+ + `; + await this.loadGroups(); + } + + async loadGroups() { + try { + const groups = await api.get('/api/model-groups'); + const list = document.getElementById('model-groups-list'); + if (!groups || groups.length === 0) { + list.innerHTML = '
No model groups defined. Create one to enable auto-routing.
'; + return; + } + + let html = ''; + html += ''; + html += ''; + + groups.forEach(g => { + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + + html += '
Group IDStrategyTargetsActions
' + this.esc(g.id) + '' + this.esc(g.strategy) + '' + this.esc(g.targets) + ''; + html += ' '; + html += ''; + html += '
'; + list.innerHTML = html; + } catch (err) { + document.getElementById('model-groups-list').innerHTML = + '
Failed to load model groups: ' + this.esc(err.message) + '
'; + } + } + + showCreateForm() { + this.renderForm(null); + } + + async showEditForm(id) { + try { + const groups = await api.get('/api/model-groups'); + const group = groups.find(g => g.id === id); + if (group) this.renderForm(group); + } catch (err) { + alert('Failed to load group: ' + err.message); + } + } + + renderForm(group) { + const isEdit = !!group; + const form = document.getElementById('model-group-form'); + form.style.display = 'block'; + form.innerHTML = ` +

${isEdit ? 'Edit' : 'Create'} Model Group

+
+
+ + + Clients use this as the model name. +
+
+ + +
+
+ + + First target = cheapest/fastest. Last target = smartest/most expensive. +
+
+ + +
+
+ + +
+
+ + + Pattern to match in user messages. target = index into targets array. +
+
+ + +
+
+ `; + + document.getElementById('mg-strategy').onchange = function() { + var isClassifier = this.value === 'classifier'; + document.getElementById('mg-selector-row').style.display = isClassifier ? '' : 'none'; + document.getElementById('mg-threshold-row').style.display = isClassifier ? '' : 'none'; + document.getElementById('mg-rules-row').style.display = isClassifier ? 'none' : ''; + }; + } + + async saveGroup(event, isEdit) { + event.preventDefault(); + var id = document.getElementById('mg-id').value.trim(); + var strategy = document.getElementById('mg-strategy').value; + var targets = document.getElementById('mg-targets').value; + var selectorModel = document.getElementById('mg-selector-model').value.trim() || null; + var thresholdVal = document.getElementById('mg-threshold').value; + var rules = document.getElementById('mg-rules').value.trim() || null; + + try { JSON.parse(targets); } catch (e) { alert('Targets must be valid JSON array'); return; } + if (rules) { try { JSON.parse(rules); } catch (e) { alert('Heuristic rules must be valid JSON'); return; } } + + var body = { id: id, strategy: strategy, targets: targets, selector_model: selectorModel, heuristic_rules: rules }; + if (thresholdVal) body.complexity_threshold = parseInt(thresholdVal); + + try { + if (isEdit) { + await api.put('/api/model-groups/' + encodeURIComponent(id), body); + } else { + await api.post('/api/model-groups', body); + } + document.getElementById('model-group-form').style.display = 'none'; + await this.loadGroups(); + } catch (err) { + alert('Failed to save: ' + err.message); + } + } + + async deleteGroup(id) { + if (!confirm('Delete model group "' + id + '"? This cannot be undone.')) return; + try { + await api.delete('/api/model-groups/' + encodeURIComponent(id)); + await this.loadGroups(); + } catch (err) { + alert('Failed to delete: ' + err.message); + } + } + + esc(str) { + if (!str) return ''; + return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); + } +} + +var modelGroupsPage = new ModelGroupsPage();