Changeset 0.14.0 (#67)

This commit is contained in:
2026-02-26 15:59:26 +00:00
parent 1a71658b24
commit e2149e249d
38 changed files with 5171 additions and 141 deletions

View File

@@ -402,32 +402,79 @@ function renderRoleConfig(containerEl, opts = {}) {
function filterModels(providerId, roleId) {
const typeFilter = Roles.typeFor(roleId);
return _models.filter(m =>
m[pidField] === providerId &&
(m.model_type || 'chat') === typeFilter
);
return _models.filter(m => {
if (m[pidField] !== providerId) return false;
const mt = m.model_type || '';
// Embedding role: include models typed as 'embedding' OR untyped.
// Many providers don't report type for embedding models.
if (typeFilter === 'embedding') return !mt || mt === 'embedding';
// Other roles: default untyped to 'chat' (existing behavior).
return (mt || 'chat') === typeFilter;
});
}
function onProviderChange(roleId, slot) {
const provSel = containerEl.querySelector(`[data-role-provider="${roleId}-${slot}"]`);
const modelSel = containerEl.querySelector(`[data-role-model="${roleId}-${slot}"]`);
const modelInput = containerEl.querySelector(`[data-role-model-input="${roleId}-${slot}"]`);
const toggleBtn = containerEl.querySelector(`[data-role-model-toggle="${roleId}-${slot}"]`);
if (!provSel || !modelSel) return;
modelSel.innerHTML = '<option value="">— select model —</option>';
const provId = provSel.value;
if (!provId) return;
filterModels(provId, roleId).forEach(m => {
const models = filterModels(provId, roleId);
models.forEach(m => {
const opt = document.createElement('option');
opt.value = m[midField];
opt.textContent = m[mnameField] || m[midField];
modelSel.appendChild(opt);
});
// Auto-switch to manual entry if dropdown has no models.
if (models.length === 0 && modelInput && toggleBtn) {
modelSel.style.display = 'none';
modelInput.style.display = '';
toggleBtn.title = 'Switch to dropdown';
toggleBtn.textContent = '▾';
}
}
function toggleManualEntry(roleId, slot) {
const modelSel = containerEl.querySelector(`[data-role-model="${roleId}-${slot}"]`);
const modelInput = containerEl.querySelector(`[data-role-model-input="${roleId}-${slot}"]`);
const toggleBtn = containerEl.querySelector(`[data-role-model-toggle="${roleId}-${slot}"]`);
if (!modelSel || !modelInput || !toggleBtn) return;
const showingDropdown = modelSel.style.display !== 'none';
if (showingDropdown) {
// Switch to manual: copy dropdown value into input.
modelInput.value = modelSel.value || modelInput.value;
modelSel.style.display = 'none';
modelInput.style.display = '';
toggleBtn.title = 'Switch to dropdown';
toggleBtn.textContent = '▾';
} else {
// Switch to dropdown: try to select the typed value.
modelSel.style.display = '';
modelInput.style.display = 'none';
toggleBtn.title = 'Type model ID manually';
toggleBtn.textContent = '✎';
if (modelInput.value) {
modelSel.value = modelInput.value;
}
}
}
function getBinding(roleId, slot) {
const prov = containerEl.querySelector(`[data-role-provider="${roleId}-${slot}"]`)?.value;
const model = containerEl.querySelector(`[data-role-model="${roleId}-${slot}"]`)?.value;
const modelSel = containerEl.querySelector(`[data-role-model="${roleId}-${slot}"]`);
const modelInput = containerEl.querySelector(`[data-role-model-input="${roleId}-${slot}"]`);
// Use manual input if it's visible, otherwise use dropdown.
const model = (modelInput && modelInput.style.display !== 'none')
? modelInput.value.trim()
: (modelSel?.value || '');
return (prov && model) ? { provider_config_id: prov, model_id: model } : null;
}
@@ -478,11 +525,15 @@ function renderRoleConfig(containerEl, opts = {}) {
`<option value="${p.id}"${p.id === selectedProv ? ' selected' : ''}>${esc(p.name)}</option>`
).join('');
const modelOptions = selectedProv
? filterModels(selectedProv, roleId).map(m =>
`<option value="${m[midField]}"${m[midField] === selectedModel ? ' selected' : ''}>${esc(m[mnameField] || m[midField])}</option>`
).join('')
: '';
const dropdownModels = selectedProv ? filterModels(selectedProv, roleId) : [];
const modelOptions = dropdownModels.map(m =>
`<option value="${m[midField]}"${m[midField] === selectedModel ? ' selected' : ''}>${esc(m[mnameField] || m[midField])}</option>`
).join('');
// Show manual entry if: saved model ID not in dropdown, or dropdown is empty with a provider selected.
const savedNotInDropdown = selectedModel && selectedProv &&
!dropdownModels.some(m => m[midField] === selectedModel);
const showManual = savedNotInDropdown || (selectedProv && dropdownModels.length === 0);
return `
<div class="form-row" style="gap:8px;align-items:center${slotName === 'fallback' ? ';margin-top:6px' : ''}">
@@ -491,10 +542,17 @@ function renderRoleConfig(containerEl, opts = {}) {
<option value="">${noneLabel}</option>
${provOptions}
</select>
<select data-role-model="${roleId}-${slotName}" style="min-width:200px">
<select data-role-model="${roleId}-${slotName}" style="min-width:200px${showManual ? ';display:none' : ''}">
<option value="">— select model —</option>
${modelOptions}
</select>
<input type="text" data-role-model-input="${roleId}-${slotName}"
placeholder="Model ID (e.g. text-embedding-3-small)"
value="${esc(showManual ? selectedModel : '')}"
style="min-width:200px;font-size:12px${showManual ? '' : ';display:none'}">
<button type="button" class="btn-small" data-role-model-toggle="${roleId}-${slotName}"
title="${showManual ? 'Switch to dropdown' : 'Type model ID manually'}"
style="padding:2px 6px;font-size:12px;line-height:1">${showManual ? '▾' : '✎'}</button>
</div>`;
}
@@ -553,6 +611,12 @@ function renderRoleConfig(containerEl, opts = {}) {
if (testBtn) { handleTest(testBtn.dataset.roleTest); return; }
const clearBtn = e.target.closest('[data-role-clear]');
if (clearBtn) { handleClear(clearBtn.dataset.roleClear); return; }
const toggleBtn = e.target.closest('[data-role-model-toggle]');
if (toggleBtn) {
const [roleId, slot] = toggleBtn.dataset.roleModelToggle.split('-');
toggleManualEntry(roleId, slot);
return;
}
});
return { refresh };