feat: add model groups dashboard page with CRUD UI
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Model Groups Management Page
|
||||
|
||||
class ModelGroupsPage {
|
||||
constructor() {
|
||||
this.container = document.getElementById('page-content');
|
||||
}
|
||||
|
||||
async render() {
|
||||
this.container.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h3>Model Groups</h3>
|
||||
<p class="text-muted">Define auto-routing groups that pick the best model for each request.</p>
|
||||
<button class="btn btn-primary" onclick="modelGroupsPage.showCreateForm()">
|
||||
<i class="fas fa-plus"></i> Add Group
|
||||
</button>
|
||||
</div>
|
||||
<div id="model-groups-list" class="table-container"></div>
|
||||
<div id="model-group-form" class="form-container" style="display:none;"></div>
|
||||
`;
|
||||
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 = '<div class="empty-state">No model groups defined. Create one to enable auto-routing.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<table class="data-table"><thead><tr>';
|
||||
html += '<th>Group ID</th><th>Strategy</th><th>Targets</th><th>Actions</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
groups.forEach(g => {
|
||||
html += '<tr>';
|
||||
html += '<td><code>' + this.esc(g.id) + '</code></td>';
|
||||
html += '<td><span class="badge">' + this.esc(g.strategy) + '</span></td>';
|
||||
html += '<td><code>' + this.esc(g.targets) + '</code></td>';
|
||||
html += '<td>';
|
||||
html += '<button class="btn btn-sm" onclick="modelGroupsPage.showEditForm(\'' + this.esc(g.id) + '\')">Edit</button> ';
|
||||
html += '<button class="btn btn-sm btn-danger" onclick="modelGroupsPage.deleteGroup(\'' + this.esc(g.id) + '\')">Delete</button>';
|
||||
html += '</td></tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
list.innerHTML = html;
|
||||
} catch (err) {
|
||||
document.getElementById('model-groups-list').innerHTML =
|
||||
'<div class="error-message">Failed to load model groups: ' + this.esc(err.message) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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 = `
|
||||
<h4>${isEdit ? 'Edit' : 'Create'} Model Group</h4>
|
||||
<form onsubmit="modelGroupsPage.saveGroup(event, ${isEdit})">
|
||||
<div class="form-control">
|
||||
<label>Group ID</label>
|
||||
<input type="text" id="mg-id" value="${this.esc(group ? group.id : '')}" ${isEdit ? 'readonly' : 'required'}
|
||||
placeholder="e.g. deepseek-auto">
|
||||
<small>Clients use this as the model name.</small>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Strategy</label>
|
||||
<select id="mg-strategy">
|
||||
<option value="heuristic" ${group && group.strategy === 'heuristic' ? 'selected' : ''}>Heuristic (rules-based)</option>
|
||||
<option value="classifier" ${group && group.strategy === 'classifier' ? 'selected' : ''}>Classifier (LLM judge)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label>Targets (JSON array)</label>
|
||||
<input type="text" id="mg-targets" value='${this.esc(group ? group.targets : '["cheap-model","smart-model"]')}' required>
|
||||
<small>First target = cheapest/fastest. Last target = smartest/most expensive.</small>
|
||||
</div>
|
||||
<div class="form-control" id="mg-selector-row" style="${group && group.strategy === 'classifier' ? '' : 'display:none'}">
|
||||
<label>Selector Model</label>
|
||||
<input type="text" id="mg-selector-model" value="${this.esc(group && group.selector_model ? group.selector_model : 'gpt-4o-mini')}"
|
||||
placeholder="Model used to judge task complexity">
|
||||
</div>
|
||||
<div class="form-control" id="mg-threshold-row" style="${group && group.strategy === 'classifier' ? '' : 'display:none'}">
|
||||
<label>Complexity Threshold</label>
|
||||
<input type="number" id="mg-threshold" value="${group && group.complexity_threshold ? group.complexity_threshold : ''}" min="1"
|
||||
placeholder="Tasks rated >= this go to the smart model">
|
||||
</div>
|
||||
<div class="form-control" id="mg-rules-row" style="${group && group.strategy === 'heuristic' ? '' : 'display:none'}">
|
||||
<label>Heuristic Rules (JSON array)</label>
|
||||
<textarea id="mg-rules" rows="4" placeholder='[{"pattern":"step by step","target":1}]'>${group && group.heuristic_rules ? group.heuristic_rules : ''}</textarea>
|
||||
<small>Pattern to match in user messages. target = index into targets array.</small>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn" onclick="document.getElementById('model-group-form').style.display='none'">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
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,'>').replace(/"/g,'"');
|
||||
}
|
||||
}
|
||||
|
||||
var modelGroupsPage = new ModelGroupsPage();
|
||||
Reference in New Issue
Block a user