Files
GopherGate/static/js/pages/model_groups.js
T
hobokenchicken 4fef201e95
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
fix: remove /api prefix from model-groups API calls (api.js already prepends it)
2026-05-05 11:33:05 -04:00

169 lines
8.1 KiB
JavaScript

// 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('/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('/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('/model-groups/' + encodeURIComponent(id), body);
} else {
await api.post('/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('/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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
}
var modelGroupsPage = new ModelGroupsPage();